前言
對ARKit感興趣的同學,可以訂閱ARKit教程專題
源代碼地址在這里
正文
在本章中,我們將學習如何檢測真實世界的曲面以及如何正確管理這些曲面的更新。還將學習如何創建一個焦點光標,通過光線投射將其置于檢測到的曲面之上。
可以在Chapter04這個項目上繼續開發。你可以拷貝一份代碼,也可以新建一個項目,把原來的實現邏輯再寫一遍。這一張我們需要一些擴展代碼,這些代碼:
GameUtils: 包含基本轉換函數,可將弧度轉換為度,反之亦然。在處理旋轉和角度時或用得上這些函數。
Generics: 添加 arc4random()的通用版本,這是生成隨機值的函數。
Random+Extension: 將 random() 函數擴展名添加到 Double 類型,以便可以輕松地在指定范圍內生成隨機 Double 值。
SCNVector3+Extension: 使用一些矢量數學函數擴展 SCNVector3 類型。現在,你可以添加、乘法和刪除矢量,獲取矢量長度,查找矢量之間的角度,甚至計算與其他矢量的距離。
添加game states
我們接下來需要實現的效果是檢測到一個表面,之后再做其他的操作。
定義game states
首先定義此游戲的所有可能游戲狀態。
在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
}
detectSurface: ARKit 需要一段時間才能了解其環境和檢測表面。當游戲處于此狀態時, 用戶必須掃描其周圍環境以尋找合適的水平表面,如餐桌。一旦用戶確信 ARKit 檢測到了表面,他們可以點擊Start按鈕以進入下一個狀態。
pointToSurface: 用戶現在必須將設備指向檢測到的曲面之一,使焦點光標變得可見。焦點光標顯示目標點,指示撲克骰子的投擲位置。
swipeToPlay: 一旦用戶可以看到焦點,他們可以向上滑動,將手中的骰子投向對焦光標。
添加游戲狀態信息
現在,你已經定義了一些游戲狀態,現在需要一種方法來通知用戶他們可以在每個狀態下做什么。
首先,添加一些新的屬性:
var gameState: GameState = .detectSurface
var statusMessage: String = ""
上面的代碼主要做了如下工作:
gameState: 這是實際的游戲狀態屬性;它將包含游戲的當前狀態。將此選項設置為默認狀態:detectSurface。
statusMessage: 它包含要向用戶顯示的說明;說明會根據游戲狀態而變化。
至此,我們需要一個更新狀態的函數:
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狀態信息呈現給用戶。現在拒用這個更新狀態的方法了。
renderer(_:updateAtTime):里面的這一行代碼可以注釋掉了:
//self.statusLabel.text = self.trackingStatus
狀態更新的操作最好放在主線程執行:
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
DispatchQueue.main.async {
//self.statusLabel.text = self.trackingStatus
self.updateStatus()
self.updateFocusNode()
}
}
錨點
你們對于錨點了解多少?
ARKit 使用附加到 3D 內容的虛擬錨點。其主要目的是在玩家移動設備時保持 3D 內容相對于真實世界的位置。
ARAnchor 對象包含一個實際變換,該變換保持其位置和方向。錨點不是可見元素,它不是可見元素。它只是一個在ARKit場景中維護的對象。默認情況下,ARKit 將每個 ARAnchor 與一個空的 SCNNode 配對。我們所要做的就是將 3D 內容添加為該節點的子節點。
ARPlaneAnchor 對象是一種專用錨點類型,包含真實世界變換(位置和方向),包含其他平面信息,包括中心點、方向和曲面范圍。然后,可以使用此信息創建相應的 SceneKit 平面節點。
其實,還有一個 ARFaceAnchor 錨點類型,后面會做介紹。現在,我們將只關注ARPlane錨點。
檢測表面
要使 ARKit 檢測真實表面需要啟用ARConfiguration對象。
要啟用該標志,轉到初始化部分,并在 sceneView.session.run(config)之前在initARSession() 內添加以下行:
config.planeDetection = .horizontal
ARKit 現在將開始檢測水平表面,并為每個檢測到的表面自動生成 ARPlaneAnchor 實例。
注意:我們也可以使用.vertical來檢測垂直曲面。
創建一個新的平面:
添加新平面錨點時,可以使用下面函數創建相應的可視組件。
func createARPlaneNode(
planeAnchor: ARPlaneAnchor, color: UIColor) -> SCNNode {
// Add code here
}
函數傳入 ARPlanAnchor 以及 UIColor。現在,我們擁有生成 SceneKit 平面節點所需的所有信息。
首先,生成平面幾何體。在createARPlaneNode()函數中添加以下內容:
let planeGeometry = SCNPlane(
width: CGFloat(planeAnchor.extent.x),
height: CGFloat(planeAnchor.extent.z))
這將使用錨點的范圍為平面的寬度和長度生成平面所需的幾何體。
創建平面所需材質
現在,我們需要通過創建材質為幾何體提供一些紋理。我們需要在createARPlaneNode()函數中添加如下代碼:
let planeMaterial = SCNMaterial()
planeMaterial.diffuse.contents ="ARResource.scnassets/Textures/Surface_diffuse.png"
planeGeometry.materials = [planeMaterial]
上述代碼創建一個新的材質,然后將其漫反射.內容屬性設置到 Surface_diffuse.png 中包含的紋理。平面現在將具有紋理而不是平面顏色。
創建平面節點
接下來我們把下面的代碼添加到createARPlaneNode()函數中:
// 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: 這將通過傳入生成的平面幾何來創建新平面節點。
- 2: 這將基于錨點的中心點設置平面節點的位置。
- 3: 默認情況下,SCNPlane 生成的幾何體是直立的,需要圍繞 x 軸順時針旋轉平面 90 度,才能將平面平放在曲面上。
- 4: 最后,新創建的平面將返回給調用者。
處理新的平面錨點
現在,我們已經擁有了能夠創建 SceneKit 平面的幫助器函數,是時候使用它了。
激活平面檢測后,ARKit 將自動開始為其檢測到的每個水平表面創建 ARPlane錨點。
將調用相應的renderer(_:didAdd:for)代理來通知新添加的錨點。我們只需等待事件觸發并為錨點創建相應的 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 節點。
- 2: 對ARPlaneAnchor類型的節點做處理,過濾掉其他類型的節點。
- 3: 把相關操放在主線程執行。
- 4: 調用剛剛創建的 createPlane() 函數,將錨點信息與顏色一起傳入進來。
- 5: 提供的平面節點將添加為 ARKit 創建的節點的子節點。
更新平面
ARKit 可能最初未檢測到整個表面,因此,當用戶移動時,我們可能需要使用新信息更新先前檢測到的平面。
獲取平面幾何體
我們需要另一個函數來更新具有新位置、方向和尺寸的現有平面節點。
func updateARPlaneNode(
planeNode: SCNNode, planeAchor: ARPlaneAnchor) { // Add code here
}
我們需要更新平面幾何體。在updateARPlaneNode()函數中添加以下代碼:
let planeGeometry = planeNode.geometry as! SCNPlane
planeGeometry.width = CGFloat(planeAchor.extent.x)
planeGeometry.height = CGFloat(planeAchor.extent.z)
這將從平面節點檢索以前生成的平面幾何體;然后,它根據提供的平面錨點更新其寬度和高度信息。
更新平面位置信息
接下來需要處理的是平面的位置,在updateARPlaneNode()函數中添加下面的代碼:
planeNode.position = SCNVector3Make(planeAchor.center.x, 0, planeAchor.center.z)
這將使用平面錨點提供的位置信息更新平面節點位置。
平面錨點更新的相關處理
最后,我們需要充分利用新的幫助器功能。如果以前檢測到的曲面必須使用新信息進行更新,ARKit 將觸發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: 這個方法里面接收到的參數是SCNNode,這是你之前存在平面里面的節點。
2: 只是對ARPlaneAnchor類型的節點做操作,過濾掉其他類型的節點
3:把上述操作放在主線程中執行。
4:最后,這將調用新的 updatePlane() 函數。這個函數需要傳入第一個子節點以及關聯的平面錨點。
創建焦點節點
現在,這個應用可以檢測表面,之前的一個模型,可以用上了:
Ray casting
光線投射是從屏幕中心(焦點)將虛擬光線投射到虛擬場景,同時查找與 3D 對象的交集的過程。
在現場。在此特定情況下,要查找場景中的光線和平面節點之間的交點。
光線與平面相交后,該交點位置將用于放置焦點節點。
創建聚焦點
我們首先需要定義用于光線投射測試的屏幕位置;這通常是屏幕的中心。在這種情況下,焦點節點比正常節點大一些。
我們添加一個成員變量保存焦點的位置信息:
var focusPoint:CGPoint!
現在需要初始化該位置。將以下代碼行添加到 initSceneView() 的底部:
focusPoint = CGPoint(x: view.center.x, y: view.center.y + view.center.y * 0.25)
這將使用屏幕高度低于視圖中心點 25% 的位置初始化對焦點。
方向更改的處理
要監聽方向的更改,需要一個通知方法:
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.orientationChanged), name: UIDevice.orientationDidChangeNotification, object: nil)
具體的實現如下:
@objc func orientationChanged() {
focusPoint = CGPoint(x: view.center.x, y: view.center.y + view.center.y * 0.25)
}
上述代碼將焦點更新到視圖中心點以下 25% 的位置。
更新焦點節點
在焦點準備就緒后,我們還需要另一個函數,該函數將根據屏幕的焦點持續更新焦點節點。
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()執行光線投射測試。用戶向它提供要觸發光線的屏幕位置;還需要提供要查找的對象類型。在這種情況下,.existingPlaneUsingExtent指定我們僅根據其范圍查找檢測到的平面。然后,命中將存儲在結果中。
??我們還可以根據其他類型(如featurePoints(要素點)、estimatedHorizontalPlane(估計水平平面)和existingPlane(現有平面))執行光線強制轉換。
- 2: 只尋找第一個命中結果。找到后,即可更新焦點節點。
- 3: 將使用命中結果的worldTransform,該矩陣包含位置、方向和縮放信息。
- 4: 根據命中結果變換矩陣更新焦點節點的位置。位置信息可以在變換矩陣的第三列中找到。
- 5: 最終,如果沒有找到命中結果,程序應繼續指示用戶指向檢測到的表面。
要完成操作,需要用updateFocusNode()方法來替代renderer(_:updateAtTime):
self.updateFocusNode()
可能前面說這么多有一些不太明白,運行一下程序,看看效果吧:
現在,檢測到的表面;焦點節點也應彈出。
現在會有平面重疊的現象。ARKit 有時可能會將多個檢測到的平面合并到單個平面中。為此, ARKit 需要在創建新平面之前刪除舊平面信息。這些操作,我們可以在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)
}
}
上一章 | 目錄 | 下一章 |
---|