ARKit系列文章目錄node
譯者注:本文是Raywenderlich上《ARKit by Tutorials》免費章節的翻譯,是原書第8章.原書7~9章完成了一個時空門app.
官網原文地址www.raywenderlich.com/195388/buil…ios
本文是咱們書籍ARKit by Tutorials中的第8章,「添加物體到你的世界」.這本書向你展現瞭如何用蘋果的加強現實框架ARKit,來構建五個沉浸式的,好看的AR應用.開始吧!swift
在本系列上一章中,你已經學會了如何用ARKit創建你的app並探測水平面.在本章中,你將繼續構建你的app並經過SceneKit添加3D虛擬內容到相機場景中.在本章結束,你將會學到:數組
在開始以前,點擊 資料下載 來下載項目資料,並打開starter文件夾下的starter工程.session
如今你已經可以探測並渲染水平面了,還須要在session被打斷時重置狀態.當app進入後臺時,或當多個app處於前臺時ARSession就會被打斷.一旦被打斷後,視頻捕捉就會失敗,ARSession也不能再接收到傳感器的數據來追蹤了.當app返回前臺時,渲染出的平面仍然顯示在視圖上.然而,若是你的設備已經改變了位置或朝向,那麼ARSession追蹤就再也不有效了.這時你就須要重啓session.app
ARSCNViewDelegate實現了ARSessionObserver的協議.這個協議包含了一些方法,會在ARSession被打斷或出錯時被調用.框架
打開PortalViewController.swift,並添加下面的代理方法實現到已存在的類擴展中.ssh
// 1
func session(_ session: ARSession, didFailWithError error: Error) {
// 2
guard let label = self.sessionStateLabel else { return }
showMessage(error.localizedDescription, label: label, seconds: 3)
}
// 3
func sessionWasInterrupted(_ session: ARSession) {
guard let label = self.sessionStateLabel else { return }
showMessage("Session interrupted", label: label, seconds: 3)
}
// 4
func sessionInterruptionEnded(_ session: ARSession) {
// 5
guard let label = self.sessionStateLabel else { return }
showMessage("Session resumed", label: label, seconds: 3)
// 6
DispatchQueue.main.async {
self.removeAllNodes()
self.resetLabels()
}
// 7
runSession()
}
複製代碼
代碼詳解:async
你會看到有一些編譯錯誤.實現缺失的方法就能夠解決這些錯誤.ide
在PortalViewController的其餘變量下面添加一些變量:
var debugPlanes: [SCNNode] = []
複製代碼
你將會使用debugPlanes數組來保存在debug模式下渲染的全部水平面.
而後,在resetLabels() 下面添加新方法:
// 1
func showMessage(_ message: String, label: UILabel, seconds: Double) {
label.text = message
label.alpha = 1
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
if label.text == message {
label.text = ""
label.alpha = 0
}
}
}
// 2
func removeAllNodes() {
removeDebugPlanes()
}
// 3
func removeDebugPlanes() {
for debugPlaneNode in self.debugPlanes {
debugPlaneNode.removeFromParentNode()
}
self.debugPlanes = []
}
複製代碼
如今,在renderer(_:, didAdd:, for:) 中,#if DEBUG對應的**#endif**預處理指令前:
self.debugPlanes.append(debugPlaneNode)
複製代碼
這樣就將添加到場景的水平面也加入到debugPlanes數組中.
注意,在runSession() 中,session執行中須要傳入一個配置:
sceneView?.session.run(configuration)
複製代碼
將上面替換爲:
sceneView?.session.run(configuration,
options: [.resetTracking, .removeExistingAnchors])
複製代碼
這裏,你運行sceneView關聯的ARSession時,傳入一個configuration對象和一個ARSession.RunOptions數組,數組中有兩個設置項:
運行一下app,試着檢測一個水平面.
如今你已經準備好在檢測出的水平面上放置物體了.你將使用ARSCNView的命中測試來檢測,用戶手指在屏幕上的觸摸對應虛擬場景的哪裏.一個視圖座標下的2D點,實際對應着3D座標空間中的一條線.命中測試就是一個找到這條線上物體的過程.
打開PortalViewController.swift,添加下列變量.
var viewCenter: CGPoint {
let viewBounds = view.bounds
return CGPoint(x: viewBounds.width / 2.0, y: viewBounds.height / 2.0)
}
複製代碼
上面這段代碼,你設置變量viewCenter爲PortalViewController的視圖中心.
如今添加下面的方法:
// 1
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// 2
if let hit = sceneView?.hitTest(viewCenter, types: [.existingPlaneUsingExtent]).first {
// 3
sceneView?.session.add(anchor: ARAnchor.init(transform: hit.worldTransform))
}
}
複製代碼
代碼解釋:
錨點添加後,ARSCNView會在代理方法renderer(_:didAdd:for:) 中收到回調.從這裏開始你將處理時空門的渲染了.
在你添加時空門到場景中以前,還須要向視圖中添加最後一個東西.在一段文章中,你實現了檢測設備屏幕中心的sceneView上的命中測試.在本段中,你將會給屏幕中心的視圖上添加一個標記,來幫助用戶定位設備.
打開Main.storyboard.進入Object Library,搜索一個View對象.拖拽一個view對象到PortalViewController.
將view的名字改成Crosshair.添加約束確保其中心對準父控件中心.將width和height設置爲10.在Size Inspector頁面中,約束應該是這樣子:
選中assistant editor,你會看到PortalViewController.swift在右側.按住Ctrl從storyboard中的Crosshair上拖拽屬性到PortalViewController代碼中,放在sceneView上方.
在IBOutlet中輸入名字爲crosshair並點擊Connect.
/ 1
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
// 2
DispatchQueue.main.async {
// 3
if let _ = self.sceneView?.hitTest(self.viewCenter,
types: [.existingPlaneUsingExtent]).first {
self.crosshair.backgroundColor = UIColor.green
} else { // 4
self.crosshair.backgroundColor = UIColor.lightGray
}
}
}
複製代碼
代碼含義:
運行app.
四處移動設備,以便探測並渲染出水平面,以下左圖所示.如今移動你的設備讓設備屏幕中心落在平面內,以下右圖所示.注意中心view的顏色變成了綠色.
如今你已經創建起一個app,能探測平面並放置一個ARAnchor,你能夠開始添加時空門了.
爲了追蹤app的狀態,在PortalViewController中添加下列變量:
var portalNode: SCNNode? = nil
var isPortalPlaced = false
複製代碼
儲存一個SCNNode類型的portalNode對象來表示你的時空門,並使用isPortalPlaced來表示時空門是否已被渲染在場景中.
在PortalViewController中添加下列方法:
func makePortal() -> SCNNode {
// 1
let portal = SCNNode()
// 2
let box = SCNBox(width: 1.0,
height: 1.0,
length: 1.0,
chamferRadius: 0)
let boxNode = SCNNode(geometry: box)
// 3
portal.addChildNode(boxNode)
return portal
}
複製代碼
這裏咱們定義了makePortal() 方法,它能夠配置並渲染時空門.共作了下面幾件事:
這裏,makePortal() 只是建立一個包含立方體物體的時空門節點做爲佔位.
如今,用下面的方法替換renderer(_:, didAdd:, for:) 和renderer(_:, didUpdate:, for:) :
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
DispatchQueue.main.async {
// 1
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
"""
}
else if !self.isPortalPlaced {// 2
// 3
self.portalNode = self.makePortal()
if let portal = self.portalNode {
// 4
node.addChildNode(portal)
self.isPortalPlaced = true
// 5
self.removeDebugPlanes()
self.sceneView?.debugOptions = []
// 6
DispatchQueue.main.async {
self.messageLabel?.text = ""
self.messageLabel?.alpha = 0
}
}
}
}
}
func renderer(_ renderer: SCNSceneRenderer,
didUpdate node: SCNNode,
for anchor: ARAnchor) {
DispatchQueue.main.async {
// 7
if let planeAnchor = anchor as? ARPlaneAnchor,
node.childNodes.count > 0,
!self.isPortalPlaced {
updatePlaneNode(node.childNodes[0],
center: planeAnchor.center,
extent: planeAnchor.extent)
}
}
}
複製代碼
代碼說明:
最後,用下面的代碼替換removeAllNodes() .
func removeAllNodes() {
// 1
removeDebugPlanes()
// 2
self.portalNode?.removeFromParentNode()
// 3
self.isPortalPlaced = false
}
複製代碼
這個方法用來從場景中清理並移除全部渲染出的物體.詳情以下:
運行app;讓app探測到一個水平面,而後當屏幕上的準心變綠時,點擊屏幕.你將會看到一個扁平的,巨大的白色立方體.
這些內容至關有趣!這裏作一下本章總結:
若是你喜歡本系列教程,請購買本書的完整版,ARKit by Tutorials, available on our online store.
本章資料下載