ARKit教程07_第五章:表面檢測

前言

ARKit感興趣的同學,可以訂閱ARKit教程專題
源代碼地址在這里

正文

在本章中骗随,我們將學習如何檢測真實世界的曲面以及如何正確管理這些曲面的更新蚕愤。還將學習如何創(chuàng)建一個焦點光標香伴,通過光線投射將其置于檢測到的曲面之上。

可以在Chapter04這個項目上繼續(xù)開發(fā)台汇。你可以拷貝一份代碼,也可以新建一個項目篱瞎,把原來的實現(xiàn)邏輯再寫一遍苟呐。這一張我們需要一些擴展代碼,這些代碼:

  • GameUtils: 包含基本轉(zhuǎn)換函數(shù),可將弧度轉(zhuǎn)換為度,反之亦然俐筋。在處理旋轉(zhuǎn)和角度時或用得上這些函數(shù)牵素。

  • Generics: 添加 arc4random()的通用版本,這是生成隨機值的函數(shù)。

  • Random+Extension: 將 random() 函數(shù)擴展名添加到 Double 類型,以便可以輕松地在指定范圍內(nèi)生成隨機 Double 值校哎。

  • SCNVector3+Extension: 使用一些矢量數(shù)學函數(shù)擴展 SCNVector3 類型×讲ǎ現(xiàn)在,你可以添加、乘法和刪除矢量,獲取矢量長度,查找矢量之間的角度,甚至計算與其他矢量的距離闷哆。

添加game states

我們接下來需要實現(xiàn)的效果是檢測到一個表面腰奋,之后再做其他的操作。

定義game states

首先定義此游戲的所有可能游戲狀態(tài)抱怔。

ViewController.swift添加一個枚舉:

// MARK: - Game State
enum GameState: Int16 {
case detectSurface  // Scan playable surface (Plane Detection On)
case pointToSurface // Point to surface to see focus point (Plane Detection Off)
case swipeToPlay    // Focus point visible on surface, swipe up to play
}
  • detectSurfaceARKit 需要一段時間才能了解其環(huán)境和檢測表面劣坊。當游戲處于此狀態(tài)時, 用戶必須掃描其周圍環(huán)境以尋找合適的水平表面,如餐桌。一旦用戶確信 ARKit 檢測到了表面,他們可以點擊Start按鈕以進入下一個狀態(tài)屈留。

  • pointToSurface: 用戶現(xiàn)在必須將設備指向檢測到的曲面之一,使焦點光標變得可見局冰。焦點光標顯示目標點,指示撲克骰子的投擲位置。

  • swipeToPlay: 一旦用戶可以看到焦點,他們可以向上滑動,將手中的骰子投向?qū)构鈽恕?/p>

添加游戲狀態(tài)信息

現(xiàn)在,你已經(jīng)定義了一些游戲狀態(tài),現(xiàn)在需要一種方法來通知用戶他們可以在每個狀態(tài)下做什么灌危。

首先,添加一些新的屬性:

var gameState: GameState = .detectSurface 
var statusMessage: String = ""

上面的代碼主要做了如下工作:

  • gameState: 這是實際的游戲狀態(tài)屬性;它將包含游戲的當前狀態(tài)康二。將此選項設置為默認狀態(tài):detectSurface

  • statusMessage: 它包含要向用戶顯示的說明;說明會根據(jù)游戲狀態(tài)而變化勇蝙。

至此沫勿,我們需要一個更新狀態(tài)的函數(shù):

func updateStatus() {
// 1
switch gameState {
case .detectSurface:
  statusMessage = "Scan entire table surface...\nHit START when ready!"
case .pointToSurface:
  statusMessage = "Point at designated surface first!"
case .swipeToPlay:
  statusMessage = "Swipe UP to throw!\nTap on dice to collect it again."
}
// 2
self.statusLabel.text = trackingStatus != "" ?
  "\(trackingStatus)" : "\(statusMessage)"
 }

上述代碼把實時的gameState狀態(tài)信息呈現(xiàn)給用戶。現(xiàn)在拒用這個更新狀態(tài)的方法了味混。

renderer(_:updateAtTime):里面的這一行代碼可以注釋掉了:

//self.statusLabel.text = self.trackingStatus

狀態(tài)更新的操作最好放在主線程執(zhí)行:

 func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
    DispatchQueue.main.async {
      //self.statusLabel.text = self.trackingStatus
      self.updateStatus()
      self.updateFocusNode()
    }
}

錨點

你們對于錨點了解多少产雹?

ARKit 使用附加到 3D 內(nèi)容的虛擬錨點。其主要目的是在玩家移動設備時保持 3D 內(nèi)容相對于真實世界的位置翁锡。

