ARKit教程

ARKit教程11_第八章:在门户应用中添加物体

2019-08-14  本文已影响10人  张芳涛

前言

ARKit感兴趣的同学,可以订阅ARKit教程专题
源代码地址在这里

正文

在上一章中,我们学习了如何设置 iOS 应用以使用 ARKit 会话和检测水平平面。在本章中,我们将构建应用,并通过 SceneKit3D 虚拟内容添加到摄像机场景中。并且可以了解:

开始

到目前为止,我们可以检测和渲染水平平面,如果有任何中断,则需要重置会话的状态。当应用移动到后台或多个应用程序位于前台时, ARSession 将中断。一旦中断,视频捕获将失败, ARSession 将无法执行任何跟踪,因为它将不再接收所需的传感器数据。当应用返回到前台时,渲染的平面仍将存在于视图中。但是,如果设备已更改其位置或旋转, ARSession 跟踪将不再工作。这是我们需要重新启动会话时需要做的工作。

ARSCNViewDelegate 实现 ARSession 观察员协议。此协议包含 ARSession 检测到中断或会话错误时调用的方法。

 func session(_ session: ARSession, didFailWithError error: Error) {
    guard let label = self.sessionStateLabel else {return}
    showMessage(error.localizedDescription, label: label, second: 3.0)
}

func sessionWasInterrupted(_ session: ARSession) {
    guard let label = self.sessionStateLabel else {return}
    showMessage("Session interrupted", label: label, second: 3.0)
}

func sessionInterruptionEnded(_ session: ARSession) {
    guard let label = self.sessionStateLabel else {return}
    showMessage("Session resumed", label: label, second: 3.0)
    DispatchQueue.main.async {
        self.removeAllNodes()
        self.resetLabels()
    }
    runSession()
}

上面的代码作用如下:

我们需要添加以下代码:

var debugPlanes: [SCNNode] = []

我们将使用调试平面,它是 SCNNode 对象的数组,用于在调试模式下跟踪所有渲染的水平平面。

我们在resetLabels()方法中添加如下代码:

func showMessage(_ message: String, label: UILabel, second: Double){
    label.text = message
    label.alpha = 1
    DispatchQueue.main.asyncAfter(deadline: .now() + second) {
        if label.text == message{
            label.text = ""
            label.alpha = 0.0
        }
    }
}

func removeAllNodes(){
    removeDebugPlanes()
    self.portalNode?.removeFromParentNode()
    self.isPortalPlaced = false
}

func removeDebugPlanes(){
    for debugPlaneNode in self.debugPlanes {
        debugPlaneNode.removeFromParentNode()
    }
    debugPlanes = []
}

以上代码作用如下:

renderer(_:, didAdd:, for:)函数中,在#if DEBUG#endif之间添加如下代码:

self.debugPlanes.append(debugPlaneNode)

这会将刚刚添加到场景的水平平面添加到调试平面数组中。

请注意,在 runSession() 中,会话使用给定的配置执行:

sceneView?.session.run(configuration)

用以下代码替换上面的代码:

sceneView?.session.run(configuration, options: [.resetTracking, .removeExistingAnchors])

在这里,我们通过传递configuration对象和 ARSession.RunOptions 数组,使用以下运行选项运行与ARSession关联的 ARSessionView:

运行应用,效果如下:

现在将应用发送到后台,然后重新打开应用。 请注意,以前渲染的水平平面将从场景中删除,应用将重置标签以向用户显示正确的说明。

命中测试

现在,我们可以开始将对象放置在检测到的水平平面上。我们将使用 ARSCNView 的命中测试来检测屏幕上用户手指的触摸,以查看他们在虚拟场景中的着陆位置。视图坐标空间中的 2D 点可以引用 3D 坐标空间中线段沿线的任何点。命中测试是查找位于此线段的世界对象的过程。

打开ViewController.swift 并且添加下面的代码:

private var viewCenter: CGPoint{
    return view.center
}

上面的代码将viewCenter设置为view的中心位置。

我们还需要添加以下代码:

 override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    if let hit = sceneView.hitTest(viewCenter, types: [.existingPlaneUsingExtent]).first {
        sceneView.session.add(anchor: ARAnchor.init(transform: hit.worldTransform))
    }
}

上面代码作用如下:

添加锚点后,ARSCNView在委托方法renderer(_:didAdd:for:)中接收回调。这是我们处理呈现门户的地方。

添加十字准线

在将门户添加到场景之前,还需要在视图中添加最后一件事。我们需要添加一个标志性的view来给用户一个提示屏幕中央的位置:

@IBOutlet weak var crosshair: UIView!

我们在renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval)方法中添加如下代码:

 func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
    DispatchQueue.main.async {
        if let _ = self.sceneView.hitTest(self.viewCenter, types: [.existingPlaneUsingExtent]).first{
            self.crosshair.backgroundColor = UIColor.green
        }else{
            self.crosshair.backgroundColor = UIColor.lightGray
        }
    }
}

上面的代码作用如下:

移动设备,使其检测并渲染水平平面,如左侧所示。现在移动设备,使设备屏幕的中心位于平面内,如右图所示。

IMG_0067.PNG

添加状态机

现在我们已经设置了用于检测平面和放置ARAnchor的应用程序,我们可以开始添加门户网站。

要跟踪应用程序的状态,请将以下变量添加到PortalViewController

var portalNode: SCNNode? = nil 
var isPortalPlaced = false

我们将表示门户的SCNNode对象存储在portalNode中,并使用isPortalPlaced来跟踪是否在场景中呈现门户。

我们添加以下代码:

func makePortal() -> SCNNode{
    let portal = SCNNode()
    let box = SCNBox(width: 1.0, height: 1.0, length: 1.0, chamferRadius: 0.0)
    let boxNode = SCNNode(geometry: box)
    portal.addChildNode(boxNode)
    return portal
}

以上代码作用如下:

这里,makePortal()创建一个门户节点,其中包含一个box对象作为占位符。现在用以下内容替换SCNSceneRendererDelegaterenderer(_:, didAdd:, for:)renderer(_:, didUpdate:, for:)方法:

func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
    DispatchQueue.main.async {
        if let planeAnchor = anchor as? ARPlaneAnchor, !self.isPortalPlaced{
            #if DEBUG
            let debugPlaneNode = createPlaneNode(center: planeAnchor.center, extent: planeAnchor.extent)
            node.addChildNode(debugPlaneNode)
            #endif
            self.messageLabel.alpha = 1.0
            self.messageLabel.text = """
            Tap on the detected \
            horizontal plane to place the portal
            """
        }
        else if !self.isPortalPlaced {
            self.portalNode = self.makePortal()
            if let portal = self.portalNode{
                node.addChildNode(portal)
                self.isPortalPlaced = true
                self.removeDebugPlanes()
                self.sceneView.debugOptions = []
                DispatchQueue.main.async {
                    self.messageLabel.text = ""
                    self.messageLabel.alpha = 0.0
                }
            }
        }
    }
}

func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
    DispatchQueue.main.async {
        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(){
    removeDebugPlanes()
    self.portalNode?.removeFromParentNode()
    self.isPortalPlaced = false
}

此方法用于清除和从场景中删除所有渲染对象。内部代码作用如下:

构建并运行应用程序;让应用程序检测水平面,然后在十字准线视图变为绿色时点击屏幕。你会看到一个相当简洁,巨大的白色盒子。

现在是一个空白的盒子。下一章,我们将会对这个空白的盒子做一些更多的工作。

上一章 目录 下一章
上一篇下一篇

猜你喜欢

热点阅读