Chapter 8: Adding Objects to Your World
大綱
上一章學會了如何偵測平面,這章就是學如何擺放3D物件到真實世界中。
摘要
Getting started
先針對session的中斷情況進行預先的處理。
extension PortalViewController: ARSessionObserver {
func session(_ session: ARSession, didFailWithError error: Error) {
guard let label = self.sessionStateLabel else { return }
showMessage(error.localizedDescription, label: label, seconds: 3)
}
func sessionWasInterrupted(_ session: ARSession) {
guard let label = self.sessionStateLabel else { return }
showMessage("Session interrupted", label: label, seconds: 3)
}
func sessionInterruptionEnded(_ session: ARSession) {
guard let label = self.sessionStateLabel else { return }
showMessage("Session resumed", label: label, seconds: 3)
DispatchQueue.main.async {
self.removeAllNodes()
self.resetLabels()
}
runSession()
}
}
Hit testing
再來要準備置放物件到偵測到的平面。
當使用者透過手機螢幕控制虛擬世界的物體時,要如何轉化使用者在螢幕按壓的2D觸碰點到虛擬世界的3D座標系統。就是透過Hit testing, 這幾乎是ARKit裡面必用的基本招術。
var viewCenter: CGPoint {
let viewBounds = view.bounds
return CGPoint(x: viewBounds.width / 2.0, y: viewBounds.height / 2.0)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// 從viewCent進行射線,尋找是否有任何虛擬平面產生交點
if let hit = sceneView?.hitTest(viewCenter, types: [.existingPlaneUsingExtent]).first {
// 利用worldTransform轉換成3D
sceneView?.session.add(anchor: ARAnchor.init(transform: hit.worldTransform))
}
}
Adding crosshairs
在螢幕的中心點增加一個crosshairs(十字線),主要讓我們可以透過手機畫面來確認螢幕的中心的hit test是否正常。
當螢幕中心落到某個虛擬平面上時,會改變中心UI的crosshairs顏色。
// 每次frame被更新都會被呼叫到
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
// 永遠在main thread進行UI調整
DispatchQueue.main.async {
// 當螢幕中心落在3D平面之內,則改變crosschair顏色
if let _ = self.sceneView?.hitTest(self.viewCenter, types: [.existingPlaneUsingExtent]).first {
self.crosschair.backgroundColor = UIColor.green
} else {
self.crosschair.backgroundColor = UIColor.lightGray
}
}
}
Adding a state machine
接下來,就是這個app本身的邏輯。
當點到偵測的平面時,我們會置放一個白色立方體到平面上,並移除剛剛偵測到平面。透過額外設置的一些flag進行確認。
var portalNode: SCNNode? = nil
var isPortalPlaced = false
func makeProtal() -> SCNNode {
let protal = SCNNode()
let box = SCNBox(width: 1.0, height: 1.0, length: 1.0, chamferRadius: 0)
let boxNode = SCNNode(geometry: box)
protal.addChildNode(boxNode)
return protal
}
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
DispatchQueue.main.async {
// 當protal還沒被放置,且偵測到的又是平面,則繼續顯示debug平面
if let planeAnchor = anchor as? ARPlaneAnchor, !self.isPortalPlaced {
#if DEBUG
let debugPlaneNode = createPlaneNode(
center: planeAnchor.center,
extent: planeAnchor.extent)
node.addChildNode(debugPlaneNode)
self.debugPlanes.append(debugPlaneNode)
#endif
self.messageLabel?.alpha = 1.0
self.messageLabel?.text = """
Tap on the detected \
horizontal plane to place the portal
"""
}
// 如果protal還沒被放置放,但偵測到又不是平面,那一定就是剛剛user tap螢幕時所加的anchor
else if !self.isPortalPlaced {
// 加入protal到畫面上
self.portalNode = self.makeProtal()
if let protal = self.portalNode {
node.addChildNode(protal)
self.isPortalPlaced = true
// 移除debug plane
self.removeDebugPlanes()
self.sceneView?.debugOptions = []
// 更新UI文字
DispatchQueue.main.async {
self.messageLabel?.text = ""
self.messageLabel?.alpha = 0
}
}
}
}
}
Last updated
Was this helpful?