场景中的 ARKit 3D 头部跟踪

ARKit 3D Head tracking in scene

我正在使用 ARKit 创建增强相机应用程序。当 ARSession 初始化时,一个 3d 字符显示在 ARSCNView 中。我正在尝试获取角色的 跟踪 ARCamera 的视角,以便在用户移动拍照时它们始终注视着相机。

我使用了 Apple 的变色龙演示,它添加了一个焦点节点,该节点使用 SCNLookAtConstraint 跟踪相机的视角,但我越来越 奇怪的行为。当 ARCamera 平移时,头部会掉到一边并旋转。如果我添加一个 SCNTransformConstraint 来限制 头部移动到 up/down/side-to-side,它保持垂直但随后看向别处并且不跟踪。

我试着把变色龙演示拆开看看为什么我的演示不起作用,但几天后我就卡住了。

我使用的代码是:

class Daisy: SCNScene, ARCharacter, CAAnimationDelegate {

    // Rig for animation
    private var contentRootNode: SCNNode! = SCNNode()
    private var geometryRoot: SCNNode!
    private var head: SCNNode!
    private var leftEye: SCNNode!
    private var rightEye: SCNNode!

    // Head tracking properties
    private var focusOfTheHead = SCNNode()
    private let focusNodeBasePosition = simd_float3(0, 0.1, 0.25)

    // State properties
    private var modelLoaded: Bool = false
    private var headIsMoving: Bool = false
    private var shouldTrackCamera: Bool = false



    /*
    * MARK: - Init methods
    */

    override init() {

        super.init()

        loadModel()
        setupSpecialNodes()
        setupConstraints()

    }


    /*
    * MARK: - Setup methods
    */

    func loadModel() {

        guard let virtualObjectScene = SCNScene(named: "daisy_3.dae", inDirectory: "art.scnassets") else {
            print("virtualObjectScene not intialised")
            return
        }

        let wrapper = SCNNode()

        for child in virtualObjectScene.rootNode.childNodes {
            wrapper.addChildNode(child)
        }

        self.rootNode.addChildNode(contentRootNode)

        contentRootNode.addChildNode(wrapper)

        hide()

        modelLoaded = true

    }


    private func setupSpecialNodes() {

        // Assign characters rig elements to nodes
        geometryRoot = self.rootNode.childNode(withName: "D_Rig", recursively: true)
        head = self.rootNode.childNode(withName: "D_RigFBXASC032Head", recursively: true)
        leftEye = self.rootNode.childNode(withName: "D_Eye_L", recursively: true)
        rightEye = self.rootNode.childNode(withName: "D_Eye_R", recursively: true)

        // Set up looking position nodes
        focusOfTheHead.simdPosition = focusNodeBasePosition

        geometryRoot.addChildNode(focusOfTheHead)

    }


    /* 
    * MARK: - Head animations
    */ 

    func updateForScene(_ scene: ARSCNView) {

        guard shouldTrackCamera, let pointOfView = scene.pointOfView else {
            print("Not going to updateForScene")
            return
        }

        followUserWithHead(to: pointOfView)

    }

    private func followUserWithHead(to pov: SCNNode) {

        guard !headIsMoving else { return }

        // Update the focus node to the point of views position
        let target = focusOfTheHead.simdConvertPosition(pov.simdWorldPosition, to: nil)

        // Slightly delay the head movement and the animate it to the new focus position
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: {
            let moveToTarget = SCNAction.move(to: SCNVector3(target.x, target.y, target.z), duration: 1.5)
            self.headIsMoving = true
            self.focusOfTheHead.runAction(moveToTarget, completionHandler: {
                self.headIsMoving = false
            })
        })

    }



    private func setupConstraints() {

        let headConstraint = SCNLookAtConstraint(target: focusOfTheHead)
        headConstraint.isGimbalLockEnabled = true

        let headRotationConstraint = SCNTransformConstraint(inWorldSpace: false) { (node, transform) -> SCNMatrix4 in

            // Only track the up/down and side to side movement
            var eulerX = node.presentation.eulerAngles.x
            var eulerZ = node.presentation.eulerAngles.z

            // Restrict the head movement so it doesn't rotate too far
            if eulerX < self.rad(-90) { eulerX = self.rad(-90) }
            if eulerX > self.rad(90) { eulerX = self.rad(90) }

            if eulerZ < self.rad(-30) { eulerZ = self.rad(-30) }
            if eulerZ > self.rad(30) { eulerZ = self.rad(30) }

            let tempNode = SCNNode()
            tempNode.transform = node.presentation.transform
            tempNode.eulerAngles = SCNVector3(eulerX, 0, eulerZ)
            return tempNode.transform

        }

        head?.constraints = [headConstraint, headRotationConstraint]

    }

    // Helper to convert degrees to radians
    private func rad(_ deg: Float) -> Float {
        return deg * Float.pi / 180
    }

}

场景编辑器中的模型是:

我遇到的问题已经解决了。有 2 个问题:

  1. followUserWithHead 中的目标应该已经为它的父级转换了 simdWorldPosition 并且从(而不是)

    focusOfTheHead.parent!.simdConvertPosition(pov.simdWorldPosition, 来自: nil)

  2. 头节点的本地坐标不正确。 z 轴应该是 x 轴所以当我得到焦点时头部运动跟踪,耳朵总是跟随相机。

我没有意识到 Xcode 中的 Debug View Hierarchy 会显示 SCNScene 的细节。这帮助我调试场景并找到节点跟踪的位置。您可以将场景导出为 dae,然后加载到 SceneKit 编辑器中

编辑: 我将 localFront 用作下面评论中建议的 mnuages,这使跟踪工作朝着正确的方向进行。不过,头部确实偶尔会动一下。我已将此归结为模型上的 运行 动画试图应用一个转换,然后在下一个更新周期中更改该转换。我决定从头部移除跟踪并使用相同的方法只跟踪眼睛。