本部分主要实现通过ARKit在现实环境找到一个plane并放置一个3D物体船。
效果预览:
在正式开始前,我们需要了解“Horizontal Plane”这一概念。当我们在ARKit中讨论水平面时,我们究竟在讨论什么呢?当我们在ARKit中检测到一个水平面时,我们在技术上检测到的其实是一个ARPlaneAnchor。那么什么是ARPlaneAnchor呢?ARPlaneAnchor大体上可以理解为一个包含检测到的水平面信息的对象(水平与位置信息)。
本部分主要介绍Horizontal Plane的实现方式,所以我们直接在这个工程(链接: https://pan.baidu.com/s/1eJeIbkzk1OUy_YLk6X3yog 提取码: vv6y )上面继续开发,对于如何在Xcode使用ARKit实现AR效果不懂的可以查看之前的文章:
下载完后在Xcode打开,并运行项目,如下图所示:
在ViewController的setUpSceneView()方法中添加以下内容:
configuration.planeDetection = .horizontal
通过将ARWorldTrackingConfiguration的planeDetection属性设置为.horizontal,这告诉ARKit去查找任何水平面。一旦ARKit检测到一个水平面,该水平面将被添加到sceneView的session中。
为了检测水平面,必须采用ARSCNViewDelegate协议。在ViewController类下面,创建一个ViewController类扩展来实现协议:
extension ViewController: ARSCNViewDelegate {
}
在类扩展的内部,实现renderer(_:didAdd:for:)方法:
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
}
每当场景视图的session添加了一个新的ARAnchor时,就会调用这个协议方法。ARAnchor是表示三维空间中物理位置和方向的对象。稍后我们将使用ARAnchor来检测水平面。
将sceneView的委托分配给setUpSceneView()中的ViewController。
还可以设置sceneView的调试选项来显示现实环境中的特征点。这可以帮助找到一个特征点足够多的水平面。一旦检测到足够的特征点来识别水平表面,就会调用renderer(_:didAdd:for:)。
目前setUpSceneView()方法应该是这样的:
func setUpSceneView() {
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = .horizontal
sceneView.session.run(configuration)
sceneView.delegate = self
sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints]
}
更新renderer(_:didAdd:for:)方法:
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
// 1
guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
// 2
let width = CGFloat(planeAnchor.extent.x)
let height = CGFloat(planeAnchor.extent.z)
let plane = SCNPlane(width: width, height: height)
// 3
plane.materials.first?.diffuse.contents = UIColor.transparentLightBlue
// 4
let planeNode = SCNNode(geometry: plane)
// 5
let x = CGFloat(planeAnchor.center.x)
let y = CGFloat(planeAnchor.center.y)
let z = CGFloat(planeAnchor.center.z)
planeNode.position = SCNVector3(x,y,z)
planeNode.eulerAngles.x = -.pi / 2
// 6
node.addChildNode(planeNode)
}
创建一个SCNPlane来可视化ARPlaneAnchor。SCNPlane是一个矩形的“单边”平面几何。我们获取未包装的ARPlaneAnchor区段的x和z属性,并使用它们创建一个SCNPlane。
使用刚刚创建的SCNPlane几何图形初始化一个SCNNode。
初始化x、y和z常量来表示planeAnchor的中心x、y和z位置。这是我们的planeNode的位置。
最后,将planeNode作为子节点添加到新添加的SceneKit节点上。
然后Build项目,效果如下图:
随着ARKit接收到关于我们的环境的额外信息,我们需要扩展之前检测到的水平面,以利用更大的表面,或者用新的环境信息更准确地表示,实现方法renderer(_:didUpdate:for:):
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
}
每次更新SceneKit节点的属性以匹配其对应的锚(anchor)时,都会调用此方法。
node参数为我们提供了锚的更新位置。锚参数给出了更新后的宽度和高度。有了这两个参数,我们可以更新之前实现的SCNPlane,用更新后的宽度和高度反映新的位置。
接下来,在renderer(_:didUpdate:for:)中添加以下代码:
// 1
guard let planeAnchor = anchor as? ARPlaneAnchor,
let planeNode = node.childNodes.first,
let plane = planeNode.geometry as? SCNPlane
else { return }
// 2
let width = CGFloat(planeAnchor.extent.x)
let height = CGFloat(planeAnchor.extent.z)
plane.width = width
plane.height = height
// 3
let x = CGFloat(planeAnchor.center.x)
let y = CGFloat(planeAnchor.center.y)
let z = CGFloat(planeAnchor.center.z)
planeNode.position = SCNVector3(x, y, z)
Build应用,实现效果如下图:
在本部分教程刚开始的时候下载的project包含了一个3D物体(ship)。
在ViewController类中插入以下方法,将船放置在水平面的顶部:
@objc func addShipToSceneView(withGestureRecognizer recognizer: UIGestureRecognizer) {
let tapLocation = recognizer.location(in: sceneView)
let hitTestResults = sceneView.hitTest(tapLocation, types: .existingPlaneUsingExtent)
guard let hitTestResult = hitTestResults.first else { return }
let translation = hitTestResult.worldTransform.translation
let x = translation.x
let y = translation.y
let z = translation.z
guard let shipScene = SCNScene(named: "ship.scn"),
let shipNode = shipScene.rootNode.childNode(withName: "ship", recursively: false)
else { return }
shipNode.position = SCNVector3(x,y,z)
sceneView.scene.rootNode.addChildNode(shipNode)
}
对于这部分内容不熟悉的可以查看之前的文章:
这里与之前唯一的区别是,我们在types参数中传递了一个不同的参数来检测sceneView中现有的平面锚。
func addTapGestureToSceneView() {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ViewController.addShipToSceneView(withGestureRecognizer:)))
sceneView.addGestureRecognizer(tapGestureRecognizer)
}
该方法将向sceneView添加一个tap手势识别器。
对于顶部的cherry,调用viewDidLoad()内部方法,向sceneView添加一个tap手势识别器:
addTapGestureToSceneView()
现在运行,你应该能够检测到一个可视化的水平面,然后上面会放置一个小船,你可以任意角度查看它。效果如下图:
可以通过取消viewDidLoad()内部的configureLighting()注释来启用照明。该功能非常简单,用两行代码来启用照明:
sceneView.autoenablesDefaultLighting = true
sceneView.automaticallyUpdatesLighting = true
完整项目链接:https://github.com/appcoda/ARKitHorizontalPlaneDemo
参考链接:https://www.appcoda.com/arkit-horizontal-plane/
------AR Portal(AR开发者社区)整理
关注微信公众号(AR开发者交流社区,提供AR开发干货,推动AR内容发展):AR开发者社区