Search code examples
iosswiftscenekitarkit

How can I fix the position of a 3D usdz object in the center of the screen?


In my app I am adding a USDZ object in an AR World. I want to make it so that the object follows the movement of the camera and always stays in the center of the screen. I am unable to do it.

I have tried adding the object and updating it's position in the renderer did update function but it did not work. Here is my code:

import UIKit
import ARKit
import SceneKit
import simd

class ARViewController: UIViewController, UIGestureRecognizerDelegate {

    @IBOutlet weak var arView: ARSCNView!
    var sceneView: ARSCNView!
    var cameraNode: SCNNode!
    let position = SCNVector3(x: 0, y: 5, z: 10)
    var scnView: SCNView!
    var scnScene: SCNScene!
    var popupView = CustomViewAR()
    var latitude = CLLocationDegrees()
    var longitude = CLLocationDegrees()
    var isShow : Bool?
    let popupPlane = SCNPlane()
    var popupNode = SCNNode()
    var tapgest = UITapGestureRecognizer()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let config = ARWorldTrackingConfiguration()
        config.planeDetection = .horizontal
        config.environmentTexturing = .automatic
        arView.delegate = self
        arView.session.run(config)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            self.addObject()
        }
        addTapGesture()
        setUpPopUpView()

    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
      let configuration = ARWorldTrackingConfiguration()
      arView.session.run(configuration)
    }
      
    override func viewWillDisappear(_ animated: Bool) {
      super.viewWillDisappear(animated)
      arView.session.pause()
    }
    func addObject(){
        
        
        let scene = SCNScene()
        let usdzNode = SCNNode()
//
        let usdzScene = SCNScene(named: "cube.usdz")!
        let usdzChildNodes = usdzScene.rootNode.childNodes
        for node in usdzChildNodes {
            usdzNode.addChildNode(node)
        }

        usdzNode.position = SCNVector3(0, 0, -2) // set the position of the object

        scene.rootNode.addChildNode(usdzNode)
//        scene!.rootNode.position = SCNVector3(0, 0, -1)
        let ambientLightNode = SCNNode()
                       ambientLightNode.light = SCNLight()
                       ambientLightNode.light?.type = .ambient
                       ambientLightNode.light?.color = UIColor.darkGray
        scene.rootNode.addChildNode(ambientLightNode)
//        scene.rootNode.position = SCNVector3(0, 0, -1)
                        let cameraNode = SCNNode()
                        cameraNode.camera = SCNCamera()
                        // 3: Place camera
                        cameraNode.position = SCNVector3(x: 0, y: 0, z: -2)
                        // 4: Set camera on scene
        scene.rootNode.addChildNode(cameraNode)
//        arView.allowsCameraControl = true
        let simdVector = SIMD3<Float>(x: 100, y: 100, z: 100)
        let vector = SCNVector3(simdVector)
        scene.rootNode.scale = vector
        arView.scene = scene
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
        arView.addGestureRecognizer(panGesture)

        let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinchGesture(_:)))
        arView.addGestureRecognizer(pinchGesture)
        
        let rotateGesture = UIRotationGestureRecognizer(target: self, action: #selector(handleRotateGesture(_:)))
        arView.addGestureRecognizer(rotateGesture)

    }
    @objc func handleRotateGesture(_ gesture: UIRotationGestureRecognizer) {
        guard let currentFrame = arView.session.currentFrame else {
            return
        }
        
        let rotation = Float(gesture.rotation)
        let rotationDelta = simd_quatf(angle: rotation, axis: simd_float3(1, 1, 1))
        
        let usdzNode = arView.scene.rootNode.childNodes.first!
        let usdzTransform = usdzNode.simdTransform
        
        let newTransform = simd_mul(usdzTransform, simd_float4x4(rotationDelta))
        usdzNode.simdTransform = newTransform
        
        gesture.rotation = 0
    }

    @objc func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
        guard let currentFrame = arView.session.currentFrame else {
            return
        }
        
        let translation = gesture.translation(in: arView)
        let deltaX = Float(translation.x) / Float(arView.bounds.width)
        let deltaY = Float(translation.y) / Float(arView.bounds.height)
        
        let usdzNode = arView.scene.rootNode.childNodes.first!
        var usdzTransform = usdzNode.simdTransform
        
        // Get the camera's orientation in world space
        let camera = currentFrame.camera
        let cameraTransform = camera.transform.inverse
