Search code examples
swiftscenekitaugmented-realityarkithittest

Place multiple SCN objects in touchesBegan method


My Swift code below uses func touchesBegan to place a SCN object in the ARKit view. The problem is – it's only placing the object one time. I would like to create the code in a way that users can select any area to place the SCN object and it can place it as many times as they want too.

Here's GitHub link.

enter image description here

class ViewController: UIViewController, ARSCNViewDelegate { 

    @IBOutlet var sceneView: ARSCNView! 

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

        // Handle the shooting
        guard let frame = sceneView.session.currentFrame else { return }
        let camMatrix = SCNMatrix4(frame.camera.transform)

        let direction = SCNVector3Make(camMatrix.m31 * 5.0, 
                                       camMatrix.m32 * 10.0, 
                                       camMatrix.m33 * 5.0)

        let position = SCNVector3Make(camMatrix.m41, camMatrix.m42, camMatrix.m43)
        let scene = SCNScene(named: "art.scnassets/dontCare.scn")!
        sceneView.scene = scene
    }    
}  

enter image description here


Solution

  • Tip: If you use RealityKit, read this post.

    Solution 1

    Adding 3D models using touchesBegan(:with:)

    Use the following code to get a desired effect (place as many objects into a scene as you want):

    enter image description here

    At first create an extension for your convenience:

    import ARKit
    
    extension SCNVector3 {
        
        static func + (lhs: SCNVector3, rhs: SCNVector3) -> SCNVector3 {
            return SCNVector3(lhs.x + rhs.x, lhs.y + rhs.y, lhs.z + rhs.z)
        }
    }
    

    Then use it in your ViewController for adding a pointOfView.position to desiredVector:

    class ViewController: UIViewController {
    
        @IBOutlet var sceneView: ARSCNView!
        
        override func touchesBegan(_ touches: Set<UITouch>,
                                  with event: UIEvent?) {
            
            sceneView.isMultipleTouchEnabled = true
            
            guard let pointOfView = sceneView.pointOfView   // Camera of SCNScene
            else { return }
            
            let cameraMatrix = pointOfView.transform
            
            let desiredVector = SCNVector3(cameraMatrix.m31 * -0.5,
                                           cameraMatrix.m32 * -0.5,
                                           cameraMatrix.m33 * -0.5)
            
            // What the extension SCNVector3 is for //
            let position = pointOfView.position + desiredVector 
            
            let sphereNode = SCNNode()
            sphereNode.geometry = SCNSphere(radius: 0.05)
            sphereNode.geometry?.firstMaterial?.diffuse.contents = UIColor.green
            sphereNode.position = position
            sceneView.scene.rootNode.addChildNode(sphereNode)
        }
        override func viewDidLoad() {
            super.viewDidLoad()
            let scene = SCNScene(named: "art.scnassets/myScene.scn")!
            sceneView.scene = scene
        }
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
            let config = ARWorldTrackingConfiguration()
            sceneView.session.run(config)
        }
    }
    

    And if you want to retrieve a 3D model from .scn file use the following code:

    (Instead of the sphereNode):

    var model = SCNNode()
    let myScene = SCNScene(named: "art.scnassets/ship.scn")
    
    // Model's name in a Scene graph hierarchy.
    // Pay particular attention – it's not a name of .scn file.
    let nodeName = "ship"
        
    model = (myScene?.rootNode.childNode(withName: nodeName, recursively: true))!
    model.position = position
    sceneView.scene.rootNode.addChildNode(model)
    

    enter image description here


    Solution 2

    Adding 3D models using Plane Detection + Hit-Testing

    Use the following code if you want to add models using plane detection and Hit-testing :

    At first create an extension for your convenience:

    extension float4x4 {
        var simdThree: SIMD3<Float> {
            let translation = self.columns.3            
            return SIMD3<Float>(translation.x, translation.y, translation.z)
        }
    }
    

    Then use it in the ViewController:

    class ViewController: UIViewController {
    
        @IBOutlet weak var sceneView: ARSCNView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
            addGesture()
        }
    
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
            sceneView.delegate = self             // for ARSCNViewDelegate
    
            let config = ARWorldTrackingConfiguration()
            config.planeDetection = [.horizontal]
            sceneView.session.run(config)
        }
    
        func addGesture() {
            let tapGesture = UITapGestureRecognizer(target: self, 
                                                    action: #selector(addModel))
            sceneView.addGestureRecognizer(tapGesture)
        }
    
        // Don't forget to drag-and-drop TapGestureRecognizer object from library
        @objc func addModel(recognizer: UIGestureRecognizer) {
            
            let tap: CGPoint = recognizer.location(in: sceneView)
    
            let results: [ARHitTestResult] = sceneView.hitTest(tap, 
                                               types: .existingPlaneUsingExtent)
            
            guard let hitTestResult = results.first 
            else { return }
    
            let translation = hitTestResult.worldTransform.simdThree
            let x = translation.x
            let y = translation.y
            let z = translation.z
            
            guard let scene = SCNScene(named: "art.scnassets/myScene.scn"),
                  let robotNode = scene.rootNode.childNode(withName: "robot", 
                                                        recursively: true)
            else { return }
    
            robotNode.position = SCNVector3(x, y, z)
            robotNode.scale = SCNVector3(0.02, 0.02, 0.02)
            sceneView.scene.rootNode.addChildNode(robotNode)
        }
    }
    

    And, you have to implement a logic inside two renderer() methods for ARPlaneAnchors:

    extension ViewController: ARSCNViewDelegate {
    
        func renderer(_ renderer: SCNSceneRenderer,
                     didAdd node: SCNNode,
                      for anchor: ARAnchor) { // your logic here....  }
    
        func renderer(_ renderer: SCNSceneRenderer,
                  didUpdate node: SCNNode,
                      for anchor: ARAnchor) { // your logic here....  }
    
    }