原文链接:http://gad.qq.com/article/detail/32958
继续我们的学习。
相信今天大家都已经被苹果秋季发布会刷屏了。
当然,除了已经预热了近一年的iPhoneX,我们还有望见证帮主的另一份遗产-ApplePark。
临渊羡鱼不如退而结网,废话不多说,继续学习。
在特征点放置物体
可能有的朋友在上一步测试时发现了,有时候半天也检测不到平面(虽然几率不大),还以为程序崩溃了或者是有bug。显然我们不能指望用户在家里跑来跑去只会了找到一块ARKit能够识别到的平面,因此我们需要使用其它的方法来进行hitdetection。当我们无法找到平面时,将使用特征点作为替代。
要实现这一点相对比较容易,我们只需要修改touchesBegan(_:with:)方法即可。使用以下代码替代之前的该方法:
overridefunctouchesBegan(_touches:Set,withevent:UIEvent?){
iflethit=sceneView.hitTest(
viewCenter,
types:[.existingPlaneUsingExtent]).first{
sceneView.session.add(anchor:ARAnchor(transform:hit.worldTransform))
return
}elseiflethit=sceneView.hitTest(
viewCenter,
types:[.featurePoint]).last{
sceneView.session.add(anchor:ARAnchor(transform:hit.worldTransform))
return
}
}
这里我们添加了一种新的hittest类型-featurePoint,以便在我们找不到任何existingPlaneUsingExtent测试的结果时使用。特征点hit检测的结果按照从最近到最远的方式来排序,因此这里使用last结果,而非first,因为这样可以提供最佳的用户体验。
再次在手机上编译运行HomeHero。我们会发现hit检测的表现很不错,但远非完美:它会将物体放置在一些我们一无所知的特征点上。关于这一点,可以作为一个小小的挑战练习。
测量距离
现在我们已经实现了将虚拟的物体放置在真实世界中,接下来我们将实现另一个有趣的功能:测量距离。ARKit可以非常精确的放置和追踪物体位置,因此我们甚至用它来测量真实世界中的距离。
在SceneKit中,1个坐标点等同于真实世界中的1米。在HomeHero这个应用中,我们将放置两个AR球体,然后计算它们之间的距离,从而测量真实世界中的距离。为此,我们需要更改renderer(:didAdd:for:)方法,并在switch语句中实现measutre这种情况下的代码:
funcrenderer(_renderer:SCNSceneRenderer,didAddnode:SCNNode,foranchor:ARAnchor){
//
DispatchQueue.main.async{
ifletplaneAnchor=anchoras?ARPlaneAnchor{
#ifDEBUG
letplaneNode=createPlaneNode(center:planeAnchor.center,extent:planeAnchor.extent)
node.addChildNode(planeNode)
#endif
}else{
switchself.currentMode{
case.none:
break
case.placeObject(letname):
letmodelClone=nodeWithModelName(name)
self.objects.append(modelClone)
node.addChildNode(modelClone)
case.measure:
//1
letsphereNode=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中添加如下的新方法:
funcmeasure(fromNode:SCNNode,toNode:SCNNode){
//1
letmeasuringLineNode=createLineNode(fromNode:fromNode,toNode:toNode)
//2
measuringLineNode.name="MeasuringLine"
//3
sceneView.scene.rootNode.addChildNode(measuringLineNode)
objects.append(measuringLineNode)
//4
letdist=fromNode.position.distanceTo(toNode.position)
letmeasurementValue=String(format:"%.2f",dist)
//5
distanceLabel.text="Distance:\(measurementValue)m"
}
以上方法创建了两个节点之间的一条直线,其代码解释如下:
1.createLineNode(fromNode:toNode:)是起始项目中所提供的一个辅助方法。它的作用是创建两个节点之间的一条直线。
2.命名直线节点的名称,以便在后续删除。
3.将直线节点添加到场景中。
4.测量两个节点之间的距离。虚拟物体之间的距离和真实世界中的位置一一对应。
5.更新UI,向用户显示所测量的距离。
此外,我们还需要添加一些逻辑代码,从而根据球体的数量来更新测量状态。在HomeHeroViewController.swift中添加以下方法:
guardmeasuringNodes.count>1else{
return
}
letfirstNode=measuringNodes[0]
letsecondNode=measuringNodes[1]
//1
letshowMeasuring=self.measuringNodes.count==2
distanceLabel.isHidden=!showMeasuring
ifshowMeasuring{
measure(fromNode:firstNode,toNode:secondNode)
}elseifmeasuringNodes.count>2{
//2
firstNode.removeFromParentNode()
secondNode.removeFromParentNode()
measuringNodes.removeFirst(2)
//3
fornodeinsceneView.scene.rootNode.childNodes{
ifnode.name=="MeasuringLine"{
node.removeFromParentNode()
}
}
}
}
以上代码的解释如下:
1.仅当有两个球体时显示测量结果。
2.如果节点超过2个,则删除旧的测量节点
3.删除旧的测量直线。
接下来我们只需要在合适的时机来调用updateMeasuringNodes()方法即可。如果在方法renderer(_:didAdd:for:)中调用有点太早,因为此时在代理方法中传递的节点还没有一个可用的位置信息。因为renderer(_:didUpdate:for:)方法在renderer(_didAdd:for:)方法之后调用,在for 语句参数中所传递的节点包含了正确的场景信息,也就意味着我们在此时开始测量。
funcrenderer(_renderer:SCNSceneRenderer,didUpdatenode:SCNNode,foranchor:ARAnchor){
DispatchQueue.main.async{
ifletplaneAnchor=anchoras?ARPlaneAnchor{
updatePlaneNode(node.childNodes[0],center:planeAnchor.center,extent:planeAnchor.extent)
}else{
self.updateMeasuringNodes()
}
}
}
当我们调用updateMearingNodes方法时,当新的ARAnchor被添加、映射到SCNNode,以及更新时,测量的逻辑就会随之更新。
在手机上编译运行项目,可以来体会一下ARKit魔法一般的测量精度。为了实现更为精确的hit测试,我们可能需要找到一个真实世界的平面来尝试。
ARSessionstate
此前我们提到过,ARSession就好比ARKit的大脑,它会根据真实世界中的不同条件而产生不同的“心情”。当光照条件重发,或是屏幕上有足够多的细节时,它的表现可谓完美而精确。但是在其它一些情况下,也可能会有很糟糕的表现。因此,我们需要使用ARFrame中所提供的状态信息让用户知道ARKit是不是在发脾气~
在HomeHeroViewController.swift中添加以下方法:
funcupdateTrackingInfo(){
//1
guardletframe=sceneView.session.currentFrameelse{
return
}
//2
switchframe.camera.trackingState{
case.limited(letreason):
switchreason{
case.excessiveMotion:
trackingInfo.text="LimitedTracking:ExcessiveMotion"
case.insufficientFeatures:
trackingInfo.text="LimitedTracking:InsufficientDetails"
default:
trackingInfo.text="LimitedTracking"
}
default:
trackingInfo.text=""
}
//3
guard
letlightEstimate=frame.lightEstimate?.ambientIntensity
else{
return
}
//4
if(lightEstimate<100){
trackingInfo.text="LimitedTracking:TooDark"
}
}
以上代码的作用是获取当前的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
funcrenderer(_renderer:SCNSceneRenderer,updateAtTimetime:TimeInterval){
DispatchQueue.main.async{
//1
self.updateTrackingInfo()
//2
iflet_=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类型,就会变成绿色,从而向用户提示获取了高质量的hittest。
在手机上编译运行项目,然后尝试在一些相对比恶劣的光照条件下进行测试。
Sessioninterruptions(进程中断)
有些情况下ARSession会被中断,比如当我们让应用进入后台运行时。这种操作将会切断视频流,从而让ARSession完全失明。当进程被中断后,下次进入应用并继续进程时,设备的位置和旋转朝向等很可能完全发生了变化。此时,我们需要重新启动进程。
RSession通过ARSessionObserver协议向其代理发送所有的进程中断和常见错误信息。ARSCNViewDelegate中已经实现了ARSessionObserver,因此我们只需在HomeHeroViewController.swift中添加ARSCNViewDelegate对于以上方法的实现代码即可。
funcsession(_session:ARSession,didFailWithErrorerror:Error){
//1
showMessage(error.localizedDescription,label:messageLabel,seconds:2)
}
//2
funcsessionWasInterrupted(_session:ARSession){
showMessage("Sessioninterrupted",label:messageLabel,seconds:2)
}
funcsessionInterruptionEnded(_session:ARSession){
showMessage("Sessionresumed",label:messageLabel,seconds:2)
removeAllObjects()
runSession()
}
以上代码会处理大多数遇到的ARSession问题,这里详细解释一下:
1.showMessage(_:label:seconds:)是起始项目中所提供的辅助方法,它将在指定的时间段内以label的形式显示一条信息。
2.sessionWasInterrupted():)将在进程被中断时调用,比如当应用进入后台运行时。
3.removeAllObjects是起始项目中所提供的辅助方法。
在手机上编译运行项目,并让应用进入后台运行,然后恢复运行应用,看看会发生些神马。
至此,我们这个使用ARKit开发的简单示例教程就到此结束了。
接下来怎么学?
在这个教程之中,我们只是简单介绍了ARKit的核心组成部分。在此之外,我们要借助SceneKit和数学来实现更多的功能。
关于ARKit的更多知识,可以观看WWDC2017中的相关视频介绍:
http://apple.co/2t4UPlA
挑战与练习
在WWDC2017视频相关的链接中,我们可以找到官方的Demo应用,从而查看如何更精确的使用特征点来放置物体。这里的挑战就是通过阅读ARKitWWDCDemo应用的diamante来优化此前的hit 测试。此外,我们还需要一些三角和代数知识来更好的解决这一问题。
在示例代码的challenge文件夹中有最终的解决方案,可以供大家参考。
结束语
好了,使用ARKitiOS11betaSceneKit进行原生开发的教程到此结束。
后续所翻译或原创的教程会趋向于更实际的功能或应用。