1

I followed this code from @rickster which 100% works and looks great. In the video he's animating a SCNNode that is set using an ARAnchor from 1 position to another and back. I tried to do something similar except I want the node that is set with the ARAnchor to follow/update it's position to another node that is a child of the camera.

I'm having a problem updating the position in func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) { }

I tried to animate the node that is set with the ARAnchor to follow the other node but it's not working, it follows backwards and in reverse:

let animation = CABasicAnimation(keyPath: #keyPath(SCNNode.transform))
animation.fromValue = nodeSetWithARAnchor.transform
animation.toValue = nodeTiedToCamera.transform
animation.duration = 1
nodeSetWithARAnchor.removeAllAnimations()
nodeSetWithARAnchor.addAnimation(animation, forKey: nil)

I then tried to remove the ARAnchor and reset its node's .worldPostion and .simdWorldTransform but the node diappears. It's in steps 7 & 8 below.

How can I get the nodeSetWithARAnchor to update its ARAnchor and position to always follow the nodeTiedToCamera?

Update In Step 6 now that I set the nodeSetWithARAnchor SCVector3 to match the nodeTiedToCameradWorld's SCVector3 and set its .transform to match the nodeTiedToCameradWorldTransform @rickster's animation code works the best because I don't I have to remove any anchors. There is another problem though. The nodeSetWithARAnchor responds when I move the device but it responds backwards and in reverse.

When I turn the device up the image goes right and when I turn the device down the image goes left. When I turn the device left the image goes up and when I turn the device right the image goes down. It's following the image I have tied to the camera but it's following it incorrectly.

let configuration = ARWorldTrackingConfiguration()

var nodeSetWithARAnchor: SCNNode?
var nodeTiedToCamera: SCNNode?
var anchors: [ARAnchor] = []

override func viewDidLoad() {
    super.viewDidLoad()

    configuration.planeDetection = [.horizontal, .vertical]
    configuration.maximumNumberOfTrackedImages = 1

    // 1. once this anchor is set inside renderer(_:didAdd:for:) I initialize the nodeSetWithARAnchor at 30cm behind the device's camera's initial position
    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
        var translation = matrix_identity_float4x4
        translation.columns.3.z = -0.3
        let transform = simd_mul(self.sceneView.session.currentFrame!.camera.transform, translation)
        let anchor = ARAnchor(transform: transform)
        self.sceneView.session.add(anchor: anchor)
    }

    // 2. the nodeTiedToCamera will always go where ever the device's camera goes
    let plane = SCNPlane(width: 0.1, height: 0.1)
    nodeTiedToCamera = SCNNode(geometry: plane)
    nodeTiedToCamera!.position = SCNVector3(x: -0.15, y: 0.45, z: -1.25) // I don't want it directly in front of the camera
    nodeTiedToCamera!.geometry?.fisrtMaterial?.diffuse.contents = UIColor.red
    sceneView.pointOfView.addChildNode(nodeTiedToCamera!)
}

// 3. I init the nodeSetWithARAnchor, add it to the sceneView's root node, and keep a copy of it's anchor
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {

    DispatchQueue.main.async {

        if self.nodeSetWithARAnchor == nil {
            // create geometry ...
            self.nodeSetWithARAnchor = SCNNode(geometry: geometry)

            node.addChildNode(self.nodeSetWithARAnchor!)
        }

        self.anchors.removeAll()
        self.anchors.append(anchor)
    }
}

