Cursor display when placing objects with ARKit + SceneKit

About the cursor to be displayed when placing an arbitrary object in AR.

Thing you want to do

This is a standard measurement app. keisoku_app.png

Cursor transform

Perform a hit test on the center coordinates of the screen and set based on the acquired worldTransform. Since the location hit by hitTest (_: types :) of ʻARSCNView can be obtained from worldTransform of ʻARHitTestResult, this is used as the cursor location and orientation. Here, when the cursor is set to SCNPlane, if the location / orientation of the cursor = the location / orientation of the hit, it interferes with the geometry of the plane and flickers, so the vector in the upward direction of the planeworldTransform.columns.1 Use to adjust the cursor position.

//0 in the direction (UP) facing the plane.Set the cursor at a position shifted by 01m
cursorTransform.columns.3 += worldTransform.columns.1 * 0.01
self.cursorNode.simdTransform = cursorTransform

Note that scaling is reset if SCNNode is scaled (scale ≠ (1.0,1.0,1.0)) because it sets a transform. In that case, set worldTransform.columns.0 ~ 2 multiplied by the scaling factor to cursorTransform.columns.0 ~ 2.

It's done

It has a pyramid shape so that the upward direction of the cursor can be easily understood. demo.gif

Source code

ViewController.swift


import ARKit
import SceneKit

class ViewController: UIViewController, ARSCNViewDelegate {

    @IBOutlet weak var scnView: ARSCNView!
    
    private let device = MTLCreateSystemDefaultDevice()!
    private let cursorNode = SCNNode()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //AR Session started
        self.scnView.delegate = self
        let configuration = ARWorldTrackingConfiguration()
        configuration.planeDetection = [.horizontal, .vertical]
        self.scnView.session.run(configuration, options: [.removeExistingAnchors, .resetTracking])
        //Cursor node preparation
        let pyramid = SCNPyramid(width: 0.1, height: 0.03, length: 0.1)
        pyramid.firstMaterial!.diffuse.contents = UIColor.yellow
        self.cursorNode.geometry = pyramid
        self.scnView.scene.rootNode.addChildNode(self.cursorNode)
        self.cursorNode.isHidden = true
    }
    //
    //Anchor added
    //
    func renderer(_: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        guard let planeAnchor = anchor as? ARPlaneAnchor else { return }

        //Added planar geometry node
        guard let geometory = ARSCNPlaneGeometry(device: self.device) else { return }
        geometory.update(from: planeAnchor.geometry)
        let material = SCNMaterial()
        material.lightingModel = .physicallyBased
        material.diffuse.contents = UIColor.red.withAlphaComponent(0.7)
        geometory.materials = [material]
        let planeNode = SCNNode(geometry: geometory)
        DispatchQueue.main.async {
            node.addChildNode(planeNode)
        }
    }
    //
    //Anchor updated
    //
    func renderer(_: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
        guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
        
        DispatchQueue.main.async {
            for childNode in node.childNodes {
                //Update plane geometry
                guard let planeGeometry = childNode.geometry as? ARSCNPlaneGeometry else { continue }
                planeGeometry.update(from: planeAnchor.geometry)
                break
            }
        }
    }
    //
    //Called frame by frame
    //
    func renderer(_ renderer: SCNSceneRenderer, updateAtTime _: TimeInterval) {
        DispatchQueue.main.async {
            //Hit test in the center of the screen
            let bounds = self.scnView.bounds
            let screenCenter =  CGPoint(x: bounds.midX, y: bounds.midY)
            let results = self.scnView.hitTest(screenCenter, types: [.existingPlaneUsingGeometry])
            guard let existingPlaneUsingGeometryResult = results.first(where: { $0.type == .existingPlaneUsingGeometry }),
                  let _ = existingPlaneUsingGeometryResult.anchor as? ARPlaneAnchor else {
                //Hide cursor
                self.cursorNode.isHidden = true
                return
            }
            
            //Post the transform of the hit location to the transform of the cursor
            let worldTransform = existingPlaneUsingGeometryResult.worldTransform
            var cursorTransform = worldTransform
            //0 in the direction (UP) facing the plane.Set the cursor at a position shifted by 01m
            cursorTransform.columns.3 += worldTransform.columns.1 * 0.01
            self.cursorNode.simdTransform = cursorTransform
            
            self.cursorNode.isHidden = false
        }
    }
}

Recommended Posts

Cursor display when placing objects with ARKit + SceneKit
Display 3D objects of SceneKit with Swift UI, etc.
Ground collapse with ARKit + SceneKit
Optical camouflage with ARKit + SceneKit + Metal ①
Web browsing with ARKit + SceneKit + Metal
Optical camouflage with ARKit + SceneKit + Metal ②
Switch the display screen when hovering the tab with jQuery