ARKit iOS 11原生开发入门(下)
继续我们的学习。
相信今天大家都已经被苹果秋季发布会刷屏了。
当然,除了已经预热了近一年的iPhone X,我们还有望见证帮主的另一份遗产-Apple Park。
临渊羡鱼不如退而结网,废话不多说,继续学习。
在特征点放置物体
可能有的朋友在上一步测试时发现了,有时候半天也检测不到平面(虽然几率不大),还以为程序崩溃了或者是有bug。显然我们不能指望用户在家里跑来跑去只会了找到一块ARKit能够识别到的平面,因此我们需要使用其它的方法来进行hit detection。当我们无法找到平面时,将使用特征点作为替代。
要实现这一点相对比较容易,我们只需要修改touchesBegan(_:with:)方法即可。使用以下代码替代之前的该方法:
override
func touchesBegan(_ touches: Set
if let hit = sceneView.hitTest(
viewCenter,
types: [.existingPlaneUsingExtent]).first{
sceneView.session.add(anchor: ARAnchor(transform:hit.worldTransform))
return
}else if let hit = sceneView.hitTest(
viewCenter,
types: [.featurePoint]).last{
sceneView.session.add(anchor: ARAnchor(transform:hit.worldTransform))
return
}
}
这里我们添加了一种新的hit test类型-featurePoint,以便在我们找不到任何existingPlaneUsingExtent测试的结果时使用。特征点hit检测的结果按照从最近到最远的方式来排序,因此这里使用last结果,而非first,因为这样可以提供最佳的用户体验。
再次在手机上编译运行HomeHero。我们会发现hit检测的表现很不错,但远非完美:它会将物体放置在一些我们一无所知的特征点上。关于这一点,可以作为一个小小的挑战练习。
测量距离
现在我们已经实现了将虚拟的物体放置在真实世界中,接下来我们将实现另一个有趣的功能:测量距离。ARKit可以非常精确的放置和追踪物体位置,因此我们甚至用它来测量真实世界中的距离。
在SceneKit中,1个坐标点等同于真实世界中的1米。在HomeHero这个应用中,我们将放置两个AR球体,然后计算它们之间的距离,从而测量真实世界中的距离。为此,我们需要更改renderer(:didAdd:for:)方法,并在switch语句中实现measutre这种情况下的代码:
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
//
DispatchQueue.main.async {
if let planeAnchor = anchor as? ARPlaneAnchor{
#if DEBUG
let planeNode = createPlaneNode(center: planeAnchor.center, extent: planeAnchor.extent)
node.addChildNode(planeNode)
#endif
}else{
switch self.currentMode{
case .none:
break
case .placeObject(let name):
let modelClone = nodeWithModelName(name)
self.objects.append(modelClone)
node.addChildNode(modelClone)
case .measure:
//1
let sphereNode = createSphereNode(radius: 0.02)
//2
self.objects.append(sphereNode)
//3
node.addChildNode(sphereNode)
//4
self.measuringNodes.append(node)
}
}
}
}
以上所新增的代码将在我们选择了UI界面中的测量工具时创建测量用的节点,数字注释行的代码解释如下:
1.使用起始项目中所提供的createSphereNode(radius:)方法创建sphere节点。
2.在对象数组中添加这个新的对象。
3.将球体节点添加到传递给代理对象的节点中。
4.将球体节点添加到起始项目所提供的measureingNodes数组中,以便追踪测量节点。
接下来我们需要实现计算两个测量节点间距离的逻辑代码,在HomeHeroViewController.swift中添加如下的新方法:
func measure(fromNode:SCNNode, toNode:SCNNode){
//1
let measuringLineNode = createLineNode(fromNode: fromNode, toNode: toNode)
//2
measuringLineNode.name = "MeasuringLine"
//3
sceneView.scene.rootNode.addChildNode(measuringLineNode)
objects.append(measuringLineNode)
//4
let dist = fromNode.position.distanceTo(toNode.position)
let measurementValue = String(format:"%.2f",dist)
//5
distanceLabel.text = "Distance: \(measurementValue) m"
}
以上方法创建了两个节点之间的一条直线,其代码解释如下:
1.createLineNode(fromNode:toNode:)是起始项目中所提供的一个辅助方法。它的作用是创建两个节点之间的一条直线。
2.命名直线节点的名称,以便在后续删除。
3.将直线节点添加到场景中。
4.测量两个节点之间的距离。虚拟物体之间的距离和真实世界中的位置一一对应。
5.更新UI,向用户显示所测量的距离。
此外,我们还需要添加一些逻辑代码,从而根据球体的数量来更新测量状态。在HomeHeroViewController.swift中添加以下方法:
func updateMeasuringNodes(){
guard measuringNodes.count > 1 else{
return
}
let firstNode = measuringNodes[0]
let secondNode = measuringNodes[1]
//1
let showMeasuring = self.measuringNodes.count == 2
distanceLabel.isHidden = !showMeasuring
if showMeasuring{
measure(fromNode: firstNode, toNode: secondNode)
}else if measuringNodes.count > 2{
//2
firstNode.removeFromParentNode()
secondNode.removeFromParentNode()
measuringNodes.removeFirst(2)
//3
for node in sceneView.scene.rootNode.childNodes {
if node.name == "MeasuringLine"{
node.removeFromParentNode()
}
}
}
}
以上代码的解释如下:
1.仅当有两个球体时显示测量结果。
2.如果节点超过2个,则删除旧的测量节点
3.删除旧的测量直线。
接下来我们只需要在合适的时机来调用updateMeasuringNodes()方法即可。如果在方法renderer(_:didAdd:for:)中调用有点太早,因为此时在代理方法中传递的节点还没有一个可用的位置信息。因为renderer(_:didUpdate:for:)方法在renderer(_didAdd:for:)方法之后调用,在for 语句参数中所传递的节点包含了正确的场景信息,也就意味着我们在此时开始测量。
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
DispatchQueue.main.async {
if let planeAnchor = anchor as? ARPlaneAnchor{
updatePlaneNode(node.childNodes[0], center: planeAnchor.center, extent: planeAnchor.extent)
}else{
self.updateMeasuringNodes()
}
}
}
当我们调用updateMearingNodes方法时,当新的ARAnchor被添加、映射到SCNNode,以及更新时,测量的逻辑就会随之更新。
在手机上编译运行项目,可以来体会一下ARKit魔法一般的测量精度。为了实现更为精确的hit测试,我们可能需要找到一个真实世界的平面来尝试。
ARSession state
此前我们提到过,ARSession就好比ARKit的大脑,它会根据真实世界中的不同条件而产生不同的“心情”。当光照条件重发,或是屏幕上有足够多的细节时,它的表现可谓完美而精确。但是在其它一些情况下,也可能会有很糟糕的表现。因此,我们需要使用ARFrame中所提供的状态信息让用户知道ARKit是不是在发脾气~
在HomeHeroViewController.swift中添加以下方法:
func updateTrackingInfo(){
//1
guard let frame = sceneView.session.currentFrame else{
return
}
//2
switch frame.camera.trackingState{
case .limited(let reason):
switch reason{
case .excessiveMotion:
trackingInfo.text = "Limited Tracking: Excessive Motion"
case .insufficientFeatures:
trackingInfo.text = "Limited Tracking: Insufficient Details"
default:
trackingInfo.text = "Limited Tracking"
}
default:
trackingInfo.text = ""
}
//3
guard
let lightEstimate = frame.lightEstimate?.ambientIntensity
else {
return
}
//4
if (lightEstimate < 100){
trackingInfo.text = "Limited Tracking: Too Dark"
}
}
以上代码的作用是获取当前的ARFrame信息,并当环境条件恶劣时向用户发出提示。以下是具体的代码解释:
1.我们可以使用场景视图中ARSession对象的currentFrame属性来获取当前的ARFrame。
2.我们可以从当前ARFrame的ARCamera对象中获取trackingState属性。trackingState 的枚举值limited提供了关联的TrackingStateReason值,可以告诉我们具体出现的追踪问题。
3.我们已经启用了ARWorldTrackingConfiguration的光线评估功能,因此可以从ARFrame的lightEstimate属性中获取光照信息。
4.ambientIntensity以流明为单位,如果小于100流明,表现环境过于昏暗,此时需要向用户发出提示。
我们需要在每一个渲染的frame中都更新追踪信息,因此需要在renderer(_:updateAtTime:)代理方法中实现这一点。在HomeHeroViewController.swift的ARSCNViewDelegate 扩展中添加该方法:
//updateattime
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
DispatchQueue.main.async {
//1
self.updateTrackingInfo()
//2
if let _ = self.sceneView.hitTest(self.viewCenter, types: [.existingPlaneUsingExtent]).first{
self.crosshair.backgroundColor = UIColor.green
}else{
self.crosshair.backgroundColor = UIColor(white:0.34,alpha:1)
}
}
}
以上方法主要做了以下事情:
1.为每个渲染的frame更新追踪信息。
2.如果中间的点在进行hit 测试时符合existingPlaneUsingExtent类型,就会变成绿色,从而向用户提示获取了高质量的hit test。
在手机上编译运行项目,然后尝试在一些相对比恶劣的光照条件下进行测试。
Session interruptions(进程中断)
有些情况下ARSession会被中断,比如当我们让应用进入后台运行时。这种操作将会切断视频流,从而让ARSession完全失明。当进程被中断后,下次进入应用并继续进程时,设备的位置和旋转朝向等很可能完全发生了变化。此时,我们需要重新启动进程。
RSession通过ARSessionObserver协议向其代理发送所有的进程中断和常见错误信息。ARSCNViewDelegate中已经实现了ARSessionObserver,因此我们只需在HomeHeroViewController.swift中添加ARSCNViewDelegate对于以上方法的实现代码即可。
func session(_ session: ARSession, didFailWithError error: Error) {
//1
showMessage(error.localizedDescription, label: messageLabel, seconds: 2)
}
//2
func sessionWasInterrupted(_ session: ARSession) {
showMessage("Session interrupted", label: messageLabel, seconds: 2)
}
func sessionInterruptionEnded(_ session: ARSession) {
showMessage("Session resumed", label: messageLabel, seconds: 2)
removeAllObjects()
runSession()
}
以上代码会处理大多数遇到的ARSession问题,这里详细解释一下:
1.showMessage(_:label:seconds:)是起始项目中所提供的辅助方法,它将在指定的时间段内以label的形式显示一条信息。
2.sessionWasInterrupted():)将在进程被中断时调用,比如当应用进入后台运行时。
3.当sessionInterruptionEnded(_:)被调用时,我们应删除所有的对象,并通过之前所实现的runSession()方法来重启AR进程。removeAllObjects是起始项目中所提供的辅助方法。
在手机上编译运行项目,并让应用进入后台运行,然后恢复运行应用,看看会发生些神马。
至此,我们这个使用ARKit开发的简单示例教程就到此结束了。
接下来怎么学?
在这个教程之中,我们只是简单介绍了ARKit的核心组成部分。在此之外,我们要借助SceneKit和数学来实现更多的功能。
关于ARKit的更多知识,可以观看WWDC 2017中的相关视频介绍:
挑战与练习
在WWDC 2017视频相关的链接中,我们可以找到官方的Demo应用,从而查看如何更精确的使用特征点来放置物体。这里的挑战就是通过阅读ARKit WWDC Demo应用的diamante来优化此前的hit 测试。此外,我们还需要一些三角和代数知识来更好的解决这一问题。
在示例代码的challenge文件夹中有最终的解决方案,可以供大家参考。
结束语
好了,使用ARKit iOS 11 beta SceneKit进行原生开发的教程到此结束。
后续所翻译或原创的教程会趋向于更实际的功能或应用。