'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 1:[1]

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....  }

}

Solution 2:[2]

an 'object' is a SCNNode. These are displayed in a 3D scene, the SCNScene. each time you tap on the screen, you apply the scene to the sceneView instead of adding a node to the scene.

You also need to find where the user has tapped in the scene, ie the 3D position of the touch. This required a hit test.

try this

class ViewController: UIViewController, ARSCNViewDelegate {

@IBOutlet var sceneView: ARSCNView!

override func viewDidLoad() {
    super.viewDidLoad()
    sceneView.delegate = self
}

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    let configuration = ARWorldTrackingConfiguration()
    sceneView.session.run(configuration)
}


override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard
        let touchPosition = touches.first?.location(in: sceneView),
        let hitTest = sceneView.hitTest(touchPosition, types: .featurePoint).first
        else {return}

    let node = SCNScene(named: "art.scnassets/dontCare.scn")!.rootNode.childNodes.first!
    node.simdTransform = hitTest.worldTransform
    sceneView.scene.rootNode.addChildNode(node)
}

}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2