ARAnchor 對象包含一個實際變換,該變換保持其位置和方向蔓挖。錨點不是可見元素,它不是可見元素。它只是一個在ARKit場景中維護的對象馆衔。默認情況下,ARKit 將每個 ARAnchor 與一個空的 SCNNode 配對瘟判。我們所要做的就是將 3D 內(nèi)容添加為該節(jié)點的子節(jié)點怨绣。

ARPlaneAnchor 對象是一種專用錨點類型,包含真實世界變換(位置和方向),包含其他平面信息,包括中心點、方向和曲面范圍荒适。然后,可以使用此信息創(chuàng)建相應的 SceneKit 平面節(jié)點梨熙。

其實,還有一個 ARFaceAnchor 錨點類型,后面會做介紹〉段埽現(xiàn)在,我們將只關注ARPlane錨點咽扇。

檢測表面

要使 ARKit 檢測真實表面需要啟用ARConfiguration對象。

要啟用該標志,轉(zhuǎn)到初始化部分,并在 sceneView.session.run(config)之前在initARSession() 內(nèi)添加以下行:

config.planeDetection = .horizontal

ARKit 現(xiàn)在將開始檢測水平表面,并為每個檢測到的表面自動生成 ARPlaneAnchor 實例陕壹。

注意:我們也可以使用.vertical來檢測垂直曲面质欲。

創(chuàng)建一個新的平面:

添加新平面錨點時,可以使用下面函數(shù)創(chuàng)建相應的可視組件。

func createARPlaneNode( 
    planeAnchor: ARPlaneAnchor, color: UIColor) -> SCNNode { 
    // Add code here
 }

函數(shù)傳入 ARPlanAnchor 以及 UIColor】饭荩現(xiàn)在,我們擁有生成 SceneKit 平面節(jié)點所需的所有信息嘶伟。

首先,生成平面幾何體。在createARPlaneNode()函數(shù)中添加以下內(nèi)容:

let planeGeometry = SCNPlane(
width: CGFloat(planeAnchor.extent.x), 
height: CGFloat(planeAnchor.extent.z))

這將使用錨點的范圍為平面的寬度和長度生成平面所需的幾何體又碌。

創(chuàng)建平面所需材質(zhì)

現(xiàn)在,我們需要通過創(chuàng)建材質(zhì)為幾何體提供一些紋理九昧。我們需要在createARPlaneNode()函數(shù)中添加如下代碼:

let planeMaterial = SCNMaterial() 
planeMaterial.diffuse.contents ="ARResource.scnassets/Textures/Surface_diffuse.png" 
planeGeometry.materials = [planeMaterial]

上述代碼創(chuàng)建一個新的材質(zhì),然后將其漫反射.內(nèi)容屬性設置到 Surface_diffuse.png 中包含的紋理。平面現(xiàn)在將具有紋理而不是平面顏色毕匀。

創(chuàng)建平面節(jié)點

接下來我們把下面的代碼添加到createARPlaneNode()函數(shù)中:

// 1 - Create plane node 
let planeNode = SCNNode(geometry: planeGeometry) 
// 2 planeNode.position = SCNVector3Make( 
planeAnchor.center.x, 0, planeAnchor.center.z) 
// 3 
planeNode.transform = SCNMatrix4MakeRotation(-Float.pi / 2, 1, 0, 0) 
// 4 
return planeNode

上面的代碼作用如下:

  • 1: 這將通過傳入生成的平面幾何來創(chuàng)建新平面節(jié)點。
  • 2: 這將基于錨點的中心點設置平面節(jié)點的位置皂岔。
  • 3: 默認情況下,SCNPlane 生成的幾何體是直立的,需要圍繞 x 軸順時針旋轉(zhuǎn)平面 90 度,才能將平面平放在曲面上蹋笼。
  • 4: 最后,新創(chuàng)建的平面將返回給調(diào)用者。

處理新的平面錨點

現(xiàn)在,我們已經(jīng)擁有了能夠創(chuàng)建 SceneKit 平面的幫助器函數(shù),是時候使用它了躁垛。

激活平面檢測后,ARKit 將自動開始為其檢測到的每個水平表面創(chuàng)建 ARPlane錨點剖毯。

將調(diào)用相應的renderer(_:didAdd:for)代理來通知新添加的錨點。我們只需等待事件觸發(fā)并為錨點創(chuàng)建相應的 SceneKit 平面教馆。

我們可以在renderer(_:didAdd:for)代理方法中這么處理:

// MARK: - Plane Management
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
    guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
    DispatchQueue.main.async {
        
        let planeNode = self.createARPlaneNode(planeAnchor: planeAnchor,
                                               color: UIColor.yellow.withAlphaComponent(0.5))
        node.addChildNode(planeNode)
    }
}

