现在我们已经完成了正确运行ARKit项目的所有基本设置,我们希望我们的设备能够坐在水平表面上。这是飞机检测。在本节中,我们将学习如何激活平面检测。我们将熟悉锚点以及如何使用它们将对象放置在锚点上。此外,我们将能够在现实生活中看到我们发现的飞机锚。从现在开始,我们将更多地投入到代码中。
要学习本教程,您需要Xcode 10或更高版本,以及来自Configuration for ARKit的最终Xcode项目。您可以下载本节的最终Xcode项目,以帮助您与自己的进度进行比较。
首先,我们需要打开配置的平面检测属性并将其设置为水平,以检测平面(如地板或桌子)。在配置声明下面写:
configuration.planeDetection = .horizontal
在ViewController类中,添加了一个委托ARSCNViewDelegate,以允许视图在渲染场景时接收信息。ARSCNViewDelegate是一种协议,它包含许多方法来帮助跟踪摄像机视图中的对象。方法就像程序或例程来实现某些东西。
在编程中,委托是一种设计模式,允许类将其职责委托给另一个对象。换句话说,就像要求别人为你做一份工作。在我们的例子中,ViewController将自己指定为ARSCNView的委托,委托者,从场景视图中检索内容的任务,管理其更新并处理其事件。
sceneView.delegate = self
一旦执行了任务,代表就会将信息报告回场景视图。
为了更多地了解Swift中的委派,我邀请您访问或查看本书第4章中的委托部分。
为了保持井井有条,让我们创建一个新文件来托管与ARSCNViewDelegate相关的所有代码。右键单击ViewController.swift并选择新建文件...。然后,在Source下选择Swift File,点击Next。将其命名为ViewController + ARSCNViewDelegate,然后命名为Create。
一旦创建了新的Swift文件ViewController + ARSCNViewDelegate.swift,就会自动导入Foundation框架。它是我们不需要的应用程序的基础框架。请改为使用以下框架替换它。
import SceneKit
import ARKit
这个文件将作为ViewController类的扩展,这里的代码将成为该类的一部分。为表明这一意图,请写下:
extension ViewController: ARSCNViewDelegate {
}
将显示错误消息:“ViewController”与协议“ARSCNViewDelegate”的冗余一致性。那是因为我们已经在同一个类中采用了ARSCNViewDelegate。在ViewController.swift文件中,将其删除。当我们在它时,向下滚动并删除Mark下的注释掉的代码,这是该协议下的一个方法的给定示例。Mark帮助我们分离文件中的代码段。
让我们回到ViewController + ARSCNViewDelegate.swift。现在,让我们从ARSCNViewDelegate实现一个新方法来查找表面。键入didAdd并在选项中选择渲染器。该didAdd方法当相机检测到物体会通知我们,然后标记的锚它。一个锚是类型的ARAnchor给出关于跟踪的对象的位置,取向和尺寸信息。ARAnchor有意用于在场景上放置虚拟对象。然后为该锚分配一个简称为节点的SCNNode。
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
}
我们不想要任何对象,更具体地说我们想要搜索平面。如果跟踪的对象是平面,让我们进行场景测试。从技术上讲,如果锚是一个ARPlaneAnchor。
if anchor is ARPlaneAnchor {
print("Horizontal surface detected")
} else {
return
}
如果确实如此,请在控制台“检测到水平表面”中打印以告知我们。否则,返回或退出方法。运行该应用程序以测试它。
返回委托文件,为planeAnchor声明一个常量。我们将使用它作为锚点来放置对象。
let planeAnchor = anchor as! ARPlaneAnchor
这意味着如果锚是平面,则将其类型转换为平面锚。
当我们运行应用程序时,我们可以在调试区域中看到找到水平表面时。但是在屏幕上看到它不是很好吗?为此,我们将添加一个函数来创建一个节点作为我们的视觉辅助。
func createPlane(planeAnchor: ARPlaneAnchor) -> SCNNode {}
该函数有一个名为参数planeAnchor型ARPlaneAnchor就像在不断didAdd方法。该函数将返回一个SCNNode,如右箭头所示。所以基本上,它输入一个平面锚并输出一个节点。
你应该在一个函数中错误地返回一个预期返回'SCNNode'的函数中的Missing return。不要担心,我们将继续编写代码并在最后添加缺少的返回值。
在此函数中,我们将为节点设置几何,并且该几何是平面。因此,使用其范围属性创建一个大小为planeAnchor的平面。
let plane = SCNPlane(width: CGFloat(planeAnchor.extent.x), height: CGFloat(planeAnchor.extent.z))
在这里,我们假设高度将被指定为y的范围。但是你看文档,y向量不存在,而z是要使用的。
您应该看到推荐的修复程序出现错误。只需单击Fix即可将Float类型的x extent值转换为CGFloat。为y做同样的事情。
接下来,让我们为飞机赋予纹理。我们将使用网格图像。
plane.firstMaterial?.diffuse.contents = UIImage(named: "grid")
然后,使用平面的几何创建一个名为planeNode的节点。
let planeNode = SCNNode(geometry: plane)
到现在为止,您应该已经识别出材料和漫反射这两个词。你在Scene Editor中看过它。您现在正在学习如何在代码中应用它。
所以,就像我们为手表所做的步骤一样,我们需要定位它。将平面节点放在检测到的曲面的中心。
planeNode.position = SCNVector3(planeAnchor.center.x, planeAnchor.center.y, planeAnchor.center.z)
最后,此函数需要在调用时返回一个值。
return planeNode
回到didAdd方法,让我们调用该函数。
let planeNode = createPlane(planeAnchor: planeAnchor)
然后,将planeNode作为表示平面的节点的子节点。
node.addChildNode(planeNode)
运行应用程序以查看网格。
检查网格时,您应该会看到一些问题。首先,网格是立起来的,另一个问题是你只能看到飞机一侧的网格。这是我们需要解决的两个问题。
好吧,还记得在我们第一次拖动飞机作为屏幕时的手表场景吗?它的默认方向是垂直的。嗯,这里也是如此。所以我们需要将它旋转90度。但是,Swift将角度存储在弧度中。如何将度数转换为弧度?我们应该回顾一下我们的高中数学。为了找到弧度的等价物,这里是等式。
根据图表,你会得到90度是pi的一半。在函数createPlane中,我们将在x轴上旋转网格以使其成为水平。也要顺时针旋转,在前面添加一个减号。
planeNode.eulerAngles.x = -.pi / 2
但严重的是,谁有时间甚至想要计算和转换度数和弧度?幸运的是,Swift有一个功能,GLKMathDegreesToRadians,所以利用它。注释掉前一行代码并替换为此代码。
planeNode.eulerAngles.x = GLKMathDegreesToRadians(-90)
此外,使网格图像覆盖平面的两侧以解决第二个问题。
plane.firstMaterial?.isDoubleSided = true
运行该应用程序以测试修复程序。因此,我们能够在检测到表面时将其可视化,在我的示例中是地板。但我们知道地板比那更大。不幸的是,当我四处走动时,网格并没有变大。
公式和图表
在我们继续之前,我想重构if else语句。有一种更好的方式来编写它。我想从这种方式开始,使其更容易理解。另一种选择是使用guard语句。Guard是另一种类似于if else语句的控制流。它有助于避免开发中的错误,因为它会强制程序在失败的情况下退出。从这开始,我们将在整个课程中使用guard。
替换此代码块:
if anchor is ARPlaneAnchor {
} else {
return
}
为这行代码:
guard anchor is ARPlaneAnchor else {return}
为了能够更新面锚点的大小,添加didUpdate后方法didAdd之一。
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {_
我们将采用与以前相同的想法。
guard anchor is ARPlaneAnchor else {return}
print("Horizontal surface updated")
运行应用程序。
与之前相同,我们将宣布一个planeAnchor。
let planeAnchor = anchor as! ARPlaneAnchor
更新平面锚点的尺寸的方法,我们首先必须将其从场景中删除,然后将其添加回来。对于的所有子节点的节点,从父节点删除它们。
node.enumerateChildNodes { (childNode, _) in
childNode.removeFromParentNode()
}
现在将其添加回场景,使用相同的功能创建另一个平面
let planeNode = createPlane(planeAnchor: planeAnchor)
node.addChildNode(planeNode)
再次运行该应用程序。您会看到在移动设备时,表面的大小会相应更新。
有时会发生错误。场景可以检测同一表面的多个锚点。我们可以通过添加didRemove方法来解决这个问题。
func renderer(_ renderer: SCNSceneRenderer, didRemove node: SCNNode, for anchor: ARAnchor) {
然后,通过应用与之前相同的代码来删除平面锚点。
guard anchor is ARPlaneAnchor else {return}
print("Horizontal surface removed")
node.enumerateChildNodes { (childNode, _) in
childNode.removeFromParentNode()
当我们上次预览我们导入的模型时,我们发现它只是漂浮在空中。检测平面锚点是允许我们添加模型,就像它们坐在它们上一样,使其成为更真实的体验。您在本教程中学到的内容不仅可以让您了解如何模拟真实曲面,还可以模拟现实生活中的事件。例如,您可以将物理应用于水平表面以使虚拟对象掉落,在其上驾驶汽车或在场景上为角色设置动画。
与此同时,我希望您能够在场景编辑器中学到的概念代码中受到教育。通过首先在视觉上向您介绍这些概念,我们相信它更容易掌握并且对代码处理不那么持怀疑态度。