ARKit 中用好 unprojectPoint: 效率又提升一倍

1,122 阅读2分钟

unprojectPoint 作用

unprojectPoint 的作用其实很类似hitTest方法,都是从屏幕指定位置发射射线,与 3D 空间某处相交,得到交点。

不同的是:

  • hitTest方法是射线与某个SCNGeometry相交,返回所在的 SCNNode 和交点信息;
  • unprojectPoint 是将屏幕上的点投影到一个3D 空间内的平面上,可脱离 SCNNode 类和 SCNGeometry 类来使用。

注意:SceneKit 中SCNSceneRenderer协议(SCNView实现了这个协议)也提供了一个unprojectPoint方法,但是这个方法只能将屏幕上的点,投影到视锥体的平面上,z 是 0 则投影到 zNear 平面上,z 是 1 则投影到 zFar 平面上。

ARKit 中提供了两个unprojectPoint 方法:一个在ARSCNView类下,一个在ARCamera类下。并且官方文档中说明了,ARSCNView类下的方法,其实就是调用了ARCamera类的方法,只是ARSCNView类自己知道自己的 size 和 orientation,所以少了两个参数。

// 官方文档说明了ontoPlane是指:投影到 4x4 矩阵的 xz 平面上
extension ARCamera {
    @available(iOS 12.0, *)
    @nonobjc public func unprojectPoint(_ point: CGPoint, ontoPlane planeTransform: simd_float4x4, orientation: UIInterfaceOrientation, viewportSize: CGSize) -> simd_float3?
}

@available(iOS 12.0, *)
extension ARSCNView {
    @nonobjc public func unprojectPoint(_ point: CGPoint, ontoPlane planeTransform: simd_float4x4) -> simd_float3?
}

示例用法

当我们想要在 AR 空间确定一个位置时,就可以用unprojectPoint来处理:
比如,在 3D 空间放置一个小球。我们先创建一个平面,然后从屏幕中点投影到平面上,在交点处放置一个小球:

// plane 在 xy 平面,是竖直的
 let planeNode = SCNNode(geometry: SCNPlane(width: 1, height: 1))
planeNode.geometry?.firstMaterial?.diffuse.contents = UIColor(white: 1, alpha: 0.8)
sceneView.scene.rootNode.addChildNode(planeNode)

点击屏幕投影到平面上:

verride func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    // 绕 x 轴旋转 90 度,将 xz 平面旋转到竖直状态,与显示的平面同一位置/姿态
    let planeT = matrix_float4x4(simd_quatf(angle: .pi / 2, axis: simd_float3(1, 0, 0)))
    // 或者直接简单粗暴旋转矩阵
    let planeT2 = simd_float4x4(
        simd_float4(1, 0, 0, 0), // x 轴不变
        simd_float4(0, 0, 1, 0), // y 轴指向父的 z 方向
        simd_float4(0, -1, 0, 0), // z 轴指向父的 -y 方向
        simd_float4(0, 0, 0, 1)
    )
    
    // 求交点
    let position = sceneView.unprojectPoint(sceneView.center, ontoPlane: planeT)
    // 在交点处放置一个红色小球
    if let position = position {
        let ballNode = SCNNode(geometry: SCNSphere(radius: 0.02))
        ballNode.simdPosition = position
        ballNode.geometry?.firstMaterial?.diffuse.contents = UIColor.red
        sceneView.scene.rootNode.addChildNode(ballNode)
    }
}

效果如下图,需要注意的是,unprojectPoint投影的xz平面是无穷大的