在上一个视频中,您学习了如何检测水平曲面并能够透视它。正如我所提到的,它们是放置物体的锚点。但是,在飞机上我们应该添加我们的物体?为此,我们需要在屏幕上选择一个点。在本节中,我们将形成并个性化焦点方块。我们将使用焦点方块跟随相机,直到我们对放置感到满意为止。我们将讨论世界变换和命中测试,这是ARKit的两个重要概念。
要学习本教程,您需要Xcode 10或更高版本,以及平面检测的最终Xcode项目。您可以下载本节的最终Xcode项目,以帮助您与自己的进度进行比较。
首先,我们将为Focus Square创建一个新类,以便我们可以个性化其风格和状态。让我们为焦点方块添加一个新的Swift文件。右键单击视图控制器+ ARSCNViewDelegate并选择新建文件...。然后,选择Swift File,单击Next。将其命名为FocusSquare,然后创建。现在,我们在FocusSquare.swift文件中。
接下来,替换** Foundation为SceneKit**。
然后,声明一个名为FocusSquare的新类,默认类型为SCNNode。要注意命名类的规则,它以大写字母开头。
在类中,我们将定义一个初始化程序,这样每当我们创建一个新的焦点方形节点时,它将执行一些额外的步骤。作为其父级,SCNNode类具有自己的属性。要添加新的,我们需要覆盖它。由于初始值设定项上没有必需参数,因此请将括号内的空白留空。
另外因为我们重写,请使用super.init()。这将调用SCNNode超类的默认初始化程序,并在我们使用自己的代码自定义之前设置所有内容。
override init() {
super.init()
}
您应该看到一个错误:'required' initializer 'init(coder:)' must be provided by subclass of 'SCNNode'。显然,此方法是必需的,因此单击“ Fix ”以实现它。我们甚至不必写它。感谢Xcode让我们的生活更轻松。
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
以与我们对网格相同的方式,让我们为焦点方块创建一个平面。后super.init(),声明一个平面并分配一个恒定宽度和高度的0.1这个时候。
let plane = SCNPlane(width: 0.1, height: 0.1)
plane.firstMaterial?.diffuse.contents = UIImage(named: "FocusSquare/close")
plane.firstMaterial?.isDoubleSided = true
geometry = plane
eulerAngles.x = GLKMathDegreesToRadians(-90)
然后,使用FocusSquare / close图像作为漫反射材质。也是双面的。将焦点方块的几何设置为我们刚刚定义的平面。这里,我们不需要planeNode,因为FocusSquare已经是一个节点。最后,旋转平面节点,使焦点方块与表格对齐,并且不垂直于表格。真棒,我们刚刚完成了课程,但我们还没有看到它。
为此,请转到ViewController.swift文件。我们将首先在sceneView声明之后为焦点方块声明一个类变量。它将是具有焦点方形类属性的节点。它也是一个可选项,因为有时它会在那里,有时候,它不是。两个名称之间的区别在于,类以大写字母F开头,而变量大小写为f。命名focusSquare的方法称为camel case,它是Swift中的标准命名约定。
var focusSquare: FocusSquare?
现在,是时候调用这个变量了。我们将在didAdd方法中生成焦点方块,仅在检测到表面时才在场景中显示。让我们首先设置一个安全措施,如果焦点平方为零,则继续。否则,退出。换句话说,如果它已经存在,那么不要创建一个新的。
guard focusSquare == nil else {return}
let focusSquareLocal = FocusSquare()
self.sceneView.scene.rootNode.addChildNode(focusSquareLocal)
self.focusSquare = focusSquareLocal
之后,创建焦点方形节点的新实例。这个将在本地使用,所以让我们在末尾添加单词Local以防止混淆。然后,通过将其添加到场景的根节点将其显示在屏幕上。最后,将其保存在稍后要使用的类变量下。运行该应用程序以查看我们的焦点方块。
我们现在能够看到它,但它的位置并不理想,就好像它是在相机的起始位置,这是世界起源。最重要的是,它是空闲的。我们希望它在场景中移动,以便我们可以选择一个位置来添加模型。
让我们回到ViewController.swift并为屏幕的中心声明另一个变量。我们将它用作焦点方块的参考点,以便在我们移动相机时跟随它们。屏幕中心始终存在,因此它不是可选的。
var screenCenter: CGPoint!
在viewDidLoad中,将屏幕的中心设置为视图的中心。
screenCenter = view.center
现在,让我们导航回ViewController + ARSCNViewDelegate。为了使焦点方向移动,我们将使用渲染器方法updateAtTime。这是为了指示代表每帧更新一次,并在系统当前时间更新。
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {}
在此方法中,创建另一个本地焦点方形节点。这一次,我们将确保在继续之前存在焦点方块。该focusSquare是我们以前存储的变量。如果有,则将其存储在局部变量中以更新场景。
guard let focusSquareLocal = focusSquare else {return}
我之前提到过,我们希望使用屏幕中心作为焦点方块的基准。我们知道屏幕中心是2D点,我们甚至将其定义为CGPoint。然而,为了在场景上定位节点,我们需要3D坐标。那么,我们如何将某些东西从2D转换为3D呢?答案是hitTest,这是一种ARHitTestResult方法,用于搜索与2D点和这些对象相交的真实世界对象。然后,它沿着相机指向的线对应于y坐标向2D点添加第三维。在代码中,我们解释为:
let hitTest = sceneView.hitTest(screenCenter, types: .existingPlane)
这确定了屏幕中心与检测到的水平表面的交点。
命中测试返回结果列表,我们只想要这些结果的第一个元素。第一个元素是离相机最近的平面。例如,如果您将相机对准您的桌子,则您希望桌子不是地板。
let hitTestResult = hitTest.first
命中测试的目的是检索表面的位置。并且该位置存储在世界变换中。世界变换是命中测试结果相对于世界坐标的节点变换属性。简而言之,这些结果包含有关变换的信息,如方向,位置和比例。
guard let worldTransform = hitTestResult?.worldTransform else {return}
世界变换是一个4x4矩阵,位置保留在第四列。因为矩阵是多维数组并且数组的值从0开始,所以第四列的数量是3。
let worldTransformColumn3 = worldTransform.columns.3
最后,将该位置指定给焦点方块。同时,它会随着相机的移动而更新。
focusSquareLocal.position = SCNVector3(worldTransformColumn3.x, worldTransformColumn3.y, worldTransformColumn3.z)
现在,运行应用程序。
接下来,我们想对焦点方块进行其他类型的更新。在viewWillDisappear之后的ViewController.swift中,为更新创建一个新函数。
func updateFocusSquare() {}
在那里,再次使用类变量在本地实例化一个新的焦点方块。另外,请确保它是第一手存在的。
guard let focusSquareLocal = focusSquare else {return}
现在,我们将进行另一次热门测试。但是这一次,我们将使用现有平面的范围,这意味着它将取决于平面的大小。原因是我们使用焦点方块告诉我们该点是否可以用作锚点,而不仅仅是用于查看目的。
let hitTest = sceneView.hitTest(screenCenter, types: .existingPlaneUsingExtent)
像以前一样,获得命中测试的第一个结果,我们将检查它是否击中了飞机。
if let hitTestResult = hitTest.first {
print("Focus square hits a plane")
} else {
print("Focus square does not hit a plane")
}
让我们在updateAtTime方法中调用updateFocusSquare()。我们需要使用DispatchQueue.main.async来在主线程中进行更新,这意味着在UI上,因为我们正在后台线程上执行代码。self绝对是必需的,因为它在一个闭包中并引用了ViewController类。不要太担心它,随着时间的推移,你将会理解所有这些对象,属性和闭包。
DispatchQueue.main.async {self.updateFocusSquare()}
再次运行应用程序并注意控制台。
我们如何为焦点方块添加漂亮的触感?您可能已经意识到我们有两个用于焦点方块的资产图像,一个是开放的,一个是关闭的。这应该会给你一个提示,我们都会在不同情况下使用它们。因此,在FocusSquare类中,让我们将一个变量isClosed作为布尔值(true或false)添加,以在打开和关闭状态之间切换图像。默认情况下,我们将其设置为true,因为它只在我们检测到曲面时才会显示在屏幕上。如果isClosed为true,请使用图像FocusSquare / close。如果没有,请改用FocusSquare / open。
var isClosed : Bool = true {
didSet {
geometry?.firstMaterial?.diffuse.contents = self.isClosed ? UIImage(named: "FocusSquare/close") : UIImage(named: "FocusSquare/open")
}
}
接下来,返回updateFocusSquare()函数。在if else语句中,如果焦点方块击中平面,则添加此代码。
let canAddNewModel = hitTestResult.anchor is ARPlaneAnchor
focusSquareLocal.isClosed = canAddNewModel
如果结果的锚点是平面锚点,那么它将是真的,我们将能够添加模型。如果是这种情况,那么焦点方块将是关闭方的图像。否则,将焦点方块打开。
focusSquareLocal.isClosed = false
运行应用程序。一切看起来都很棒但是如果你旋转设备怎么办?您将看到焦点方块不再粘在屏幕中间。
当我们切换到横向模式时,我们将不得不更新屏幕的中心点。首先,让我们在updateFocusSquare()函数的正上方添加一个viewWillTransition子类。然后,将viewCenter声明为视图大小的中间点,并将该点分配给screenCenter。
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
let viewCenter = CGPoint(x: size.width / 2, y: size.height / 2)
screenCenter = viewCenter
}
此时,我们不再需要看到网格了。我们现在有焦点方块向我们展示我们是否找到了合适的平面。到目前为止,它是为了帮助我们更好地可视化飞机和命中测试的结果。我们将在委托方法中注释掉与网格相关的代码。
在didAdd中:
// let planeAnchor = anchor as! ARPlaneAnchor
// let planeNode = createPlane(planeAnchor: planeAnchor)
// node.addChildNode(planeNode)
在didUpdate中:
// let planeAnchor = anchor as! ARPlaneAnchor
//
// node.enumerateChildNodes { (childNode, _) in
// childNode.removeFromParentNode()
// }
//
// let planeNode = createPlane(planeAnchor: planeAnchor)
// node.addChildNode(planeNode)
在didRemove中:
// node.enumerateChildNodes { (childNode, _) in
// childNode.removeFromParentNode()
// }
运行应用程序或最后一次并检查出来。
在本课程中,您已经学习了很多很棒的东西,从创建自己的类开始并自定义它。你能够将焦点方块从非活动变形到整个房间循环,并在打开和关闭状态之间切换。焦点方块广泛用于要检测表面的AR应用程序中。命中测试也是一项重要功能。它允许用户在纯粹的设备和现实世界之间进行交互,提供这种娱乐体验。事实上,在增强现实之外,即使您点击此视频观看,也可以在任何地方找到热门测试。有了这个,继续下一节。到时候那里见。