1

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
    }
}
Andy Jazz
  • 49,178
  • 17
  • 136
  • 220
Kevvv
  • 3,655
  • 10
  • 44
  • 90

2 Answers2

0

It could be probably a solution (woraround) to add i.Ex. invisible childNodes (without geometry) to each of the corners or edges which positions you want to track. Then grab the presentation.worldPosition of each of those nodes at anytime you like, and convert them using your function above to 2D space.

ZAY
  • 3,882
  • 2
  • 13
  • 21
0

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])
}
Andy Jazz
  • 49,178
  • 17
  • 136
  • 220
  • 1
    Thank you for your answer. It works great, but is there a way to use the vertices of the plane node without adding new sphere nodes? – Kevvv Jan 15 '23 at 15:05