//        let cameraForward = cameraTransform.forward
        let rotationFactor: Float = 10.0
        // Rotate the USDZ object around the world Y and X axes based on gesture translation
        usdzTransform *= simd_float4x4(simd_quatf(angle: -deltaX * rotationFactor, axis: simd_float3(0, 1, 0)))
        usdzTransform *= simd_float4x4(simd_quatf(angle: deltaY * rotationFactor, axis: simd_float3(1, 0, 0)))
        
        usdzNode.simdTransform = usdzTransform
        
        gesture.setTranslation(CGPoint.zero, in: arView)
    }




    @objc func handlePinchGesture(_ gesture: UIPinchGestureRecognizer) {
        guard let currentFrame = arView.session.currentFrame else {
            return
        }
        
        let usdzNode = arView.scene.rootNode.childNodes.first!
        let usdzTransform = usdzNode.simdTransform
        
        var scaleMatrix = matrix_identity_float4x4
        let scale = Float(gesture.scale)
        scaleMatrix.columns.0.x = scale
        scaleMatrix.columns.1.y = scale
        scaleMatrix.columns.2.z = scale
        
        let newTransform = simd_mul(usdzTransform, scaleMatrix)
        
        usdzNode.simdTransform = newTransform
        
        gesture.scale = 1
    }


    func addTapGesture()
    {
        let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
        arView.addGestureRecognizer(tap)
    }
    
    @objc func handleTap(_ gesture: UITapGestureRecognizer) {
        let location = gesture.location(in: arView)
        let hitResults = arView.hitTest(location, options: nil)
        
        if let hitNode = hitResults.first?.node {
            showPopup(nextTo: hitNode)
            
            tapgest = UITapGestureRecognizer(target: self, action: #selector(closePopup))
            arView.addGestureRecognizer(tapgest)

            isShow = true
        }
        
    }
    
    @objc func dismissPopupView() {
//        popupView.alpha = 0
    }
    
    func setUpPopUpView(){
        
        popupView = CustomViewAR().loadNib() as! CustomViewAR

        let geocoder = CLGeocoder()
        let location = CLLocation(latitude: latitude, longitude: longitude)

        geocoder.reverseGeocodeLocation(location) { (placemarks, error) in
            guard error == nil else {
                print("Error: \(error!)")
                return
            }

            guard let placemark = placemarks!.first else {
                print("Error: placemark is nil")
                return
            }
            let address = "\(placemark.name ?? "Could not find info"), \( placemark.locality ?? "No City"), \( placemark.administrativeArea ?? "No Administrative Area"), \( placemark.country ?? "No Country")"
            self.popupView.setValues(labelName: placemark.name!, placeAddress: address)

        }
    }
    
    func showPopup(nextTo node: SCNNode) {
            
        popupView.isHidden = false
        setUpPopUpView()
        popupView.bringSubviewToFront(popupView.lblAddress)
        popupView.bringSubviewToFront(popupView.lblPlaceName)
        popupView.lblAddress.textColor = UIColor.black
            
        let worldPosition = node.simdWorldPosition - 30
        let x = worldPosition.x + 30 // adjust as needed
        let y = worldPosition.y + 30// adjust as needed
        
        popupView.frame = CGRect(x: Int(x), y: Int(y), width: 250, height: 250)
        
        popupPlane.width = 60 // adjust as needed
        popupPlane.height = 60 // adjust as needed
        popupPlane.cornerRadius = 10
        popupPlane.firstMaterial?.diffuse.contents = popupView
        
        popupNode = SCNNode(geometry: popupPlane)
//        popupNode.position = SCNVector3(0, 0, -1000)
        popupNode.opacity = 0.8
        popupNode.simdWorldPosition = worldPosition // set position using simdWorldPosition
        let constraint = SCNBillboardConstraint()
        constraint.freeAxes = [.Y]
        popupNode.constraints = [constraint]
            
        arView.scene.rootNode.addChildNode(popupNode)

        // Animate the popup to appear
        SCNTransaction.begin()
        SCNTransaction.animationDuration = 0.5
        SCNTransaction.commit()
    }


    
    @objc func closePopup() {
        
        if isShow == true {
            SCNTransaction.begin()
                    SCNTransaction.animationDuration = 0.5
                    popupView.alpha = 0
                    SCNTransaction.commit()
            popupView.isHidden = true

            let configuration = ARWorldTrackingConfiguration()
            arView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
            arView.setNeedsDisplay()
            popupNode.isHidden = true
            isShow = false
            arView.removeGestureRecognizer(tapgest)
            setUpPopUpView()

        } else {
            
        }
    }

    func addAnimation(node: SCNNode) {
        let rotateOne = SCNAction.rotateBy(x: 0, y: CGFloat(Float.pi), z: 0, duration: 5.0)
        let hoverUp = SCNAction.moveBy(x: 0, y: 0.2, z: 0, duration: 2.5)
        let hoverDown = SCNAction.moveBy(x: 0, y: -0.2, z: 0, duration: 2.5)
        let hoverSequence = SCNAction.sequence([hoverUp, hoverDown])
        let rotateAndHover = SCNAction.group([rotateOne, hoverSequence])
        let repeatForever = SCNAction.repeatForever(rotateAndHover)
        node.runAction(repeatForever)
      }
    
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
        if touch.view == popupView || touch.view?.isDescendant(of: popupView) == true {
            
            return false
            
        }else{
            popupView.alpha = 0
            return true
        }
    }
    
    @IBAction func backBtnTapped(_ sender: Any) {
        self.dismiss(animated: true, completion: nil)
    }
}
extension ARViewController: ARSCNViewDelegate, SCNSceneRendererDelegate {
  func session(_ session: ARSession,
               didFailWithError error: Error) {
  print("Session Failed - probably due to lack of camera access")
}
  
func sessionWasInterrupted(_ session: ARSession) {
  print("Session interrupted")
}
  
func sessionInterruptionEnded(_ session: ARSession) {
  print("Session resumed")
 }
    
}
extension UIView {
    /** Loads instance from nib with the same name. */
    func loadNib() -> UIView {
        let bundle = Bundle(for: type(of: self))
        let nibName = "CustomViewAR"
        let nib = UINib(nibName: nibName, bundle: bundle)
        return nib.instantiate(withOwner: self, options: nil).first as! UIView
    }
}


Solution

  • Your SCNView or ARSCNView (depending on which you use the camera) should both have a so called pointOfView, which is in fact a SCNNode. You could try to add your Object(s) (from the USDZ file) to this node, with a position value of i.Ex: SCNVector3(0.0, 0.0, -2.0) - depending on its size of course. (Instead to add it to the rootNode of the main Scene). This should keep the Object always in front of the Camera, regardless in which direction you point the Camera or your device (if you use AR).