6c85e9c5fde9c12ccd7fa01b5517e0cc
ARKit 中巧用物理引擎,解决距离问题

说明

在 AR 开发中,我们有时会遇到这样的问题:让一个物体根据距离来决定显示还是隐藏,比如当你走到 2 米处,显示一个详情介绍页等;或者是场景非常大,希望走远 100 米外,隐藏某些物体。

在 RealityKit 中,我们可以用接近触发器来实现,但 ARKit 中并没有直接提供相应功能,我们只能通过其它方式。

zFar 的使用

首先让我想到的,就是 camera 的属性:zFar

在 ARKit 中,zFar 默认是 1000,单位是米。我们可以根据需要把 zFar 设置为100 米,就可以自动实现走近出现,走远隐藏功能了。

但是,这样做有两个明显的不方便之处:

  • zFar 属性适用于整个场景,这样一来,所有物体都会受到距离的影响;
  • zFar 会强制截断物体的显示,如果虚拟物体很大,或者细长型,就会只显示一半;

距离计算

这样一来,我们就只能手动计算距离,来实现这个功能了。苹果提供了现成的距离计算方法:simdDistance(),我们可以利用它,在 ARSCNView 的渲染循环代理方法中,或者 ARSession 的渲染代理方法中,计算物体虚拟物体坐标原点(或坐标系内任一点)到手机(即相机)的距离。

为了节省性能开销,我们可以每 10 帧或者 20 帧计算一次距离,来减小系统压力;甚至放在后台线程计算距离来减少卡顿。但是这样做,也有一定的不足之处:

  • 如果物体过多,仍然会有计算压力;
  • 如果物体很大,或者细长型,会导致相机距离物体表面很近甚至已经进入物体内部了,但中心点距离仍不满足条件;

boundingBox 距离计算

于是我们想到了用边界盒/边界球来计算,ARKit 提供了获取 boundingBox 最大值最小值的方法:

let (min,max) = tempNode.boundingBox


这样,我们实际拿到了物体在自身坐标系下的 (x, y, z) 方向的最小值和最大值(即 6 个面的坐标)。然后拿到 8 个顶点的坐标,逐个计算其与相机的距离,就可以拿到最大距离和最小距离,这样避免了细长型物体的干扰。

但是需要注意的是,有时候物体太大,可能会出现:离 8 个顶点都很远,但实际已经离物体表面很近,甚至已经进入 boundingBox 内部了。如下图,物体大小有 10 米,但我们希望相机离物体 5 米时再显示,大于 5 米则不显示。这时候如果相机从物体中心穿过去,显示离 8 个顶点都是大于 5 米距离的:

此时也不是没有解决办法:

  • 可以将 6 个面向外扩张 5 米,只需要再判断相机是否进入扩大后的边界盒内部就行了:当相机位置(x, y, z)同时满足 min.x < x < max.x , min.y < y < max.y,min.z < z < max.z 即说明进入了边界盒内部
  • 使用物体几何体顶点,逐个判断,步骤复杂

物理引擎使用

实际上,当我们使用 boundingBox 或者几何体顶点来做判断的时候,我们就相当于实现了一个最简单版本的碰撞计算。

那我们干脆用 SceneKit 自带的物理引擎进行碰撞计算不就行了?物理引擎的算法经过了优化,非常高效,同时还可以对 boundingBox 形状进行指定,控制碰撞的精确程度。

我们可以在相机上套一个透明的球体,观察这个球体和虚拟飞机的碰撞效果。

这里我们将飞机的物理形体类型设置为.static,即静止物体,可碰撞但不受碰撞效果影响(不会被撞飞)。
球体的物理形体设置为.kinamatic,即可移动的物体,可碰撞但不受碰撞效果影响(不会被撞飞)。
设置完成后,它们的 physicsBody?.categoryBitMask 会自动被设置为SCNPhysicsCollisionCategory.staticSCNPhysicsCollisionCategory.default

另外需要注意的是,默认情况下,所有物体的collisionBitMask为 all,即能够与其它所有类型物体发生碰撞。还需要设置当前物体与哪些物体碰撞要调用代理,这里注意,两个都要设置才能在代理中收到调用(只设置一个不管用):

  • shipNode?.physicsBody?.contactTestBitMask = Int(SCNPhysicsCollisionCategory.default.rawValue)
  • distanceBall.physicsBody?.contactTestBitMask = Int(SCNPhysicsCollisionCategory.static.rawValue)

代码如下:
```swift
override func viewDidLoad() {
super.viewDidLoad()

    // Set the view's delegate
    sceneView.delegate = self
    // Show statistics such as fps and timing information
    sceneView.showsStatistics = true

    // Create a new scene
    let scene = SCNScene(named: "art.scnassets/ship.scn")!
    sceneView.debugOptions.insert(.showFeaturePoints)
    sceneView.debugOptions.insert(.showWorldOrigin)
    // Set the scene to the view
    sceneView.scene = scene
    scene.physicsWorld.contactDelegate = self // 控制器添加SCNPhysicsContactDelegate协议
top Created with Sketch.