Search code examples
swiftscenekitarkit

How to track the 2D coordinates of node's vertices?


I have a simple plane node that tracks a face.

func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
    guard anchor is ARFaceAnchor else { return nil }
    
    let plane = SCNPlane(width: 0.1, height: 0.2)
    let planeNode = SCNNode(geometry: plane)
    planeNode.geometry?.firstMaterial?.fillMode = .lines
    planeNode.geometry?.firstMaterial?.diffuse.contents = UIColor.blue
    planeNode.name = "Plane Node"
    return planeNode
}

I want to be able to track the coordinates of all four corners of the plane. I'm looking to get the 2D coordinates that are projected on the screen.

func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
    guard let faceAnchor = anchor as? ARFaceAnchor else {
        return
    }
    
    node.enumerateChildNodes { childNode, _ in
        guard childNode.name == "Plane Node" else { return }
        let worldPosition = childNode.worldPosition
        let screenPosition = renderer.projectPoint(worldPosition)
        print(CGPoint(x: Int(screenPosition.x), y: Int(screenPosition.y)))
    }
}

Above tracks the center position of the plane, but how do I track the four corner coordinates?

I tried using the width and the height of the plane using the following to calculate the distance from the center coordinate, but I'm unable to get the proper width and the height that I can work with screen position I've obtained for the center coordinate.

extension SCNNode {
    var width: Float {
        return (boundingBox.max.x - boundingBox.min.x) * scale.x
    }
    
    var height: Float {
        return (boundingBox.max.y - boundingBox.min.y) * scale.y
    }
}

Solution

  • Invisible tracking spheres

    Try the following solution. Despite the fact that it's a macOS project, you can implement this idea in your ARKit project. Also, this technique is very easy to understand.

    enter image description here

    import SceneKit
    
    class ViewController: NSViewController {
        
        var sceneView: SCNView? = nil
        let planeNode = SCNNode()
        var vertices: [SCNVector3] = []
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            sceneView = self.view as? SCNView
            sceneView?.scene = SCNScene()
            sceneView?.delegate = self
            sceneView?.backgroundColor = .black
            sceneView?.autoenablesDefaultLighting = true
            
            // Plane
            let plane = SCNPlane(width: 0.5, height: 0.5)
            planeNode.name = "planar"
            planeNode.geometry = plane
            planeNode.geometry?.firstMaterial?.isDoubleSided = true
            sceneView?.scene?.rootNode.addChildNode(planeNode)
            planeNode.runAction(SCNAction.move(to: SCNVector3(0.2, 0.2,-0.2), 
                                         duration: 2))            
            self.trackingSpheres()
        }
    }
    

    Delegate's method (in your case it's renderer(_:didUpdate:for:) instance method).

    extension ViewController: SCNSceneRendererDelegate {
        
        func renderer(_ renderer: SCNSceneRenderer, 
               updateAtTime time: TimeInterval) {
            
            if let spheres = sceneView?.scene?.rootNode.childNodes[0].childNodes {
                
                for (_, sphere) in spheres.enumerated() {
                    
                    let truncateX = String(format: "%.2f", sphere.worldPosition.x)
                    let truncateY = String(format: "%.2f", sphere.worldPosition.y)
                    let truncateZ = String(format: "%.2f", sphere.worldPosition.z)
                    
                    print("\(sphere.name!):", truncateX, truncateY, truncateZ)
                }
            }
        }
    }
    

    A method creating four invisible tiny tracking spheres.

    extension ViewController { 
      
        fileprivate func trackingSpheres() { 
            // retrieving a plane node from scene
            if let node = sceneView?.scene?.rootNode.childNode(
                                                      withName: "planar",
                                                   recursively: true) {
                let left = node.boundingBox.min.x
                let right = node.boundingBox.max.x
                let lower = node.boundingBox.min.y
                let upper = node.boundingBox.max.y
                let south = node.boundingBox.min.z                
                // Counter clock-wise
                let ll = SCNVector3(x: left, y: lower, z: south)
                let lr = SCNVector3(x: right, y: lower, z: south)
                let ur = SCNVector3(x: right, y: upper, z: south)
                let ul = SCNVector3(x: left, y: upper, z: south)
                
                self.vertices += [ll, lr, ur, ul]
                            
                for i in 1...4 {                    
                    let sphereNode = SCNNode(geometry: SCNSphere(radius: 0.01))
                    
                    sphereNode.position = SCNVector3(vertices[i-1].x,
                                                     vertices[i-1].y,
                                                     vertices[i-1].z) 
                    sphereNode.name = "sphere\(i)"
                    sphereNode.opacity = 0.0         // 100% transparent
    
                    sphereNode.geometry?.firstMaterial?.diffuse.contents = 
                                                                   NSColor.red
                    node.addChildNode(sphereNode)
                }
            }
        }
    }
    

    enter image description here


    Model's decomposition using vertex semantics

    This technique is much tougher than the first one, it's something like reverse engineering of this.

    extension SCNGeometry {
    
        func getVerticesPositions() -> [SCNVector3] {
    
            let sources = self.sources(for: .vertex)
            guard let source = sources.first else { return [] }
    
            let stride = source.dataStride / source.bytesPerComponent
            let offset = source.dataOffset / source.bytesPerComponent
            let vectorCount = source.vectorCount
    
            return source.data.withUnsafeBytes { 
                             (pointer: UnsafePointer<Float>) -> [SCNVector3] in
                
                var vertices: [SCNVector3] = []
                
                for index in 0 ... (vectorCount - 1) {
                    
                    let bytes = index * stride + offset
                    
                    let x = pointer[bytes + 0]
                    let y = pointer[bytes + 1]
                    let z = pointer[bytes + 2]
                    
                    vertices.append(SCNVector3(x, y, z))
                }
                return vertices
            }
        }
    }
    

    Let's see how we can use it.

    var vertices: [SCNVector3] = []
    
    fileprivate func trackingSpheres() {
        
        if let node = sceneView?.scene?.rootNode.childNode(withName: "planar",
                                                        recursively: true) {
    
            vertices = (node.geometry?.getVerticesPositions())!
        }
        print("vertex1 :", vertices[0])
        print("vertex2 :", vertices[1])
        print("vertex3 :", vertices[2])
        print("vertex4 :", vertices[3])
    }