swiftarkit

ARKit SCNNode always in the center when camera move


I am working on a project where I have to place a green dot to be always in the center even when we rotate the camera in ARKit. I am using ARSCNView and I have added the node so far everything is good. Now I know I need to modify the position of the node in

func session(_ session: ARSession, didUpdate frame: ARFrame)

But I have no idea how to do that. I saw some example which was close to what I have but it does not run as it suppose to.

func session(_ session: ARSession, didUpdate frame: ARFrame) {
    let location = sceneView.center
    let hitTest = sceneView.hitTest(location, types: .featurePoint)
    
    if hitTest.isEmpty {
        print("No Plane Detected")
        return
    } else {
        
        let columns = hitTest.first?.worldTransform.columns.3
        
        let position = SCNVector3(x: columns!.x, y: columns!.y, z: columns!.z)
        
        var node = sceneView.scene.rootNode.childNode(withName: "CenterShip", recursively: false) ?? nil
        if node == nil {
            let scene = SCNScene(named: "art.scnassets/ship.scn")!
            node = scene.rootNode.childNode(withName: "ship", recursively: false)
            node?.opacity = 0.7
            let columns = hitTest.first?.worldTransform.columns.3
            node!.name = "CenterShip"
            node!.position = SCNVector3(x: columns!.x, y: columns!.y, z: columns!.z)
            sceneView.scene.rootNode.addChildNode(node!)
        }
        let position2 = node?.position
        
        if position == position2! {
            return
        } else {
            //action
            let action = SCNAction.move(to: position, duration: 0.1)
            node?.runAction(action)
        }
    }
}

It doesn't matter how I rotate the camera this dot must be in the middle.

enter image description here


Solution

  • It's not clear exactly what you're trying to do, but I assume its one of the following:

    A) Place the green dot centered in front of the camera at a fixed distance, eg. always exactly 1 meter in front of the camera.

    B) Place the green dot centered in front of the camera at the depth of the nearest detected plane, i.e. using the results of a raycast from the mid point of the ARSCNView

    I would have assumed A, but your example code is using (now deprecated) sceneView.hitTest() function which in this case would give you the depth of whatever is behind the pixel at sceneView.center

    Anyway here's both:

    Fixed Depth Solution

    This is pretty straightforward, though there are few options. The simplest is to make the green dot a child node of the scene's camera node, and give it position with a negative z value, since z increases as a position moves toward the camera.

    cameraNode.addChildNode(textNode)
    textNode.position = SCNVector3(x: 0, y: 0, z: -1)
    

    As the camera moves, so too will its child nodes. More details in this very thorough answer

    Scene Depth Solution

    To determine the estimated depth behind a pixel, you should use ARSession.raycast instead of SceneView.hitTest, because the latter is definitely deprecated.

    Note that, if the raycast() (or still hitTest()) methods return an empty result set (not uncommon given the complexity of scene estimation going on in ARKit), you won't have a position to update the node and this it might not be directly centered in every frame. To handle this is a bit more complex, as you'd need decide exactly what you want to do in that case.

    The SCNAction is unnecessary and potentially causing problems. These delegate methods run 60fps, so simply updating the position directly will produce smooth results.

    Adapting and simplifying the code you posted:

    func createCenterShipNode() -> SCNNode {
        let scene = SCNScene(named: "art.scnassets/ship.scn")!
        let node = scene.rootNode.childNode(withName: "ship", recursively: false)
        node!.opacity = 0.7
        node!.name = "CenterShip"
        sceneView.scene.rootNode.addChildNode(node!)
        return node!
    }
    
    func session(_ session: ARSession, didUpdate frame: ARFrame) {
        // Check the docs for what the different raycast query parameters mean, but these
        // give you the depth of anything ARKit has detected
        guard let query = sceneView.raycastQuery(from: sceneView.center, allowing: .estimatedPlane, alignment: .any) else {
            return
        }
        
        let results = session.raycast(query)
        if let hit = results.first {
            let node = sceneView.scene.rootNode.childNode(withName: "CenterShip", recursively: false) ?? createCenterShipNode()
            let pos = hit.worldTransform.columns.3
            node.simdPosition = simd_float3(pos.x, pos.y, pos.z)
        }
    }
    

    See also: ARRaycastQuery

    One last note - you generally don't want to do scene manipulation within this delegate method. It runs on a different thread than the Scenekit rendering thread, and SceneKit is very thread sensitive. This will likely work fine, but beyond adding or moving a node will certainly cause crashes from time to time. You'd ideally want to store the new position, and then update the actual scene contents from within the renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) delegate method.