ARKit教程07_第五章:表面檢測(cè)

前言

對(duì)ARKit感興趣的同學(xué),可以訂閱ARKit教程專(zhuān)題
源代碼地址在這里

正文

在本章中,我們將學(xué)習(xí)如何檢測(cè)真實(shí)世界的曲面以及如何正確管理這些曲面的更新。還將學(xué)習(xí)如何創(chuàng)建一個(gè)焦點(diǎn)光標(biāo),通過(guò)光線(xiàn)投射將其置于檢測(cè)到的曲面之上。

可以在Chapter04這個(gè)項(xiàng)目上繼續(xù)開(kāi)發(fā)。你可以拷貝一份代碼,也可以新建一個(gè)項(xiàng)目,把原來(lái)的實(shí)現(xiàn)邏輯再寫(xiě)一遍。這一張我們需要一些擴(kuò)展代碼,這些代碼:

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

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

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

  • SCNVector3+Extension: 使用一些矢量數(shù)學(xué)函數(shù)擴(kuò)展 SCNVector3 類(lèi)型?,F(xiàn)在,你可以添加、乘法和刪除矢量,獲取矢量長(zhǎng)度,查找矢量之間的角度,甚至計(jì)算與其他矢量的距離。

添加game states

我們接下來(lái)需要實(shí)現(xiàn)的效果是檢測(cè)到一個(gè)表面,之后再做其他的操作。

定義game states

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

ViewController.swift添加一個(gè)枚舉:

// 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 需要一段時(shí)間才能了解其環(huán)境和檢測(cè)表面。當(dāng)游戲處于此狀態(tài)時(shí), 用戶(hù)必須掃描其周?chē)h(huán)境以尋找合適的水平表面,如餐桌。一旦用戶(hù)確信 ARKit 檢測(cè)到了表面,他們可以點(diǎn)擊Start按鈕以進(jìn)入下一個(gè)狀態(tài)。

  • pointToSurface: 用戶(hù)現(xiàn)在必須將設(shè)備指向檢測(cè)到的曲面之一,使焦點(diǎn)光標(biāo)變得可見(jiàn)。焦點(diǎn)光標(biāo)顯示目標(biāo)點(diǎn),指示撲克骰子的投擲位置。

  • swipeToPlay: 一旦用戶(hù)可以看到焦點(diǎn),他們可以向上滑動(dòng),將手中的骰子投向?qū)构鈽?biāo)。

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

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

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

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

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

  • gameState: 這是實(shí)際的游戲狀態(tài)屬性;它將包含游戲的當(dāng)前狀態(tài)。將此選項(xiàng)設(shè)置為默認(rèn)狀態(tài):detectSurface。

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

至此,我們需要一個(gè)更新?tīng)顟B(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)"
 }

上述代碼把實(shí)時(shí)的gameState狀態(tài)信息呈現(xiàn)給用戶(hù)?,F(xiàn)在拒用這個(gè)更新?tīng)顟B(tài)的方法了。

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

//self.statusLabel.text = self.trackingStatus

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

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

錨點(diǎn)

你們對(duì)于錨點(diǎn)了解多少?

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

ARAnchor 對(duì)象包含一個(gè)實(shí)際變換,該變換保持其位置和方向。錨點(diǎn)不是可見(jiàn)元素,它不是可見(jiàn)元素。它只是一個(gè)在ARKit場(chǎng)景中維護(hù)的對(duì)象。默認(rèn)情況下,ARKit 將每個(gè) ARAnchor 與一個(gè)空的 SCNNode 配對(duì)。我們所要做的就是將 3D 內(nèi)容添加為該節(jié)點(diǎn)的子節(jié)點(diǎn)。

ARPlaneAnchor 對(duì)象是一種專(zhuān)用錨點(diǎn)類(lèi)型,包含真實(shí)世界變換(位置和方向),包含其他平面信息,包括中心點(diǎn)、方向和曲面范圍。然后,可以使用此信息創(chuàng)建相應(yīng)的 SceneKit 平面節(jié)點(diǎn)。

其實(shí),還有一個(gè) ARFaceAnchor 錨點(diǎn)類(lèi)型,后面會(huì)做介紹?,F(xiàn)在,我們將只關(guān)注ARPlane錨點(diǎn)。

檢測(cè)表面

要使 ARKit 檢測(cè)真實(shí)表面需要啟用ARConfiguration對(duì)象。

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

config.planeDetection = .horizontal

ARKit 現(xiàn)在將開(kāi)始檢測(cè)水平表面,并為每個(gè)檢測(cè)到的表面自動(dòng)生成 ARPlaneAnchor 實(shí)例。

注意:我們也可以使用.vertical來(lái)檢測(cè)垂直曲面。

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

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

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

函數(shù)傳入 ARPlanAnchor 以及 UIColor?,F(xiàn)在,我們擁有生成 SceneKit 平面節(jié)點(diǎn)所需的所有信息。

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

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

這將使用錨點(diǎn)的范圍為平面的寬度和長(zhǎng)度生成平面所需的幾何體。

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

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

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

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

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

