3
votes

I am trying to just detect a tap on an SCNNode instantiated in my AR scene. It does not seem to work like SceneKit hit test results here, and I don't have much experience with scene kit.

I just want to detect if the tapped point is contained within any node in the scene, thus detecting a tap on the object. What I have tried from other answers:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }

        let results = sceneView.hitTest(touch.location(in: sceneView), types: [ARHitTestResult.ResultType.featurePoint])
        guard let hitFeature = results.last else { return }

        let hitTransform = SCNMatrix4.init(hitFeature.worldTransform)

        let hitPosition = SCNVector3Make(hitTransform.m41,
                                         hitTransform.m42,
                                         hitTransform.m43)

        if(theDude.node.boundingBoxContains(point: hitPosition))
        {

        }

    }

Nothing gets printed with the last if statement, which I got with:

extension SCNNode {
    func boundingBoxContains(point: SCNVector3, in node: SCNNode) -> Bool {
        let localPoint = self.convertPosition(point, from: node)
        return boundingBoxContains(point: localPoint)
    }

    func boundingBoxContains(point: SCNVector3) -> Bool {
        return BoundingBox(self.boundingBox).contains(point)
    }
}

struct BoundingBox {
    let min: SCNVector3
    let max: SCNVector3

    init(_ boundTuple: (min: SCNVector3, max: SCNVector3)) {
        min = boundTuple.min
        max = boundTuple.max
    }

    func contains(_ point: SCNVector3) -> Bool {
        let contains =
            min.x <= point.x &&
                min.y <= point.y &&
                min.z <= point.z &&

                max.x > point.x &&
                max.y > point.y &&
                max.z > point.z

        return contains
    }
}

And the ARHitTestResult doesn't contain nodes. What can I do to detect a tap on a node in an ARScene?

2

2 Answers

26
votes

When you're working with ARSCNView, there are two kinds of hit testing you can do, and they use entirely separate code paths.

  • Use the ARKit method hitTest(_:types:) if you want to hit test against the real world (or at least, against ARKit's estimate of where real-world features are). This returns ARHitTestResult objects, which tell you about real-world features like detected planes. In other words, use this method if you want to find a real object that anyone can see and touch without a device — like the table you're pointing your device at.
  • Use the SceneKit method hitTest(_:options:) if you want to hit test against SceneKit content; that is, to search for virtual 3D objects you've placed in the AR scene. This returns SCNHitTestResult objects, which tell you about things like nodes and geometry. Use this method if you want to find SceneKit nodes, the model (geometry) in a node, or the specific point on the geometry at a tap location.

In both cases the 3D position found by a hit test is the same, because ARSCNView makes sure that the virtual "world coordinates" space matches real-world space.

It looks like you're using the former but expecting the latter. When you do a SceneKit hit test, you get results if and only if there's a node under the hit test point — you don't need any kind of bounding box test because it's already being done for you.

9
votes

Add a UITapGestureRecognizer

let tapRec = UITapGestureRecognizer(target: self, action: #selector(ViewController.handleTap(rec:)))

Inside your handleTap method on state .ended:

@objc func handleTap(rec: UITapGestureRecognizer){
    if rec.state == .ended {
        let location: CGPoint = rec.location(in: sceneView)
        let hits = self.sceneView.hitTest(location, options: nil)
        if let tappednode = hits.first?.node {
            //do something with tapped object
        }
    }
}