func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) {

    DispatchQueue.main.async {

        // 4. get the only child that is tied to the camera which is the nodeTiedToCamera
        guard let pointOfView = self.sceneView.pointOfView else { return }
        guard let child = pointOfView.childNodes.first else { return }

        // 5. get it's .worldPosition && it's .simdWorldTransform
        let nodeTiedToCameradWorldPosition = child.worldPosition
        let nodeTiedToCameradWorldTransform = child.worldTransform

        if let nodeSetWithARAnchor = self.nodeSetWithARAnchor, let anchorToRemove = self.anchors.first {

             // 6. set the nodeSetWithARAnchor SCVector3 to match the nodeTiedToCameradWorld's SCVector3 and set its .transform to match the nodeTiedToCameradWorldTransform
             nodeSetWithARAnchor.position = nodeTiedToCameradWorldPosition
             nodeSetWithARAnchor.transform = nodeTiedToCameradWorldTransform

             let animation = CABasicAnimation(keyPath: #keyPath(SCNNode.transform))
             animation.fromValue = nodeSetWithARAnchor.transform
             animation.toValue = nodeTiedToCamera.transform
             animation.duration = 1
             nodeSetWithARAnchor.removeAllAnimations()
             nodeSetWithARAnchor.addAnimation(animation, forKey: nil)

             // 7. remove all ARAnchors
             //self.sceneView.session.remove(anchor: anchorToRemove)
             //self.anchors.removeAll()

             // 8. add a new anchor to the session and set it with the nodeSetWithARAnchor.simdWorldTransform
            //let anchor = ARAnchor(transform: nodeSetWithARAnchor.simdWorldTransform)
            //self.sceneView.session.add(anchor: anchor)
        }
    }
}

func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
    guard let node = self.nodeSetWithARAnchor else { return }

    if let pointOfView = sceneView.pointOfView {
        let isVisible = sceneView.isNode(node, insideFrustumOf: pointOfView)
        print("Is node visible: \(isVisible)")
    }
}
Lance Samaria
  • 17,576
  • 18
  • 108
  • 256

1 Answers1

1

It was an ALL day thing but got it working. I only had to switch around 2 lines of code in renderer(:willRenderScene:atTime:) and run them in the exact order below. I didn't have to remove and add anchors or run any animation code.

func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) {
    
    DispatchQueue.main.async { [weak self] in

        guard let safeSelf = self else { return }
        guard let pointOfView = safeSelf.sceneView.pointOfView else { return }
        guard let child = pointOfView.childNodes.first else { return } // child is the nodeTiedToCamera

        if let nodeSetWithARAnchor = safeSelf.nodeSetWithARAnchor {
            
            // *** I just had to switch around these 2 lines of code and run them in this exact order ***
            nodeSetWithARAnchor.transform = child.worldTransform
            nodeSetWithARAnchor.worldPosition = child.worldPosition
        }
    }
}
Lance Samaria
  • 17,576
  • 18
  • 108
  • 256
  • 1
    I don't think your problem had anything to do with the order of those two lines. The problem is that you were applying a `worldTransform` and `worldPosition` to `nodeSetWithARAnchor` local transform and local position. The local transform and position are calculated with respect to it's parent node (which is not `rootNode` in this case). The code you have now is working because you are setting `.worldPosition` last, so the local `.transform` you have is being overridden. Additionally, the `transform` and `worldTransform` includes position information as well as orientation. – jfriesenhahn Feb 21 '20 at 15:50
  • 1
    So if you need both orientation **and** position, then use `worldTransform`. If you just need position, then only use `worldPosition`. – jfriesenhahn Feb 21 '20 at 15:51
  • @jfriesenhahn When I use it the way I have it now but in reverse it doesn’t work. For whatever reason it only works in that exact order. You are correct that when I initially set the local .transform with the other nodes local local .transform it was causing a problem. It also wouldn’t let me set nodeSetWithAnchor’s .worldTransform property directly because I get a ‘it’s a get-only’ error. I was only able to change it’s .transform property. Thanks for the advice though :) – Lance Samaria Feb 21 '20 at 16:12
  • 1
    `worldTransform` is get only, but you can get around this by setting `simdWorldTransform` instead. You just have to convert from `SCNVector4` to `simd_float4x4`. However, the reason it is only working in that exact order is that your `transform` assignment is setting the node's position as well as orientation, and that position is set relatively, meaning it is not where you think it is/want it to be. Then you are overriding that position when you set `worldPosition` afterwards. Unless you actually care about the node's orientation, you can skip the `transform` and use `worldPosition` – jfriesenhahn Feb 21 '20 at 20:32