利用iOS ARKit制作AR传送门

前言

苹果推出ARKit半年了,开发者对其兴趣有增无减,AR产业也借苹果谷歌等厂商的努力得到了快速发展。作为广大iOS开发者的一员,我也加入了学习AR的队伍中。
得益于SceneKit优越的性能和封装,ARKit的开发也如鱼得水,如果你有SceneKit开发经验,那么短时间开发出一款很酷的AR应用不是难事。这次,我们尝试使用ARKit来制作一个传送门(或者说哆啦A梦的任意门)

项目效果:
AR任意门



前期准备

制作前,我们需要准备好任意门中的3D模型,以及任意门中的天空盒贴图。
在这里我使用了大学的钟塔模型,使用了Cinema4D制作,SceneKit支持dae或obj格式的模型,导入后可以转换成SceneKit对应的scn格式。
天空盒贴图是什么?游戏中对于一些有边界地图,想要创造远距离场景的视觉效果,就可以采用将天空盒包裹当前真实场景的方法,如CS。
天空盒的示例图,图片来自网络

模型文件,使用Cinema4D制作



项目的配置

1. Info.plist的配置

AR需要使用摄像头权限,在Info.plis中添加“Privacy - Camera Usage Description”键值
Info.plist的设置

2. 界面设置

显示AR摄像机,需要使用AR场景控件,拖ARKit Scene View至故事版,同时我们需要放置按钮以及检测到平面的提示Label

界面设置



开始Coding

配置ARSceneView

ARKit追踪需要一个AR世界追踪配置项,可以通过实例化ARWorldTrackingConfiguration类来实现(早期是ARWorldTrackingSessionConfiguration)。
我们想要追踪水平面,从而放置传送门模型,所以在此将其planeDetection设置为追踪水平面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 用于配置AR世界追踪
// The Configuration of World Tracking
let configuration = ARWorldTrackingConfiguration()
var planeAnchor: ARPlaneAnchor?

override func viewDidLoad() {
super.viewDidLoad()

// 设置AR平面检测类型
// set the plane detecing type of world tracking
configuration.planeDetection = .horizontal
sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints]
sceneView.session.run(configuration)
sceneView.delegate = self
sceneView.automaticallyUpdatesLighting = true
}

获取追踪平面

我们在上一步设置了水平面追踪,在这里进行设置。
ARKit提供了识别水平面的代理方法,在ARSCNViewDelegate中。ARKit会不停追踪平面,当追踪到新平面(plane)时,会向该平面上添加锚点(anchor)。代理中为我们提供了这一代理方法。对于追踪到的平面,会添加ARAnchor,而如果检测到的是用户设置的检测平面(我们之前设置的planeDetection),将会放置ARPlaneAnchor。所以我们直接判断该anchor的类型即可。

1
2
3
4
5
6
7
8
9
10
11
12
// MARK: - ARSCNViewDelegate implemention
extension ARViewController: ARSCNViewDelegate {

func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
// 如果检测到的是水平面,那么就是我们需要的,所以在此判断是否为水平面
guard anchor is ARPlaneAnchor else {return}

self.planeAnchor = anchor as? ARPlaneAnchor

// 处理逻辑
}
}

获取平面的位置

上一步我们获取了水平面对应的ARPlaneAnchor,而此时我们需要获取该平面在AR世界中的位置信息。
方法就是使用transform属性,该属性返回一个matrix_float4x4结构体。了解过计算机图形学的应该知道,图形的变换矩阵正是这样的一个结构。我们可以通过修改transform中的对应位置值,就可以对图形进行自由变换。
而我们现在只需要获取他的坐标信息,也就对应着第3行的前三个值。同时,在SceneKit中三维坐标使用的是SCNVector3。所以我们在此扩展SCNVector3以便复用。

1
2
3
4
5
6
7
8
9
extension SCNVector3 {

init (withTransform transform: matrix_float4x4) {
self.x = transform.columns.3.x
self.y = transform.columns.3.y
self.z = transform.columns.3.z
}

}

设置模型

在创建模型之前,我们先创建一个模型的集合,拓展名为scnassets。接下来我们创建一个天空盒,并将想要往其中放置的模型也一并放置。天空盒的材质设置为我们准备好的天空盒贴图。
但是现在有一个问题,如何才能使这个天空盒在外面不可见而在里面可见呢?
答案就是,使用渲染顺序,渲染顺序在前的优先渲染,渲染顺序决定了模型之间的关系,我们可以通过优先渲染前面的透明遮罩平面来来后面的内容“被透明”。
所以在每一个平面上添加一个名为“mask”的平面,大小与父平面相同即可,厚度尽可能小就好。
如下图,具体请见工程源文件。
模型设置

使天空盒从外面不可见

首先我们先添加模型,代码比较简单,在此不赘述。

1
2
3
4
5
guard let portalScene = SCNScene(named: "Model.scnassets/tjgc.scn") else {return}
let portalNode = portalScene.rootNode.childNode(withName: "tjgc", recursively: false)!
let newVector3 = SCNVector3.init(withTransform: transform)
portalNode.position = SCNVector3.init(newVector3.x, newVector3.y, newVector3.z-1)
sceneView.scene.rootNode.addChildNode(portalNode)

接下来就是实现的重点:如何可以让天空盒从外面不可见?
刚才谈到了,我们可以通过让一个渲染顺序更靠前的透明平面遮挡来实现让某个平面“被透明”的效果,而这样也可以使另一个方向观察不受影响。
在SceneKit中,渲染顺序对应renderingOrder属性,数值越小越优先渲染。

1
2
3
let child = portalNode.childNode(withName: nodeName, recursively: true)
child?.geometry?.firstMaterial?.diffuse.contents = UIImage(named: "Model.scnassets/\(imageName).png")
child?.renderingOrder = 200

如上面代码所示,我们只需要把需要处理的节点取出并设置渲染顺序即可。

重置AR场景

很多时候我们想要重置AR追踪,并将现有节点全部移除。可以通过如下代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func reset() {
// 清除节点之前先停止AR会话,否则会crash
// pause ar session before remove node, or will be crash
self.sceneView.session.pause()

self.sceneView.scene.rootNode.enumerateChildNodes { (node, _) in
node.removeFromParentNode()
}

self.planeAnchor = nil
self.addButton.isEnabled = false

// 使用重置配置启动AR会话,场景将会被重置
// Run AR session with reset options, then session will be reset
self.sceneView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
}

具体代码见Github项目
https://github.com/Minecodecraft/ARDoor



如果有问题,欢迎评论区留言