Search code examples
iosswiftcollision-detectionscenekitarkit

Can't detect collision between rootNode and pointOfView child nodes in SceneKit / ARKit


In an AR app, I want to detect collisions between the user walking around and the walls of an AR node that I construct. In order to do that, I create an invisible cylinder right in front of the user and set it all up to detect collisions.

The walls are all part of a node which is a child of sceneView.scene.rootNode. The cylinder, I want it to be a child of sceneView.pointOfView so that it would always follow the camera. However, when I do so, no collisions are detected.

I know that I set it all up correctly, because if instead I set the cylinder node as a child of sceneView.scene.rootNode as well, I do get collisions correctly. In that case, I continuously move that cylinder node to always be in front of the camera in a renderer(updateAtTime ...) function. So I do have a workaround, but I'd prefer it to be a child of pointOfView.

Is it impossible to detect collisions if nodes are children of different root nodes? Or maybe I'm missing something in my code? The contactDelegate is set like that: sceneView.scene.physicsWorld.contactDelegate = self so maybe this only includes sceneView.scene, but will exclude sceneView.pointOfView??? Is that the issue?

Here's what I do:

I have a separate file to create and configure my cylinder node which I call pov:

import Foundation
import SceneKit


func createPOV() -> SCNNode {
    
    let pov = SCNNode()
    pov.geometry = SCNCylinder(radius: 0.1, height: 4)
    pov.geometry?.firstMaterial?.diffuse.contents = UIColor.blue
    pov.opacity = 0.3    // will be set to 0 when it'll work correctly
    
    pov.physicsBody = SCNPhysicsBody(type: .kinematic, shape: nil)
    pov.physicsBody?.isAffectedByGravity = false
    pov.physicsBody?.mass = 1
    
    pov.physicsBody?.categoryBitMask = BodyType.cameraCategory.rawValue
    pov.physicsBody?.collisionBitMask = BodyType.wallsCategory.rawValue
    pov.physicsBody?.contactTestBitMask = BodyType.wallsCategory.rawValue
    
    pov.simdPosition = simd_float3(0, -1.5, -0.3)   // this position only makes sense when setting as a child of pointOfView, otherwise the position will always be changed by renderer
    
    return pov
    
}

Now in my viewController.swift file I call this function and set is as a child of either root nodes:

pov = createPOV()
sceneView.pointOfView?.addChildNode(pov!)

(Don't worry right now about not checking and unwrapping). The above does not detect collisions. But if instead I add it like so:

sceneView.scene.rootNode.addChildNode(pov!)

then collisions are detected just fine.

But then I need to always move this cylinder to be in front of the camera and I do it like that:

func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {

        guard let pointOfView = sceneView.pointOfView else {return}
        let currentPosition = pointOfView.simdPosition
        let currentTransform = pointOfView.simdTransform
        let orientation = SCNVector3(-currentTransform.columns.2.x, -currentTransform.columns.2.y, -currentTransform.columns.2.z)
        let currentPositionOfCamera = orientation + SCNVector3(currentPosition)
        
        DispatchQueue.main.async {
            self.pov?.position = currentPositionOfCamera
        }
}

For completeness, here's the code I use to configure the node of walls in ViewController (they're built elsewhere in another function):

        node?.physicsBody = SCNPhysicsBody(type: .dynamic, shape: SCNPhysicsShape(node: node!, options: nil))
        node?.physicsBody?.isAffectedByGravity = false
        node?.physicsBody?.mass = 1
        node?.physicsBody?.damping = 1.0            // remove linear velocity, needed to stop moving after collision
        node?.physicsBody?.angularDamping = 1.0     // remove angular velocity, needed to stop rotating after collision
        node?.physicsBody?.velocityFactor = SCNVector3(1.0, 0.0, 1.0)           // will allow movement only in X and Z coordinates
        node?.physicsBody?.angularVelocityFactor = SCNVector3(0.0, 1.0, 0.0)    // will allow rotation only around Y axis

        node?.physicsBody?.categoryBitMask = BodyType.wallsCategory.rawValue
        node?.physicsBody?.collisionBitMask = BodyType.cameraCategory.rawValue
        node?.physicsBody?.contactTestBitMask = BodyType.cameraCategory.rawValue

And here's my physycsWorld(didBegin contact) code:

    func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
        
        if contact.nodeA.physicsBody?.categoryBitMask == BodyType.wallsCategory.rawValue || contact.nodeB.physicsBody?.categoryBitMask == BodyType.wallsCategory.rawValue {
            print("Begin COLLISION")           
            contactBeginLabel.isHidden = false
        }
    }

So I print something to the console and I also turn on a label on the view so I'll see that collision was detected (and the walls indeed move as a whole when it works).

So Again, it all works fine when the pov node is a child of sceneView.scene.rootNode, but not if it's a child of sceneView.pointOfView.

Am I doing something wrong or is this a limitation of collision detection?

Is there something else I can do to make this work, besides the workaround I already implemented?

Thanks!


Solution

  • Regarding the positioning of your cyliner:

    instead to use the render update at time, you better use a position constraint for your cylinder node to move with the point of view. the result will be the same, as if it were a child of the point of view, but collisions will be detected, because you add it to the main rootnode scenegraph.

    let constraint = SCNReplicatorConstraint(target: pointOfView) // must be a node
    constraint.positionOffset           = positionOffset // some SCNVector3
    constraint.replicatesOrientation    = false
    constraint.replicatesScale          = false
    constraint.replicatesPosition       = true
    
    cylinder.constraints = [constraint]
    

    There is also an influence factor you can configure. By default the influence is 100%, the position will immediatly follow.