说明
译者注:本文是Raywenderlich上《ARKit by Tutorials》免费章节的翻译,是原书第7章.原书7~9章完成了一个时空门app.
官网原文地址www.raywenderlich.com/195361/buil…
本文是我们书籍ARKit by Tutorials中的第7章,“创建你的时空门”.这本书向你展示了如何用苹果的增强现实框架ARKit,来构建五个沉浸式的,好看的AR应用.开始吧
通过这一系列教程,你将用ARKit和SceneKit实现一个时空门应用.时空门类的app可以用于教育目的,比如一个太阳系虚拟浏览应用,或者一些休闲活动,比如享受一场虚拟的沙滩假期.
时空门app
在这个应用中,你将在现实世界中的某个水平面上,放置一个通往充满未来感的房间的虚拟门.你可以走进走出这个房间,探索里面有什么.
在该教程中,你将建立时空门应用的基础.在本教程中,你将学会如何:
- 建立一个ARSession
- 用ARKit检测并渲染水平面
你准备好建立通往另一个世界的通道了么?
开始
在Xcode中,打开starter工程, Portal.xcodeproj.创建并运行工程,你会看到一个空白屏幕.
啊,是的,一个空白充满机遇的画布!
打开Main.storyboard再展开Portal View Controller Scene
PortalViewController是应用启动后呈现给用户的界面.它包含一个ARSCNView来显示相机预览画面.还包含了两个UILabels来提供说明和反馈给用户.
现在,打开PortalViewController.swift.在这个文件中,你将看到下面的变量,它们代表了storyboard中的元素:
// 1
@IBOutlet var sceneView: ARSCNView?
// 2
@IBOutlet weak var messageLabel: UILabel?
// 3
@IBOutlet weak var sessionStateLabel: UILabel?
复制代码
让我们看看其中的内容:
- sceneView用来显示3D的SceneKit物体在相机视图上.
- messageLabel,它是个UILabel,会给用户展示说明性的消息.说明,告诉他们如何与你的app交互.
- sessionStateLabel,另一个UILabel,会通知用户session打断情况,例如当app进入后台或环境光不满足条件.
注意:ARKit会处理所有的传感器和相机数据,但它不会实际去渲染任何虚拟内容.要在你的场景中渲染内容,可以使用与ARKit协同的各种渲染器,比如SceneKit or SpriteKit.
ARSCNView是苹果提供的一个框架,你可以轻易将ARKit中数据与SceneKit融合在一起.使用ARSCNView会有很多好处,这就是为什么你要在本教程的项目中用它.
在starter工程中,你将会在Helpers分组下看到很多工具类.你会在app后面的开发中用到它们.
建立ARKit
第一步是用相机来捕捉视频流.为此,你需要使用ARSCNView对象.
打开PortalViewController.swift并添加下面的方法:
func runSession() {
// 1
let configuration = ARWorldTrackingConfiguration.init()
// 2
configuration.planeDetection = .horizontal
// 3
configuration.isLightEstimationEnabled = true
// 4
sceneView?.session.run(configuration)
// 5
#if DEBUG
sceneView?.debugOptions = [ARSCNDebugOptions.showFeaturePoints]
#endif
}
复制代码
代码说明:
- 首先实例化一个ARWorldTrackingConfiguration对象.它为ARSession定义了配置信息.对ARSession来说可用的配置类型有两种: ARSessionConfiguration和ARWorldTrackingConfiguration.
使用ARSessionConfiguration是不推荐的,因为它使用了设备的旋转信息,还没有位置.对于使用A9处理器的设置来说,ARWorldTrackingSessionConfiguration是最佳选择,因为它追踪了设备的所有运动. - configuration.planeDetection是设置为检测水平面.平面的范围可能会改变,并且随着相机的移动多个平面可能会合并成一个.它可以找到任何水平面例如地板,桌子或床.
- 它启用了灯光估计计算,可以被渲染框架利用来制造出更真实的虚拟物体.
- 使用指定的配置来启动session的AR进程.这将会启动ARKit session和视频捕捉,并显示在sceneView上.
- 调试设置,这样会添加可见的特征点;覆盖在相机视图上.
现在,是时候建立labels的默认设置了.用下面的代码替换resetLabels():
func resetLabels() {
messageLabel?.alpha = 1.0
messageLabel?.text =
"Move the phone around and allow the app to find a plane." +
"You will see a yellow horizontal plane."
sessionStateLabel?.alpha = 0.0
sessionStateLabel?.text = ""
}
复制代码
这将messageLabel和sessionStateLabel设置好透明度和文本.记住,messageLabel是用于展示给用户说明,而sessionStateLabel是用于展示错误信息的,以防出错.
现在,添加runSession()到PortalViewController中的viewDidLoad() 里面:
override func viewDidLoad() {
super.viewDidLoad()
resetLabels()
runSession()
}
复制代码
这将会在app启动并加载视图时运行ARKit session.
接着,构建并运行app.不要忘记--你需要给app授于相机访问权限.
ARSCNView完成了繁重的相机视频捕捉和显示任务.因为是调试模式,你可以看到渲染出的特征点,它们形成了点云,显示出场景分析的中间结果.
平面探测和渲染
先前,在runSession()中, 你设置了planeDetection为 .horizontal,这意味着你的app能探测水平面.你能够在ARSCNViewDelegate协议的代理回调方法中获得捕捉到的平面信息.
在PortalViewController中添加类扩展,实现ARSCNViewDelegate协议:
extension PortalViewController: ARSCNViewDelegate {
}
复制代码
在runSession() 末尾添加下面代码:
sceneView?.delegate = self
复制代码
这行代码将PortalViewController设置为sceneView对象的ARSCNViewDelegate代理.
ARPlaneAnchors会被自动添加到ARSession锚点数组中,并且ARSCNView自动将ARPlaneAnchor对象转换为SCNNode节点.
现在,要渲染这些平面,你需要做的是实现ARSCNViewDelegate代理方法:
// 1
func renderer(_ renderer: SCNSceneRenderer,
didAdd node: SCNNode,
for anchor: ARAnchor) {
// 2
DispatchQueue.main.async {
// 3
if let planeAnchor = anchor as? ARPlaneAnchor {
// 4
#if DEBUG
// 5
let debugPlaneNode = createPlaneNode(
center: planeAnchor.center,
extent: planeAnchor.extent)
// 6
node.addChildNode(debugPlaneNode)
#endif
// 7
self.messageLabel?.text =
"Tap on the detected horizontal plane to place the portal"
}
}
}
复制代码
代码含义:
- 当ARSession探测到新的平面时,renderer(_:didAdd:for:) 方法会被调用,并且ARSCNView自动为平面添加一个ARPlaneAnchor.
- 这个回调是在后台线程.这里,你需要派发到主线程,因为更新UI需要在主线程完成.
- 检查ARAnchor是否是一个ARPlaneAnchor.
- 检查是否在debug模式.
- 如果是,用ARKit探测到的planeAnchor的中心点和面积坐标来创建平面SCNNode节点.createPlaneNode() 是个帮助类的方法稍后实现.
- node对象是一个空的SCNNode,会被ARSCNView自动添加到场景中;它的坐标对准到ARAnchor的位置上.这里,你添加一个debugPlaneNode作为子节点,这样它就会被放置在节点的位置上.
- 最后,不管是否在debug模式,我们都为用户更新说明信息,以提示用户app现在已经准备好放置时空门到场景中了.
现在是时候建立帮助类的方法了.
创建一个新的Swift文件,命名为SCNNodeHelpers.swift.用来盛放渲染SCNNode对象相关的所有工具方法.
导入SceneKit到文件中:
import SceneKit
复制代码
现在,添加下面的帮助方法:
// 1
func createPlaneNode(center: vector_float3,
extent: vector_float3) -> SCNNode {
// 2
let plane = SCNPlane(width: CGFloat(extent.x),
height: CGFloat(extent.z))
// 3
let planeMaterial = SCNMaterial()
planeMaterial.diffuse.contents = UIColor.yellow.withAlphaComponent(0.4)
// 4
plane.materials = [planeMaterial]
// 5
let planeNode = SCNNode(geometry: plane)
// 6
planeNode.position = SCNVector3Make(center.x, 0, center.z)
// 7
planeNode.transform = SCNMatrix4MakeRotation(-Float.pi / 2, 1, 0, 0)
// 8
return planeNode
}
复制代码
代码解读:
- createPlaneNode方法有两个参数:要渲染平面的center和extent,类型都是vector_float3.这个类型表示点的坐标.该函数返回一个SCNNode类型的对象.
- 用指定的宽度和高度创建一个SCNPlane平面.宽度是extent中的x坐标,高度是z坐标.
- 初始化SCNMaterial对象并赋值漫反射内容.漫反射层颜色设置为半透明的黄色.
- SCNMaterial对象然后被添加到平面的materials数组中.这定义了平面的纹理和颜色.
- 创建一个带有plane几何体的SCNNode节点.SCNPlane继承于SCNGeometry类,它只提供了SceneKit渲染出的可见物体.通过将几何体附加到SCNNode对象来指定它的位置和朝向.多个节点可以引用同一个几何体对象,并允许在一个场景的不同位置出现.
- 设置planeNode的位置.注意,节点是根据ARKit上报的ARPlaneAnchor实例对象的信息被平移到坐标点 (center.x, 0, center.z) 处.
- SceneKit中的平面默认是竖直的,所以你需要旋转90度以使它呈水平状态.
- 该步返回前步创建的planeNode对象.
运行一下app,如果ARKit能探测到合适的平面,你就能看到一个黄色的水平面了.
移动一下设备,你会注意到app有时会显示多个平面.当它发现更多平面时,它将其加到视图中.然而,已经存在的平面,却不会随着ARKit分析出更多特征点而更新或改变尺寸.ARKit会根据新发现的特征点来持续更新平面的位置和尺寸.要想接收这些更新,在PortalViewController.swift中添加下列的renderer(_:didUpdate:for:) 代理方法:
// 1
func renderer(_ renderer: SCNSceneRenderer,
didUpdate node: SCNNode,
for anchor: ARAnchor) {
// 2
DispatchQueue.main.async {
// 3
if let planeAnchor = anchor as? ARPlaneAnchor,
node.childNodes.count > 0 {
// 4
updatePlaneNode(node.childNodes[0],
center: planeAnchor.center,
extent: planeAnchor.extent)
}
}
}
复制代码
解释:
- renderer(_:didUpdate:for:) 将会在相应的ARAnchor更新时被调用.
- 在主线程更新UI操作.
- 检查ARAnchor,确保是一个ARPlaneAnchor类型,并且它至少有一个子节点对应于平面的SCNNode.
- updatePlaneNode(_:center:extent:) 方法将会在稍后实现.它根据ARPlaneAnchor中的信息更新平面的坐标和尺寸.
打开SCNNodeHelpers.swift文件,添加下列代码:
func updatePlaneNode(_ node: SCNNode,
center: vector_float3,
extent: vector_float3) {
// 1
let geometry = node.geometry as? SCNPlane
// 2
geometry?.width = CGFloat(extent.x)
geometry?.height = CGFloat(extent.z)
// 3
node.position = SCNVector3Make(center.x, 0, center.z)
}
复制代码
代码解释:
- 检查节点是否有SCNPlane几何体.
- 使用传递过来的参数更新节点的几何体.用ARPlaneAnchor中的extent或size来更新平面的宽度和高度.
- 更新平面节点的位置到新位置上.
现在你可以成功地更新平面的位置了,运行一下app.你会看到平面的尺寸和位置会随着探测到新的特征点而调整.
还有一个问题需要解决.一旦app检测到平面,如果你退出app再重新回来,你会看到前一个探测到的平面还在相机视图上,显示在其他物体前面;它已经不再匹配先前的平面了.
要修复这个问题,你需要在ARSession被打断时移除平面节点.我们会在下一章处理这个问题.
下一步做什么?
你可能还没意识到,但你已经踏上了创建一个时空门app的漫漫长路!是的,还有很多要做的事,但是你已经在进入虚拟空间的路上了.
本章节简单总结:
- 探索starter项目,并复习了ARKit基础.
- 配置一个ARSession来在app中展示相机的输出.
- 添加平面检测和其他函数,以便app能使用ARSCNViewDelegate协议来渲染水平面.
在下一章教程中,你将会学习如何处理session的打断,及在视图中使用SceneKit来渲染3D物体.点击这里来继续本系列教程的第2部分!
如果你喜欢本教程,可以来查看我们的完整版书籍ARKit by Tutorials.