Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to detect touch and show new SCNPlane using ARKit?

Now I am able to show different SCNPlane, when card detected. After displaying SCNPlanes, the user touches any plane to show new SCNPlane. But right now touch is working properly but new SCNPlane is not showing.

Here is the code I've tried:

var cake_1_PlaneNode : SCNNode? = nil

func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {

    guard let imageAnchor = anchor as? ARImageAnchor else { return }

    if let imageName  = imageAnchor.referenceImage.name {            
        print(imageName)

        if imageName == "menu" {

            // Check To See The Detected Size Of Our menu Card (Should By 5cm*3cm)
            let menuCardWidth = imageAnchor.referenceImage.physicalSize.width
            let menuCardHeight =  imageAnchor.referenceImage.physicalSize.height

            print(
                """
                We Have Detected menu Card With Name \(imageName)
                \(imageName)'s Width Is \(menuCardWidth)
                \(imageName)'s Height Is \(menuCardHeight)
                """)

            //raspberry
            //cake 1

            let cake_1_Plane = SCNPlane(width: 0.045, height: 0.045)
            cake_1_Plane.firstMaterial?.diffuse.contents = UIImage(named: "france")
            cake_1_Plane.cornerRadius = 0.01

            let cake_1_PlaneNode = SCNNode(geometry: cake_1_Plane)
            self.cake_1_PlaneNode = cake_1_PlaneNode
            cake_1_PlaneNode.eulerAngles.x = -.pi/2
            cake_1_PlaneNode.runAction(SCNAction.moveBy(x: 0.15, y: 0, z: -0.125, duration: 0.75))

            node.addChildNode(cake_1_PlaneNode)
            self.sceneView.scene.rootNode.addChildNode(node)                
        }  
   }       

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

    let touch = touches.first as! UITouch
    if(touch.view == self.sceneView){
        //print("touch working")
        let viewTouchLocation:CGPoint = touch.location(in: sceneView)
        guard let result = sceneView.hitTest(viewTouchLocation, options: nil).first else {
            return
        }

        if let planeNode = cake_1_PlaneNode, cake_1_PlaneNode == result.node{
            print("match")
            cake_1() 
        }
    }
}

func cake_1() {

    let plane = SCNPlane(width: 0.15  , height: 0.15)
    plane.firstMaterial?.diffuse.contents = UIColor.black.withAlphaComponent(0.75)

    let planeNodee = SCNNode(geometry: plane)
    planeNodee.eulerAngles.x = -.pi / 2
    planeNodee.runAction(SCNAction.moveBy(x: 0.21, y: 0, z: 0, duration: 0))

} //cake_1

Follow this link: Detect touch on SCNNode in ARKit.

like image 565
PvDev Avatar asked Aug 30 '18 06:08

PvDev


2 Answers

Looking at your code I can see several issues (not to mention the naming conventions for your variables and methods).

Firstly, you are creating a Global Variable which you have declared like so:

var cake_1_PlaneNode : SCNNode? = nil

However you use both a Local and Global Variable for your cake_1_PlaneNode in yourDelegate Callback:

let cake_1_PlaneNode = SCNNode(geometry: cake_1_Plane)
self.cake_1_PlaneNode = cake_1_PlaneNode

Which should simply read like so:

self.cake_1_PlaneNode = SCNNode(geometry: cake_1_Plane)

Secondly, you are adding your cake_1_PlaneNode to the rootNode of your ARSCNView rather than your detected ARImageAnchor which is probably what you don't want to do, since when an ARAnchor is detected:

You can provide visual content for the anchor by attaching geometry (or other SceneKit features) to this node or by adding child nodes.

As such, this method (unless you actually want to do it like this) is unnecessary.

The remaining issues lie within your cake_1 function itself.

Firstly you are not actually adding your planeNodee to your sceneHierachy.

Since you haven't specified whether or not the newly initialised planeNode should be added directly to your ARSCNView or as a childNode of your cake_1_planeNode your function should include one of the following:

self.sceneView.scene.rootNode.addChildNode(planeNodee)    
self.cake_1_planeNode.addChildNode(planeNodee)

In addition there is probably also no need to rotate your planeNodee since by default an SCNPlane is rendered vertically.

Since you haven't stipulated where you will be placing your content, it could be that using -.pi / 2 is unnecessary, since this could make it virtually invisible to the naked eye.

One other issue which could also account for you not seeing your node, when you actually add it, is the Z position.

If you set 2 nodes at the same position you will likely experience an issue know as Z-fighting (which you can read more about here). As such you should probably move your added node forward slightly when adding it e.g. SCNVector3 (0,0,0.001)to account for this.

Based on all of these points, I have provided a fully working and commented example below:

import UIKit
import ARKit

//-------------------------
//MARK: - ARSCNViewDelegate
//-------------------------

extension ViewController: ARSCNViewDelegate {

    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {

        //1. Check We Have An ARImageAnchor, Then Get It's Reference Image & Name
        guard let imageAnchor = anchor as? ARImageAnchor else { return }

        let detectedTarget = imageAnchor.referenceImage

        guard let detectedTargetName = detectedTarget.name  else { return }

        //2. If We Have Detected Our Virtual Menu Then Add The CakeOnePlane
        if detectedTargetName == "cakeMenu" {

            let cakeOnePlaneGeometry = SCNPlane(width: 0.045, height: 0.045)
            cakeOnePlaneGeometry.firstMaterial?.diffuse.contents = UIColor.cyan
            cakeOnePlaneGeometry.cornerRadius = 0.01

            let cakeOnPlaneNode = SCNNode(geometry: cakeOnePlaneGeometry)
            cakeOnPlaneNode.eulerAngles.x = -.pi/2

            //3. To Allow Us To Easily Keep Track Our Our Currently Added Node We Will Assign It A Unique Name
            cakeOnPlaneNode.name = "Strawberry Cake"

            node.addChildNode(cakeOnPlaneNode)
            cakeOnPlaneNode.runAction(SCNAction.moveBy(x: 0.15, y: 0, z: 0, duration: 0.75))


        }
    }
}

