Search code examples
iosswiftaugmented-realityarkit

Calculate image size corresponding to wall size in Augmented Reality and Drag it smoothly


I'm creating an app that choose image from gallery and place it on wall using ARKit vertical plane detection.I'm also able to scale it properly but dragging is not as expected. Also, i'm also confused about image size. I want it like suppose if wall is of 10 feet and i want to place image of 3*2 feet.I referred many links and docs.Below is the code that renders and add place image:

func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for 
anchor: ARAnchor) {
    if nodeWeCanChange == nil{
        guard let planeAnchor = anchor as? ARPlaneAnchor else { return 
    }
        let width = CGFloat(planeAnchor.extent.x)
        let height = CGFloat(planeAnchor.extent.z)
        nodeWeCanChange = SCNNode(geometry: SCNPlane(width: width, 
        height: height))
        nodeWeCanChange?.position = SCNVector3(planeAnchor.center.x, 0, 
        planeAnchor.center.z)
        nodeWeCanChange?.transform = SCNMatrix4MakeRotation(-Float.pi / 2.0, 1.0, 0.0, 0.0)
        nodeWeCanChange?.geometry?.firstMaterial?.diffuse.contents = UIColor.white
        DispatchQueue.main.async(execute: {
             node.addChildNode(self.nodeWeCanChange!)
             self.zIndex = (self.nodeWeCanChange?.simdPosition.z)!})
    }  }
    }                                                               

func placeImage(image : UIImage){
    guard let pointsPerInch = UIScreen.pointsPerInch else{
       return}
     let pixelWidth = (image.size.width) * (image.scale)
    let pixelHeight = (image.size.height) * (image.scale)
    let inchWidth = pixelWidth/pointsPerInch
    let inchHeight = pixelHeight/pointsPerInch 
    let widthInMetres = (inchWidth * 2.54) / 100
    let heightInMeters = (inchHeight * 2.54) / 100
    let photo = SCNPlane(width: (widthInMetres), height: (heightInMeters))
    nodeWeCanChange?.geometry = photo
    nodeWeCanChange?.geometry?.firstMaterial?.diffuse.contents = image}`

@objc func handleTap(_ gestureRecognize: ThresholdPanGesture)
{
    // Function that handles drag
    let position = gestureRecognize.location(in: augentedRealityView)
    var foundNode:SCNNode? = nil
    do {
         if gestureRecognize.state == .began {
            print("Pan state began")
            //let hitTestOptions = [SCNHitTestOption: Any]()
            let hitResult: [SCNHitTestResult] = (self.augentedRealityView?.hitTest(gestureRecognize.location(in: self.augentedRealityView), options: nil))!
           guard let firstNode  = hitResult.first else {
                    return
                }
            print("first node =\(firstNode.node)")
            if firstNode.node.isEqual(nodeWeCanChange){
                foundNode = nodeWeCanChange
                print("node found")}
            else if (firstNode.node.parent?.isEqual(nodeWeCanChange))!{ print("node found")}
            else{ return print("node not found")}
            latestTranslatePos = position
        }
    }
    if gestureRecognize.state == .changed{
        let deltaX = Float(position.x - latestTranslatePos.x)/700
        let deltaY = Float(position.y - latestTranslatePos.y)/700
         nodeWeCanChange?.localTranslate(by: SCNVector3Make(deltaX, -deltaY , zIndex))
        latestTranslatePos = position
        print("Pan State Changed")
    }
    if gestureRecognize.state == .ended {
        print("Done moving object")
        let deltaX = Float(position.x - latestTranslatePos.x)/700
        let deltaY = Float(position.y - latestTranslatePos.y)/700
        nodeWeCanChange?.localTranslate(by: SCNVector3Make(deltaX, -deltaY , zIndex))
        latestTranslatePos = position
        foundNode = nil
    }
}`

Can any one please help me what i'm missing?