上述代碼作用如下:

  • 1: 通過代理方法來接收一個 SCNNode逊谋。這是一個新的空 SceneKit 節(jié)點。
  • 2: 對ARPlaneAnchor類型的節(jié)點做處理土铺,過濾掉其他類型的節(jié)點胶滋。
  • 3: 把相關操放在主線程執(zhí)行。
  • 4: 調(diào)用剛剛創(chuàng)建的 createPlane() 函數(shù),將錨點信息與顏色一起傳入進來舒憾。
  • 5: 提供的平面節(jié)點將添加為 ARKit 創(chuàng)建的節(jié)點的子節(jié)點镀钓。

更新平面

ARKit 可能最初未檢測到整個表面,因此,當用戶移動時,我們可能需要使用新信息更新先前檢測到的平面穗熬。

獲取平面幾何體

我們需要另一個函數(shù)來更新具有新位置镀迂、方向和尺寸的現(xiàn)有平面節(jié)點。

func updateARPlaneNode( 
  planeNode: SCNNode, planeAchor: ARPlaneAnchor) { // Add code here 
}

我們需要更新平面幾何體唤蔗。在updateARPlaneNode()函數(shù)中添加以下代碼:

let planeGeometry = planeNode.geometry as! SCNPlane 
planeGeometry.width = CGFloat(planeAchor.extent.x) 
planeGeometry.height = CGFloat(planeAchor.extent.z)

這將從平面節(jié)點檢索以前生成的平面幾何體;然后,它根據(jù)提供的平面錨點更新其寬度和高度信息探遵。

更新平面位置信息

接下來需要處理的是平面的位置窟赏,在updateARPlaneNode()函數(shù)中添加下面的代碼:

planeNode.position = SCNVector3Make(planeAchor.center.x, 0, planeAchor.center.z)

這將使用平面錨點提供的位置信息更新平面節(jié)點位置。

平面錨點更新的相關處理

最后,我們需要充分利用新的幫助器功能箱季。如果以前檢測到的曲面必須使用新信息進行更新,ARKit 將觸發(fā)renderer(_:didUpdate:for)代理方法涯穷。我們可以在代理方法中添加如下的代碼:

// 1 
func renderer(_ renderer: SCNSceneRenderer,

    didUpdate node: SCNNode, for anchor: ARAnchor) { 
// 2 
    guard let planeAnchor = anchor as? ARPlaneAnchor else { return } 
      // 3 
      DispatchQueue.main.async { 
    // 4 
    self.updateARPlaneNode(planeNode: node.childNodes[0], planeAchor: planeAnchor) 
     }
  }

上面的代碼作用如下:

  • 1: 這個方法里面接收到的參數(shù)是SCNNode,這是你之前存在平面里面的節(jié)點藏雏。

  • 2: 只是對ARPlaneAnchor類型的節(jié)點做操作拷况,過濾掉其他類型的節(jié)點

  • 3:把上述操作放在主線程中執(zhí)行。

  • 4:最后,這將調(diào)用新的 updatePlane() 函數(shù)掘殴。這個函數(shù)需要傳入第一個子節(jié)點以及關聯(lián)的平面錨點赚瘦。

創(chuàng)建焦點節(jié)點

現(xiàn)在,這個應用可以檢測表面,之前的一個模型奏寨,可以用上了:

image.png

Ray casting

光線投射是從屏幕中心(焦點)將虛擬光線投射到虛擬場景,同時查找與 3D 對象的交集的過程起意。

在現(xiàn)場。在此特定情況下,要查找場景中的光線和平面節(jié)點之間的交點病瞳。

光線與平面相交后,該交點位置將用于放置焦點節(jié)點揽咕。

創(chuàng)建聚焦點

我們首先需要定義用于光線投射測試的屏幕位置;這通常是屏幕的中心。在這種情況下,焦點節(jié)點比正常節(jié)點大一些套菜。

我們添加一個成員變量保存焦點的位置信息:

var focusPoint:CGPoint!

現(xiàn)在需要初始化該位置亲善。將以下代碼行添加到 initSceneView() 的底部:

focusPoint = CGPoint(x: view.center.x, y: view.center.y + view.center.y * 0.25)

這將使用屏幕高度低于視圖中心點 25% 的位置初始化對焦點。

方向更改的處理

要監(jiān)聽方向的更改笼踩,需要一個通知方法:

