干货 | ARKit iOS 11原生开发入门(下)

2017-09-18 18:04

原文链接: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进行原生开发的教程到此结束。

后续所翻译或原创的教程会趋向于更实际的功能或应用。

Ben

Ben

线上线下专访、稿件发布合作请联系QQ或微信:328624956

评论已关闭!

相关资讯