接下來(lái)我們把下面的代碼添加到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: 這將通過(guò)傳入生成的平面幾何來(lái)創(chuàng)建新平面節(jié)點(diǎn)。
  • 2: 這將基于錨點(diǎn)的中心點(diǎn)設(shè)置平面節(jié)點(diǎn)的位置。
  • 3: 默認(rèn)情況下,SCNPlane 生成的幾何體是直立的,需要圍繞 x 軸順時(shí)針旋轉(zhuǎn)平面 90 度,才能將平面平放在曲面上。
  • 4: 最后,新創(chuàng)建的平面將返回給調(diào)用者。

處理新的平面錨點(diǎn)

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

激活平面檢測(cè)后,ARKit 將自動(dòng)開(kāi)始為其檢測(cè)到的每個(gè)水平表面創(chuàng)建 ARPlane錨點(diǎn)。

將調(diào)用相應(yīng)的renderer(_:didAdd:for)代理來(lái)通知新添加的錨點(diǎn)。我們只需等待事件觸發(fā)并為錨點(diǎn)創(chuàng)建相應(yī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: 通過(guò)代理方法來(lái)接收一個(gè) SCNNode。這是一個(gè)新的空 SceneKit 節(jié)點(diǎn)。
  • 2: 對(duì)ARPlaneAnchor類(lèi)型的節(jié)點(diǎn)做處理,過(guò)濾掉其他類(lèi)型的節(jié)點(diǎn)。
  • 3: 把相關(guān)操放在主線(xiàn)程執(zhí)行。
  • 4: 調(diào)用剛剛創(chuàng)建的 createPlane() 函數(shù),將錨點(diǎn)信息與顏色一起傳入進(jìn)來(lái)。
  • 5: 提供的平面節(jié)點(diǎn)將添加為 ARKit 創(chuàng)建的節(jié)點(diǎn)的子節(jié)點(diǎn)。

更新平面

ARKit 可能最初未檢測(cè)到整個(gè)表面,因此,當(dāng)用戶(hù)移動(dòng)時(shí),我們可能需要使用新信息更新先前檢測(cè)到的平面。

獲取平面幾何體

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

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é)點(diǎn)檢索以前生成的平面幾何體;然后,它根據(jù)提供的平面錨點(diǎn)更新其寬度和高度信息。

更新平面位置信息

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

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

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

平面錨點(diǎn)更新的相關(guān)處理

最后,我們需要充分利用新的幫助器功能。如果以前檢測(cè)到的曲面必須使用新信息進(jìn)行更新,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: 這個(gè)方法里面接收到的參數(shù)是SCNNode,這是你之前存在平面里面的節(jié)點(diǎn)。

  • 2: 只是對(duì)ARPlaneAnchor類(lèi)型的節(jié)點(diǎn)做操作,過(guò)濾掉其他類(lèi)型的節(jié)點(diǎn)

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

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

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

現(xiàn)在,這個(gè)應(yīng)用可以檢測(cè)表面,之前的一個(gè)模型,可以用上了:

image.png

Ray casting

光線(xiàn)投射是從屏幕中心(焦點(diǎn))將虛擬光線(xiàn)投射到虛擬場(chǎng)景,同時(shí)查找與 3D 對(duì)象的交集的過(guò)程。

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

光線(xiàn)與平面相交后,該交點(diǎn)位置將用于放置焦點(diǎn)節(jié)點(diǎn)。

創(chuàng)建聚焦點(diǎn)

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

我們添加一個(gè)成員變量保存焦點(diǎn)的位置信息:

var focusPoint:CGPoint!

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

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

這將使用屏幕高度低于視圖中心點(diǎn) 25% 的位置初始化對(duì)焦點(diǎn)。

方向更改的處理

要監(jiān)聽(tīng)方向的更改,需要一個(gè)通知方法:

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

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

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

上述代碼將焦點(diǎn)更新到視圖中心點(diǎn)以下 25% 的位置。

更新焦點(diǎn)節(jié)點(diǎn)

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

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í)行光線(xiàn)投射測(cè)試。用戶(hù)向它提供要觸發(fā)光線(xiàn)的屏幕位置;還需要提供要查找的對(duì)象類(lèi)型。在這種情況下,.existingPlaneUsingExtent指定我們僅根據(jù)其范圍查找檢測(cè)到的平面。然后,命中將存儲(chǔ)在結(jié)果中。

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

  • 2: 只尋找第一個(gè)命中結(jié)果。找到后,即可更新焦點(diǎn)節(jié)點(diǎn)。
  • 3: 將使用命中結(jié)果的worldTransform,該矩陣包含位置、方向和縮放信息。
  • 4: 根據(jù)命中結(jié)果變換矩陣更新焦點(diǎn)節(jié)點(diǎn)的位置。位置信息可以在變換矩陣的第三列中找到。
  • 5: 最終,如果沒(méi)有找到命中結(jié)果,程序應(yīng)繼續(xù)指示用戶(hù)指向檢測(cè)到的表面。

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

self.updateFocusNode()

可能前面說(shuō)這么多有一些不太明白,運(yùn)行一下程序,看看效果吧:

現(xiàn)在,檢測(cè)到的表面;焦點(diǎn)節(jié)點(diǎn)也應(yīng)彈出。

現(xiàn)在會(huì)有平面重疊的現(xiàn)象。ARKit 有時(shí)可能會(huì)將多個(gè)檢測(cè)到的平面合并到單個(gè)平面中。為此, 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)
    }
   }
上一章 目錄 下一章
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容