NotificationCenter.default.addObserver(self, selector: #selector(ViewController.orientationChanged), name: UIDevice.orientationDidChangeNotification, object: nil)

具體的實現(xiàn)如下:

@objc func orientationChanged() { 
    focusPoint = CGPoint(x: view.center.x, y: view.center.y + view.center.y * 0.25) 
}

上述代碼將焦點更新到視圖中心點以下 25% 的位置逗爹。

更新焦點節(jié)點

在焦點準備就緒后,我們還需要另一個函數(shù),該函數(shù)將根據(jù)屏幕的焦點持續(xù)更新焦點節(jié)點。

func updateFocusNode() {
    // 1 
    let results = self.sceneView.hitTest(self.focusPoint, types: [.existingPlaneUsingExtent]) 
    // 2 
    if results.count == 1 {
        if let match = results.first {
            // 3
            let t = match.worldTransform
            // 4 
            self.focusNode.position = SCNVector3( x: t.columns.3.x, y: t.columns.3.y, z: t.columns.3.z) self.gameState = .swipeToPlay 
            } 
        } else { 
            // 5 
            self.gameState = .pointToSurface 
        }
    }

上述代碼作用如下:

  • 1: sceneView.hitTest()執(zhí)行光線投射測試嚎于。用戶向它提供要觸發(fā)光線的屏幕位置;還需要提供要查找的對象類型掘而。在這種情況下,.existingPlaneUsingExtent指定我們僅根據(jù)其范圍查找檢測到的平面。然后,命中將存儲在結果中于购。

??我們還可以根據(jù)其他類型(如featurePoints(要素點)袍睡、estimatedHorizontalPlane(估計水平平面)和existingPlane(現(xiàn)有平面))執(zhí)行光線強制轉(zhuǎn)換。

  • 2: 只尋找第一個命中結果肋僧。找到后,即可更新焦點節(jié)點斑胜。
  • 3: 將使用命中結果的worldTransform,該矩陣包含位置、方向和縮放信息嫌吠。
  • 4: 根據(jù)命中結果變換矩陣更新焦點節(jié)點的位置止潘。位置信息可以在變換矩陣的第三列中找到。
  • 5: 最終,如果沒有找到命中結果,程序應繼續(xù)指示用戶指向檢測到的表面辫诅。

要完成操作,需要用updateFocusNode()方法來替代renderer(_:updateAtTime)

self.updateFocusNode()

可能前面說這么多有一些不太明白凭戴,運行一下程序,看看效果吧:

現(xiàn)在,檢測到的表面;焦點節(jié)點也應彈出炕矮。

現(xiàn)在會有平面重疊的現(xiàn)象么夫。ARKit 有時可能會將多個檢測到的平面合并到單個平面中者冤。為此, ARKit 需要在創(chuàng)建新平面之前刪除舊平面信息。這些操作档痪,我們可以在renderer(_:didRemove:for)代理方法中做處理涉枫。

    func removeARPlaneNode(node: SCNNode) {
    for childNode in node.childNodes {
        childNode.removeFromParentNode()
    }
  }

  func renderer(_ renderer: SCNSceneRenderer, didRemove node: SCNNode, for anchor: ARAnchor) {
    guard anchor is ARPlaneAnchor else { return }
    DispatchQueue.main.async {
        self.removeARPlaneNode(node: node)
    }
   }
上一章 目錄 下一章
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市腐螟,隨后出現(xiàn)的幾起案子愿汰,更是在濱河造成了極大的恐慌,老刑警劉巖乐纸,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件尼桶,死亡現(xiàn)場離奇詭異,居然都是意外死亡锯仪,警方通過查閱死者的電腦和手機泵督,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來庶喜,“玉大人小腊,你說我怎么就攤上這事【每撸” “怎么了秩冈?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長斥扛。 經(jīng)常有香客問我入问,道長,這世上最難降的妖魔是什么稀颁? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任芬失,我火速辦了婚禮,結果婚禮上匾灶,老公的妹妹穿的比我還像新娘棱烂。我一直安慰自己,他們只是感情好阶女,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布颊糜。 她就那樣靜靜地躺著,像睡著了一般秃踩。 火紅的嫁衣襯著肌膚如雪衬鱼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天憔杨,我揣著相機與錄音鸟赫,去河邊找鬼。 笑死,一個胖子當著我的面吹牛惯疙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播妖啥,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼霉颠,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了荆虱?” 一聲冷哼從身側響起蒿偎,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎怀读,沒想到半個月后诉位,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡菜枷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年苍糠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片啤誊。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡岳瞭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蚊锹,到底是詐尸還是另有隱情瞳筏,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布牡昆,位于F島的核電站姚炕,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏丢烘。R本人自食惡果不足惜柱宦,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望播瞳。 院中可真熱鬧捷沸,春花似錦、人聲如沸狐史。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽骏全。三九已至苍柏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間姜贡,已是汗流浹背试吁。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人熄捍。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓烛恤,卻偏偏與公主長得像,于是被迫代替她去往敵國和親余耽。 傳聞我的和親對象是個殘疾皇子缚柏,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355