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.
本章资料下载