class ViewController: UIViewController {

    @IBOutlet var augmentedRealityView: ARSCNView!
    let augmentedRealitySession = ARSession()
    let configuration = ARWorldTrackingConfiguration()

    //------------------
    //MARK: - Life Cycle
    //------------------

    override func viewDidLoad() {
        super.viewDidLoad()

        setupARSession()
    }


    //-----------------
    //MARK: - ARSession
    //-----------------

    /// Runs The ARSession
    func setupARSession(){

        //1. Load Our Detection Images
        guard let detectionImages = ARReferenceImage.referenceImages(inGroupNamed: "AR Resources", bundle: nil) else { return }

        //2. Configure & Run Our ARSession
        augmentedRealityView.session = augmentedRealitySession
        augmentedRealityView.delegate = self
        configuration.detectionImages = detectionImages
        augmentedRealitySession.run(configuration, options: [.resetTracking, .removeExistingAnchors])

    }

    //--------------------
    //MARK: - Interaction
    //--------------------

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

        //1. Get The Current Touch Location & Perform An ARSCNHitTest To Check For Any Hit SCNNode's
        guard let currentTouchLocation = touches.first?.location(in: self.augmentedRealityView),
             let hitTestNode = self.augmentedRealityView.hitTest(currentTouchLocation, options: nil).first?.node else { return }

        //2. If We Have Hit Our Strawberry Cake Then We Call Our makeCakeOnNode Function
        if let cakeID = hitTestNode.name, cakeID == "Strawberry Cake"{

            makeCakeOnNode(hitTestNode)

        }
    }


    /// Adds An SCNPlane To A Detected Cake Target
    ///
    /// - Parameter node: SCNNode
    func makeCakeOnNode(_ node: SCNNode){

        let planeGeometry = SCNPlane(width: 0.15  , height: 0.15)
        planeGeometry.firstMaterial?.diffuse.contents = UIColor.black.withAlphaComponent(0.75)

        let planeNode = SCNNode(geometry: planeGeometry)
        planeNode.position = SCNVector3(0, 0, 0.001)
        planeNode.runAction(SCNAction.moveBy(x: 0.21, y: 0, z: 0, duration: 0))
        node.addChildNode(planeNode)

    }
}

Which yields the following on my device:

enter image description here

For your information, this seems to show that your calculations for placing your content are off (unless of course this is the desired result).

As you can see, all of content rendered correctly, however the spacing of these was quite large, and as such you will likely need to pan your device somewhat to see it all when testing and developing further.

Hope it helps...

like image 56
BlackMirrorz Avatar answered Oct 21 '22 15:10

BlackMirrorz


***Please use descriptive and clear names for your variables and functions, it is very hard to read and understand your code. You can read more about swift styling guidelines here: https://github.com/raywenderlich/swift-style-guide#naming

You are creating a new plane when the user touches the screen, but you are not adding that plane to the scene, therefore your "cake_1()" function only creates a new plane. When ARKit detects an image, it automatically creates an empty node and adds it to our scene, at the center of the detected image. We must first keep a reference to the node ARKit has added for us when the image is detected. Add this variable to the top of your class:

var detectedImageNode: SCNNode?

Then in func renderer(renderer: didAdd node:, for anchor:) add the following line:

detectedImageNode = node

Now that we have a reference to the node, we can easily add and remove other nodes. Add the following line at the end of cake_1():

if let detectedImageNode = detectedImageNode {
    cake_1_PlaneNode?.removeFromParentNode()
    detectedImageNode.addChildNode(planeNodee)
}

Your final code should look like this:

func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {

    guard let imageAnchor = anchor as? ARImageAnchor else { return }

    if let imageName  = imageAnchor.referenceImage.name {
        print(imageName)

        if imageName == "menu" {

            let cake_1_Plane = SCNPlane(width: 0.045, height: 0.045)
            cake_1_Plane.firstMaterial?.diffuse.contents = UIImage(named: "france")
            cake_1_Plane.cornerRadius = 0.01

            let cake_1_PlaneNode = SCNNode(geometry: cake_1_Plane)
            self.cake_1_PlaneNode = cake_1_PlaneNode
            cake_1_PlaneNode.eulerAngles.x = -.pi/2
            cake_1_PlaneNode.runAction(SCNAction.moveBy(x: 0.15, y: 0, z: -0.125, duration: 0.75))

            node.addChildNode(cake_1_PlaneNode)
            // No need to add the following line. The node is already added to the scene
            //self.sceneView.scene.rootNode.addChildNode(node)                
            detectedImageNode = node
        }
    }
}

func cake_1() {

        let plane = SCNPlane(width: 0.15  , height: 0.15)
        plane.firstMaterial?.diffuse.contents = UIColor.black.withAlphaComponent(0.75)

        let planeNodee = SCNNode(geometry: plane)
        planeNodee.eulerAngles.x = -.pi / 2

        if let detectedImageNode = detectedImageNode {
            cake_1_PlaneNode?.removeFromParentNode()
            detectedImageNode.addChildNode(planeNodee)
        }    
    }

Alternative solution

If you are just trying to change the image of the plane then an easier way to approach this is to just change the texture of the plane. Replace the contents of cake_1() with:

if let planeGeometry = cake_1_PlaneNode?.geometry {
    planeGeometry.firstMaterial?.diffuse.contents = UIImage(named: "newImage")
}
like image 23
SilentK Avatar answered Oct 21 '22 16:10

SilentK