Solution

  • There are two questions here... ^__________^.

    In regard to panning your SCNNode the easiest thing to do would be to use touches, although you can modify the example below quite easily.

    Assuming your content is placed on ARPlaneAnchor and you want to move it on the plane then you can do something like this (assuming you have also selected an SCNNode to be your nodeToDrag) which in my example will be the picture:

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    
        //1. Get The Current Touch Point
        guard let currentTouchPoint = touches.first?.location(in: self.augmentedRealityView),
            //2. Get The Existing ARPlaneAnchor
            let hitTest = augmentedRealityView.hitTest(currentTouchPoint, types: .existingPlane).first else { return }
    
        //3. Convert To World Coordinates
        let worldTransform = hitTest.worldTransform
    
        //4. Set The New Position
        let newPosition = SCNVector3(worldTransform.columns.3.x, worldTransform.columns.3.y, worldTransform.columns.3.z)
    
        //5. Apply To The Node
        nodeToDrag.simdPosition = float3(newPosition.x, newPosition.y, newPosition.z)
    
    }
    

    Alternatively you can try the following:

     override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    
            //1. Get The Current Touch Point
            guard let currentTouchPoint = touches.first?.location(in: self.augmentedRealityView),
            //2. Get The Existing ARPlaneAnchor
            let hitTest = augmentedRealityView.hitTest(currentTouchPoint, types: .existingPlane).first else { return }
    
            //3. Convert To Local Coordinates
            nodeToDrag.simdPosition.x = Float(hitTest.localTransform.columns.3.x)
            nodeToDrag.simdPosition.y = Float(-hitTest.localTransform.columns.3.z)
    
        }
    

    In regard to the sizing you it's not entirely clear what you are wanting to do. However here is a small example with comments that should help:

    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
    
        //1. Check We Have An ARPlaneAnchor
        guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
    
        //2. Get The Approximate Width & Height Of The Anchor
        let width = CGFloat(planeAnchor.extent.x)
        let height = CGFloat(planeAnchor.extent.z)
    
        //3. Create A Dummy Wall To Illustrate The Vertical Plane
        let dummyWall = SCNNode(geometry: SCNPlane(width: width, height: height))
        dummyWall.geometry?.firstMaterial?.diffuse.contents = UIColor.black
        dummyWall.opacity = 0.5
        dummyWall.eulerAngles.x = -.pi/2
        node.addChildNode(dummyWall)
    
        //4. Create A Picture Which Will Be Half The Size Of The Wall
        let picture = SCNNode(geometry: SCNPlane(width: width/2, height: height/2))
        picture.geometry?.firstMaterial?.diffuse.contents = UIColor.white
    
        //5. Add It To The Dummy Wall (As We Dont Assign A Position It Will Be Centered Automatically At SCNVector3Zero)
        dummyWall.addChildNode(picture)
    
        //6. To Prevent Z-Fighting Move The Picture Forward Slightly
        picture.position.z = 0.0001
    }
    

    You could also create a helper function e.g:

    //--------------------------
    //MARK: - CGFloat Extensions
    //--------------------------
    
    extension CGFloat{
    
        /// Desired Scale Factor
        enum Scalar{
    
            case Quarter
            case Third
            case Half
            case ThreeQuarters
            case FullSize
    
            var value: CGFloat{
                switch self {
                case .Quarter:       return 0.25
                case .Third:         return 0.33
                case .Half:          return 0.5
                case .ThreeQuarters: return 0.75
                case .FullSize:      return 1
                }
            }
        }
    
        /// Returns A CGFloat Based On The Desired Scale
        ///
        /// - Parameter size:  Scalar
        /// - Returns: CGFloat
        func scaledToSize(_ size: Scalar) -> CGFloat {
    
           return self * size.value
    
        }
    }
    

    Which you can use to scale your picture based on the extent of the anchor e.g:

     let width = CGFloat(planeAnchor.extent.x)
     let height = CGFloat(planeAnchor.extent.z)
     let picture = SCNNode(geometry: SCNPlane(width: width.scaledToSize(.ThreeQuarters), height: height.scaledToSize(.ThreeQuarters)))
    

    Update:

    Since you were unable to get the example to work, and have now been more specific as to you requirements here is a fully working example (which needs a little tweaking) but it should be more than enough to point you in the right direction:

    //-----------------------
    //MARK: ARSCNViewDelegate
    //-----------------------
    
    extension ARViewController: ARSCNViewDelegate{
    
        func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
    
            if wallAndPictureNode != nil { return }
    
            //1. Check We Have An ARPlaneAnchor
            guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
    
            //2. Get The Approximate Width & Height Of The Anchor
            let width = CGFloat(planeAnchor.extent.x)
            let height = CGFloat(planeAnchor.extent.z)
    
            //3. Create A Dummy Wall To Illustrate The Vertical Plane
            let dummyWall = SCNNode(geometry: SCNPlane(width: width, height: height))
            dummyWall.geometry?.firstMaterial?.diffuse.contents = UIColor.black
            dummyWall.opacity = 0.5
            dummyWall.eulerAngles.x = -.pi/2
            node.addChildNode(dummyWall)
    
            //4. Create A Picture Which Will Be Half The Size Of The Wall
            let picture = SCNNode(geometry: SCNPlane(width: width/2, height: height/2))
            picture.geometry?.firstMaterial?.diffuse.contents = UIColor.white
    
            //5. Add It To The Dummy Wall (As We Dont Assign A Position It Will Be Centered Automatically At SCNVector3Zero)
            dummyWall.addChildNode(picture)
    
            //6. To Prevent Z-Fighting Move The Picture Forward Slightly
            picture.position.z = 0.0001
    
            //7. Set The Wall & Picture Node
            wallAndPictureNode = dummyWall
    
        }
    
    }
    
    class ARViewController: UIViewController {
    
        //1. Create A Reference To Our ARSCNView In Our Storyboard Which Displays The Camera Feed
        @IBOutlet weak var augmentedRealityView: ARSCNView!
    
        //2. Create Our ARWorld Tracking Configuration
        let configuration = ARWorldTrackingConfiguration()
    
        //4. Create Our Session
        let augmentedRealitySession = ARSession()
    
        //5. Create Variable To Store Our Wall With The Picture On It
        var wallAndPictureNode: SCNNode?
    
        //--------------------
        //MARK: View LifeCycle
        //--------------------
    
        override func viewDidLoad() {
    
            super.viewDidLoad()
    
        }
    
        override func viewDidAppear(_ animated: Bool) { setupARSession() }
    
        override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() }
    
        //-------------------
        //MARK: - Interaction
        //-------------------
    
        override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    
            //1. Get The Current Touch Point, Check We Have Our WallAndPictureNode, & Then Check We Have A Valid ARPlaneAnchor
            guard let nodeToMove = wallAndPictureNode,
                let currentTouchPoint = touches.first?.location(in: self.augmentedRealityView),
                let hitTest = augmentedRealityView.hitTest(currentTouchPoint, types: .existingPlane).first else { return }
    
            //3. Move The Wall & Picture
            nodeToMove .simdPosition.x = Float(hitTest.worldTransform.columns.3.x)
            nodeToMove .simdPosition.z = Float(-hitTest.worldTransform.columns.3.y)
    
        }
    
        //---------------
        //MARK: ARSession
        //---------------
    
        /// Sets Up The ARSession
        func setupARSession(){
    
            //1. Set The AR Session
            augmentedRealityView.session = augmentedRealitySession
            augmentedRealityView.delegate = self
    
            //2. Conifgure The Type Of Plane Detection
            configuration.planeDetection = [.vertical]
    
            //3. Configure The Debug Options
            augmentedRealityView.debugOptions = debug(.None)
    
            //4. Run The Session
            augmentedRealitySession.run(configuration, options: [.resetTracking, .removeExistingAnchors])
    
            //5. Disable The IdleTimer
            UIApplication.shared.isIdleTimerDisabled = true
    
        }
    
    }
    

    Hope it helps...