Post

Replies

Boosts

Views

Activity

Reply to ARKit: Keep USDZ node fixed after image tracking is lost (prevent drifting)
When the image is within the camera’s view, the node anchored to the image anchor remains stable. However, once the image goes out of view, the node starts drifting as the camera moves. What’s the correct way in ARKit to “freeze” the node at its last known world transform once ARImageAnchor stops tracking, so it doesn’t drift? here is the code. import UIKit import SceneKit import ARKit class ViewController: UIViewController, ARSCNViewDelegate { @IBOutlet var sceneView: ARSCNView! var deviceNode: SCNNode? // Persistent container driven by the image anchor (world-space) private var anchorContainer: SCNNode? private var orientationNode: SCNNode? // holds your -180/0/-90 local rotation private var debugPlaneNode: SCNNode? var previousTransform: simd_float4x4? override func viewDidLoad() { super.viewDidLoad() sceneView.delegate = self sceneView.autoenablesDefaultLighting = true sceneView.automaticallyUpdatesLighting = true sceneView.preferredFramesPerSecond = 60 let scene = SCNScene(named: "art.scnassets/rootScene.scn")! sceneView.scene = scene } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) let configuration = ARWorldTrackingConfiguration() configuration.detectionImages = ARReferenceImage.referenceImages(inGroupNamed: "QRImages", bundle: .main) configuration.maximumNumberOfTrackedImages = 1 configuration.isLightEstimationEnabled = true configuration.worldAlignment = .gravity sceneView.session.run(configuration) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) sceneView.session.pause() } // MARK: - Model loading and local transform func loadUSDZNode(from url: URL) -> SCNNode? { do { let scene = try SCNScene(url: url, options: nil) let containerNode = SCNNode() for child in scene.rootNode.childNodes { containerNode.addChildNode(child) } return containerNode } catch { print("❌ Failed to load .usdz at \(url): \(error)") return nil } } // MARK: - ARSCNViewDelegate // We don't attach content to ARKit's managed node, return empty. func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? { return SCNNode() } // Create (once) a persistent container under root, and keep your local hierarchy inside it. func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { guard let imageAnchor = anchor as? ARImageAnchor else { return } DispatchQueue.main.async { if self.anchorContainer == nil { // World-space container driven by the image let container = SCNNode() self.sceneView.scene.rootNode.addChildNode(container) self.anchorContainer = container // Debug plane showing the detected image pose (child-local) let plane = SCNPlane(width: 0.125, height: 0.125) plane.firstMaterial?.diffuse.contents = UIColor.green let planeNode = SCNNode(geometry: plane) planeNode.eulerAngles.x = -.pi / 2 container.addChildNode(planeNode) self.debugPlaneNode = planeNode let orient = SCNNode() orient.simdOrientation = self.createRotation(x: -180, y: 0, z: -90) container.addChildNode(orient) self.orientationNode = orient // Put your device node under the orientation node (preserves your local offset) if let dev = loadUSDZNode(from: url) { // Make sure it's not parented elsewhere dev.removeFromParentNode() orient.addChildNode(dev) } } // Re-anchor the container to the current image transform self.anchorContainer?.simdTransform = imageAnchor.transform self.previousTransform = imageAnchor.transform self.debugPlaneNode?.isHidden = false } } // Drive only the container with smoothing; keep your model’s local offset intact. func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) { guard let imageAnchor = anchor as? ARImageAnchor, let container = anchorContainer else { return } if imageAnchor.isTracked { let currentTransform = imageAnchor.transform self.debugPlaneNode?.isHidden = false // First frame after regaining tracking → snap if previousTransform == nil { container.simdTransform = currentTransform previousTransform = currentTransform return } previousTransform = currentTransform.translation } } else { // Lost tracking: keep last pose, hide the plane, mark to snap on next detection debugPlaneNode?.isHidden = false previousTransform = nil } } // Keep using your degree-based helper func createRotation(x: Float, y: Float, z: Float) -> simd_quatf { let xRotation = x * (.pi / 180.0) let yRotation = y * (.pi / 180.0) let zRotation = z * (.pi / 180.0) return simd_quatf(angle: xRotation, axis: SIMD3<Float>(1, 0, 0)) * simd_quatf(angle: yRotation, axis: SIMD3<Float>(0, 1, 0)) * simd_quatf(angle: zRotation, axis: SIMD3<Float>(0, 0, 1)) } }
Topic: Spatial Computing SubTopic: ARKit Tags:
Oct ’25