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

发表于2017-09-13
评论21 3.8k浏览

继续我们的学习。

相信今天大家都已经被苹果秋季发布会刷屏了。

 

 

当然,除了已经预热了近一年的iPhone X,我们还有望见证帮主的另一份遗产-Apple Park

 

 

临渊羡鱼不如退而结网,废话不多说,继续学习。


在特征点放置物体

可能有的朋友在上一步测试时发现了,有时候半天也检测不到平面(虽然几率不大),还以为程序崩溃了或者是有bug。显然我们不能指望用户在家里跑来跑去只会了找到一块ARKit能够识别到的平面,因此我们需要使用其它的方法来进行hit detection。当我们无法找到平面时,将使用特征点作为替代。

要实现这一点相对比较容易,我们只需要修改touchesBegan(_:with:)方法即可。使用以下代码替代之前的该方法:

override func touchesBegan(_ touches: Set, with event: UIEvent?) {

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.我们可以从当前ARFrameARCamera对象中获取trackingState属性。trackingState 的枚举值limited提供了关联的TrackingStateReason值,可以告诉我们具体出现的追踪问题。

3.我们已经启用了ARWorldTrackingConfiguration的光线评估功能,因此可以从ARFramelightEstimate属性中获取光照信息。

4.ambientIntensity以流明为单位,如果小于100流明,表现环境过于昏暗,此时需要向用户发出提示。

我们需要在每一个渲染的frame中都更新追踪信息,因此需要在renderer(_:updateAtTime:)代理方法中实现这一点。在HomeHeroViewController.swiftARSCNViewDelegate 扩展中添加该方法:

//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中的相关视频介绍:

http://apple.co/2t4UPlA


挑战与练习

WWDC 2017视频相关的链接中,我们可以找到官方的Demo应用,从而查看如何更精确的使用特征点来放置物体。这里的挑战就是通过阅读ARKit WWDC Demo应用的diamante来优化此前的hit 测试。此外,我们还需要一些三角和代数知识来更好的解决这一问题。

在示例代码的challenge文件夹中有最终的解决方案,可以供大家参考。


结束语

好了,使用ARKit iOS 11 beta SceneKit进行原生开发的教程到此结束。

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

ARKit iOS 11原生开发入门(上)

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

标签: