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()
  }

}

SIMD

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
            }
        }
      }
    }
  }

SIMD

Last updated

Was this helpful?