I created two nodes: a sphere and a box:
var sphere = SCNNode(geometry: SCNSphere(radius: 0.005))
//I get the box node from scn file
let boxScene = SCNScene(named: "art.scnassets/world.scn")!
var boxNode: SCNNode?
I want two nodes or physicsBody's to interact, so I created a category for categoryBitMask and contactTestBitMask:
struct CollisionCategory: OptionSet {
let rawValue: Int
static let box = CollisionCategory(rawValue: 1)
static let sphere = CollisionCategory(rawValue: 2)
}
Here I set the box node as a physics body:
self.boxScene.rootNode.enumerateChildNodes { (node, _) in
if node.name == "box" {
boxNode = node
let boxBodyShape = SCNPhysicsShape(geometry: SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0.1), options: nil)
let physicsBody = SCNPhysicsBody(type: .static, shape: boxBodyShape)
boxNode!.physicsBody = physicsBody
boxNode!.physicsBody?.categoryBitMask = CollisionCategory.box.rawValue
boxNode!.physicsBody?.contactTestBitMask = CollisionCategory.sphere.rawValue
boxNode!.physicsBody?.collisionBitMask = boxNode!.physicsBody!.contactTestBitMask
}
}
Here I set the sphere node in the render function, which you can move around the view:
func setUpSphere() {
let sphereBodySphere = SCNPhysicsShape(geometry: SCNSphere(radius: 0.005))
let physicsBody = SCNPhysicsBody(type: .kinematic, shape: sphereBodySphere)
sphere.physicsBody = physicsBody
sphere.physicsBody?.categoryBitMask = CollisionCategory.sphere.rawValue
sphere.physicsBody?.contactTestBitMask = CollisionCategory.box.rawValue
sphere.geometry?.firstMaterial?.diffuse.contents = UIColor.blue
sphere.physicsBody?.collisionBitMask = sphere.physicsBody!.contactTestBitMask
previousPoint = currentPosition
}
///It Adds a sphere and changes his position
func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) {
guard let pointOfView = sceneView.pointOfView else { return }
let mat = pointOfView.transform
let dir = SCNVector3(-1 * mat.m31, -1 * mat.m32, -1 * mat.m33)
let currentPosition = pointOfView.position + (dir * 0.185)
if buttonPressed {
if let previousPoint = previousPoint {
sphere.position = currentPosition
sceneView.scene.rootNode.addChildNode(sphere)
}
}
}
I added the protocol SCNPhysicsContactDelegate to the ViewController,
and I set in ViewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
sceneView.scene.physicsWorld.contactDelegate = self
///I correctly see the shapes of the sphere and the box physics bodies using
sceneView.debugOptions = .showPhysicsShapes
createBox()
setUpSphere()
sceneView.scene = boxScene
sceneView.scene.physicsWorld.contactDelegate = self
}
Then I added that function:
func physicsWorld(_ world: SCNPhysicsWorld, didEnd contact: SCNPhysicsContact) {
print("Collision!")
}
This is what happens.
When the two nodes collide nothing happens, so I can't know if the two bodies are touching. Could the problem be about .kinematic, .static or about the function render()?
I followed step by step different tutorials about collisions in ARKit: Tutorial 1, Tutorial 2.
I have no idea why it doesn't work like expected.
Is there something wrong in my code?
Download file code link: https://ufile.io/20sla
willRenderScene is called uptown 60 times a second for every time the scene is going to be rendered. Since you're recreating the physics body every time it's probably messing up the physics engine determine collisions.
Try changing your code to only create the physics body once during setup.
Related
Why this 2 object pass through each other and not interact, what I'm doing wrong?
on each object I applied the property physicsBody which should allow to the physic engine to work.
I add the square object using a tap gesture, creating the anchor with name "base" and in the render add the object base.
for the ball I use a raycast query to position the ball over the plane.
all working fine, only the dynamic looks wrong, they pass each other.
// my square base
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
if let nome = anchor.name, nome == "base" {
let geometry = SCNPlane(width: 1, height: 1)
let material = SCNMaterial()
material.diffuse.contents = UIColor(red: 90/255, green: 200/255, blue: 250/255, alpha: 0.50)
geometry.materials = [material]
// physics
let physSchape = SCNPhysicsShape(geometry: geometry, options: nil)
let planePhysic = SCNPhysicsBody(type: .static, shape: physSchape)
planePhysic.restitution = 0.0
planePhysic.friction = 1.0
let nodo = SCNNode(geometry: geometry)
nodo.eulerAngles.x = -.pi / 2
nodo.physicsBody = planePhysic
node.addChildNode(nodo)
}
}
and this ball:
func addBall(recognizer: UITapGestureRecognizer){
let tapLocation = recognizer.location(in: view)
guard let query = view.raycastQuery(from: tapLocation, allowing: .existingPlaneGeometry, alignment: .horizontal) else {return}
// ottengo posizione real world
guard let translation = view.castRay(for: query).first?.worldTransform.translation else {return}
let x = translation.x
let y = translation.y
let z = translation.z
let ballGeometry = SCNSphere(radius: 0.1)
let mat = SCNMaterial()
mat.diffuse.contents = UIImage(named: "ball")
ballGeometry.materials = [mat]
//Physics
let physSchape = SCNPhysicsShape(geometry: ballGeometry, options: nil)
let ballPhysic = SCNPhysicsBody(type: .dynamic, shape: physSchape)
ballPhysic.mass = 0.2
ballPhysic.friction = 0.8
let nodeBall = SCNNode(geometry: ballGeometry)
nodeBall.position = SCNVector3(x,y+0.3,z)
nodeBall.physicsBody = ballPhysic
view.scene.rootNode.addChildNode(nodeBall)
}
they should interact each other , but actually my ball pass through the base.
If you're trying to simulate two objects' collision, first of all, you must implement the protocol called SCNPhysicsContactDelegate and its delegate:
weak var contactDelegate: SCNPhysicsContactDelegate? { get set }
for using its three optional instance methods:
func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact)
func physicsWorld(_ world: SCNPhysicsWorld, didUpdate contact: SCNPhysicsContact)
func physicsWorld(_ world: SCNPhysicsWorld, didEnd contact: SCNPhysicsContact)
And, of course, category bit mask and collision bit mask are required.
If you need a more detailed info, please look at THIS POST.
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.
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:
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...
***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")
}
Im having trouble with detecting collisions in AR/SCNKit. Ive found this: How to set up SceneKit collision detection, although helpful, I still dont have my code working.
The main goal is to detect when two scene nodes touch, one is a ball, the other is a scnbox acting as a scnplane.
ViewController.swift
class ViewController: UIViewController, ARSCNViewDelegate, SCNPhysicsContactDelegate{
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene()
sceneView.scene = scene
sceneView.scene.physicsWorld.contactDelegate = self
}
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
let width = CGFloat(planeAnchor.extent.x)
let length = CGFloat(planeAnchor.extent.z)
let planeHeight = CGFloat(0.01)
let plane = SCNBox(width: width, height:planeHeight, length:length, chamferRadius: 0)
plane.materials.first?.diffuse.contents = UIColor.orange
let planeNode = SCNNode(geometry: plane)
let x = CGFloat(planeAnchor.center.x)
let y = CGFloat(planeAnchor.center.y)
let z = CGFloat(planeAnchor.center.z)
planeNode.position = SCNVector3(x,y,z)
planeNode.physicsBody = SCNPhysicsBody(type:.kinematic, shape: SCNPhysicsShape(geometry:plane, options:nil))
planeNode.physicsBody?.categoryBitMask = Int(CategoryMask.plane.rawValue)
planeNode.physicsBody?.collisionBitMask = Int(CategoryMask.ball.rawValue) | Int(CategoryMask.plane.rawValue)
}
func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
print("-> didBeginContact")
}
func physicsWorld(_ world: SCNPhysicsWorld, didUpdate contact: SCNPhysicsContact) {
print("-> didUpdateContact")
}
func physicsWorld(_ world: SCNPhysicsWorld, didEnd contact: SCNPhysicsContact) {
print("-> didEndContact")
}
Ball.swift
class Ball {
init(x: Float, y: Float, z:Float){
let sphereNode = SCNNode()
let sphere = SCNSphere(radius: 0.05)
sphereNode.addChildNode(SCNNode(geometry: sphere))
sphereNode.position = SCNVector3(x, y, z)
self.node = sphereNode
self.node.physicsBody = SCNPhysicsBody(type: .dynamic, shape: SCNPhysicsShape(geometry:sphere, options:nil))
self.node.physicsBody?.categoryBitMask = Int(CategoryMask.ball.rawValue)
self.node.physicsBody?.collisionBitMask = Int(CategoryMask.ball.rawValue) | Int(CategoryMask.plane.rawValue)
self.node.physicsBody?.friction = 1
self.node.physicsBody?.mass = 2.0
}
Constants.swift
enum CategoryMask: UInt32 {
case ball = 0b01 // 1
case plane = 0b11 // 2
}
I am expecting physicsWorld to get called when the plane and ball collide but it's not happening. Any insight is much appreciated.
I'm using SceneKit's physicsBody system to detect collisions between objects, and getting some very strange results. To illustrate, I've got a minimal example that produces two spheres with kinematic physicsBodies and moves them in straight lines so that they briefly overlap.
I would expect to see physicsWorld(:didBeginContact:) called exactly once, when the spheres first overlap, and physicsWorld(:didEndContact:) called once when they stop overlapping. Instead, I'm seeing each function called 25 times!
Here's the code to reproduce: In Xcode 8.0, create a brand new project using the "Game" template. Replace the contents of GameViewController.swift with this:
import UIKit
import SceneKit
class GameViewController: UIViewController, SCNSceneRendererDelegate, SCNPhysicsContactDelegate {
var scnScene: SCNScene!
var scnView: SCNView!
var cameraNode: SCNNode!
var nodeA: SCNNode!
var nodeB: SCNNode!
var countBeginnings: Int = 0
var countEndings: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
setupScene()
setupNodes()
}
func setupScene() {
// create a new SCNScene and feed it to the view
scnView = self.view as! SCNView
scnScene = SCNScene()
scnView.scene = scnScene
// assign self as SCNView delegate to get access to render loop
scnView.delegate = self
// assign self as contactDelegate to handle collisions
scnScene.physicsWorld.contactDelegate = self
// create the camera and position it at origin
cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3Zero
scnScene.rootNode.addChildNode(cameraNode)
// tell scnView to update every frame
scnView.isPlaying = true
}
func setupNodes() {
// create two spheres with physicsBodies, one inside the other
nodeA = SCNNode()
nodeA.name = "Node A"
nodeA.geometry = SCNSphere(radius: 1.0)
nodeA.geometry!.firstMaterial?.diffuse.contents = UIColor.yellow.withAlphaComponent(0.6)
// expected behavior
// nodeA.position = SCNVector3(x: 0.0, y: -0.8, z: -10.0)
// weird behavior
nodeA.position = SCNVector3(x: 0.0, y: -0.9, z: -10.0)
nodeA.physicsBody = SCNPhysicsBody(type: .kinematic, shape: SCNPhysicsShape(geometry: nodeA.geometry!, options: nil))
scnScene.rootNode.addChildNode(nodeA)
nodeB = SCNNode()
nodeB.name = "Node B"
nodeB.geometry = SCNSphere(radius: 0.5)
nodeB.geometry!.firstMaterial?.diffuse.contents = UIColor.red
nodeB.position = SCNVector3(x: -2.0, y: 0.0, z: -10.0)
nodeB.physicsBody = SCNPhysicsBody(type: .kinematic, shape: SCNPhysicsShape(geometry: nodeB.geometry!, options: nil))
scnScene.rootNode.addChildNode(nodeB)
// node A can collide with node B but not the other way around
nodeA.physicsBody!.categoryBitMask = 2
nodeB.physicsBody!.categoryBitMask = 1
nodeA.physicsBody!.contactTestBitMask = 1
}
func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
countBeginnings += 1
print("(" + String(countBeginnings) + ") " + contact.nodeA.name! + " began contact with " + contact.nodeB.name!)
}
func physicsWorld(_ world: SCNPhysicsWorld, didEnd contact: SCNPhysicsContact) {
countEndings += 1
print("(" + String(countEndings) + ") " + contact.nodeA.name! + " ended contact with " + contact.nodeB.name!)
}
var frameNumber = 0
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
nodeB.position.x += 0.01
nodeB.position.y -= 0.01
}
}
There's other weirdness going on too. If I change the initial position of one of the spheres just a little bit, moving the y position from -0.9 to -0.8:
nodeA.position = SCNVector3(x: 0.0, y: -0.8, z: -10.0)
Now I get the expected behavior, one call to begin and one call to end! A slightly different collision angle results in totally different behavior.
Could this be a SceneKit bug or is this actually the expected behavior?
The SCNRenderer runs the physics simulation at every frame, calling the renderer(_:didSimulatePhysicsAtTime:) method of its SCNSceneRendererDelegate.
During the course of the crossing of the two spheres, there will be several frames rendered and each time the physics simulation is run, the collision will be detected.
This is expected. It's up to you to process the collision and put the two spheres in a state where they don't collide anymore. For example, in a game, once a projectile hits a player, it disappears. The collision is processed and therefore does not fire anymore.
I used your code in the following way with XCode Version 8.0 beta 5 (8S193k) running as an OS X app. I see the following trace in the console. The begin and end methods are called exactly once !
(1) Node A began contact with Node B
(1) Node A ended contact with Node B
import SceneKit
import QuartzCore
class GameViewController: NSViewController, SCNSceneRendererDelegate, SCNPhysicsContactDelegate {
#IBOutlet weak var gameView: GameView!
// MARK: Properties
var cameraNode: SCNNode!
var nodeA: SCNNode!
var nodeB: SCNNode!
var countBeginnings: Int = 0
var countEndings: Int = 0
// MARK: Initialization
override func awakeFromNib(){
super.awakeFromNib()
// create a new scene
let scene = SCNScene(named: "art.scnassets/scene.scn")!
// create and add a camera to the scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
// place the camera
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
// create and add a light to the scene
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light!.type = SCNLightTypeOmni
lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
scene.rootNode.addChildNode(lightNode)
// create and add an ambient light to the scene
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = SCNLightTypeAmbient
ambientLightNode.light!.color = NSColor.darkGray
scene.rootNode.addChildNode(ambientLightNode)
// set the scene to the view
self.gameView!.scene = scene
// allows the user to manipulate the camera
self.gameView!.allowsCameraControl = true
// show statistics such as fps and timing information
self.gameView!.showsStatistics = true
// configure the view
self.gameView!.backgroundColor = NSColor.black
self.gameView!.delegate = self
setupScene()
setupNodes()
}
func setupScene() {
// assign self as contactDelegate to handle collisions
self.gameView!.scene?.physicsWorld.contactDelegate = self
// create the camera and position it at origin
cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3Zero
self.gameView!.scene?.rootNode.addChildNode(cameraNode)
// tell scnView to update every frame
self.gameView.isPlaying = true
}
func setupNodes() {
// create two spheres with physicsBodies, one inside the other
nodeA = SCNNode()
nodeA.name = "Node A"
nodeA.geometry = SCNSphere(radius: 1.0)
nodeA.geometry!.firstMaterial?.diffuse.contents = NSColor.yellow.withAlphaComponent(0.6)
nodeA.position = SCNVector3(x: 0.0, y: -0.8, z: -10.0)
nodeA.physicsBody = SCNPhysicsBody(type: .kinematic, shape: SCNPhysicsShape(geometry: nodeA.geometry!, options: nil))
self.gameView!.scene?.rootNode.addChildNode(nodeA)
nodeB = SCNNode()
nodeB.name = "Node B"
nodeB.geometry = SCNSphere(radius: 0.5)
nodeB.geometry!.firstMaterial?.diffuse.contents = NSColor.red
nodeB.position = SCNVector3(x: -2.0, y: 0.0, z: -10.0)
nodeB.physicsBody = SCNPhysicsBody(type: .kinematic, shape: SCNPhysicsShape(geometry: nodeB.geometry!, options: nil))
self.gameView!.scene?.rootNode.addChildNode(nodeB)
// node A can collide with node B but not the other way around
nodeA.physicsBody!.categoryBitMask = 2
nodeB.physicsBody!.categoryBitMask = 1
nodeA.physicsBody!.contactTestBitMask = 1
}
// MARK: SCNPhysicsContactDelegate
func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
countBeginnings += 1
print("(" + String(countBeginnings) + ") " + contact.nodeA.name! + " began contact with " + contact.nodeB.name!)
}
func physicsWorld(_ world: SCNPhysicsWorld, didEnd contact: SCNPhysicsContact) {
countEndings += 1
print("(" + String(countEndings) + ") " + contact.nodeA.name! + " ended contact with " + contact.nodeB.name!)
}
// MARK: SCNSceneRendererDelegate
var frameNumber = 0
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
nodeB.position.x += 0.01
nodeB.position.y -= 0.01
}
}
I have a level based game, so I am using the Xcode Scene Editor for level creation, but am having troubles detecting collision between objects, that were created in separate scenes and dragged in game scenes as reference nodes.
It seems that if I try to create the physics body of an object, that is used as a reference node in that .scn file where I created it, that physics body is not being referenced to nodes in the game scene, but the properties that were created directly in Scene Editor are.
If I try and set the physics body of each node that is actually a reference node via code, it does set it (the physics body is not nil), but the collision is not detected.
This is the code, where I set the physics bodies:
// player (I want to be notified when player collides with any walls)
playerNode = level1Scene.rootNode.childNodeWithName("playerNode", recursively: true)!
playerNode.physicsBody = SCNPhysicsBody(type: .Dynamic, shape: nil)
playerNode.physicsBody?.affectedByGravity = false
playerNode.physicsBody?.categoryBitMask = PhysicsCategory.Player.rawValue
playerNode.physicsBody?.collisionBitMask = PhysicsCategory.Wall.rawValue
playerNode.physicsBody?.contactTestBitMask = PhysicsCategory.Wall.rawValue
//wall from separate .scn file
let wallScene = SCNScene(named: "wallObject.scn")
let wall = wallScene!.rootNode.childNodeWithName("wall", recursively: true)!
wall.physicsBody = SCNPhysicsBody(type: .Kinematic, shape: nil)
wall.physicsBody?.categoryBitMask = PhysicsCategory.Wall.rawValue
wall.physicsBody?.collisionBitMask = PhysicsCategory.Player.rawValue
wall.physicsBody?.contactTestBitMask = PhysicsCategory.Player.rawValue
// walls that are reference nodes of the wall and are located in game scene
let wall2 = level1Scene.rootNode.childNodeWithName("wallObject reference", recursively: true)!
wall2.physicsBody = SCNPhysicsBody(type: .Kinematic, shape: nil)
wall2.physicsBody?.categoryBitMask = PhysicsCategory.Wall.rawValue
wall2.physicsBody?.collisionBitMask = PhysicsCategory.Player.rawValue
wall2.physicsBody?.contactTestBitMask = PhysicsCategory.Player.rawValue
print(wall2.physicsBody!) // prints <SCNPhysicsBody: 0x7fe5f9dc75a0>
I am not contacted about any contacts, nor does the player physically collide with any wall on contact. I have conformed the class to the SCNPhysicsContactDelegate protocol:
extension GameViewController: SCNPhysicsContactDelegate {
func physicsWorld(world: SCNPhysicsWorld, didBeginContact contact: SCNPhysicsContact) {
playerNode.physicsBody?.velocity = SCNVector3Zero
print("player and wall collided")
}
}
and set the contact delegate to my game scene:
level1Scene.physicsWorld.contactDelegate = self
I have been trying to get this to work for two days now, with no success. When I was working with Sprite Kit, I could almost always find relevant posts about my problem and even when posting my question, I would get an answer. But Scene Kit is not as popular I guess and I can't find anything.
If you do know where I am wrong or how the collision could be detected in code, please tell me.
I cobbled together an example loosely based on the code you posted up, the only 'problem' is that it worked, in that the contact delegate is successfully called. The player geometry in this case is the SceneKit spaceship, and the wall2 is a SCNBox added into the other scene then dragged into the ship scene from Finder. I've included gravity, so the ship just falls onto the wall.
Do the objects bounce off each other, and it's just that your contact delegate isn't called? Or do the objects pass through each other. I'm also wondering how you move the player object to cause the contact?
import UIKit
import QuartzCore
import SceneKit
let colors = [UIColor.redColor(), UIColor.brownColor(), UIColor.cyanColor(), UIColor.greenColor(), UIColor.yellowColor(), UIColor.blueColor()]
enum PhysicsCategory:Int {
case Player = 2
case Wall = 4
}
extension GameViewController: SCNPhysicsContactDelegate {
func physicsWorld(world: SCNPhysicsWorld, didBeginContact contact: SCNPhysicsContact) {
playerNode.geometry?.firstMaterial?.diffuse.contents = colors[Int(arc4random_uniform(UInt32(colors.count)))]
print("player and wall collided")
}
}
class GameViewController: UIViewController {
var playerNode:SCNNode!
override func viewDidLoad() {
super.viewDidLoad()
// create a new scene
//let scene = SCNScene()
let scene = SCNScene(named: "art.scnassets/ship.scn")!
//playerNode = SCNNode(geometry: SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0))
playerNode = scene.rootNode.childNodeWithName("ship", recursively: true)!
playerNode.geometry?.firstMaterial?.diffuse.contents = UIColor.redColor()
playerNode.physicsBody = SCNPhysicsBody(type: .Dynamic, shape: nil)
playerNode.physicsBody?.affectedByGravity = true
playerNode.physicsBody?.categoryBitMask = PhysicsCategory.Player.rawValue
playerNode.physicsBody?.collisionBitMask = PhysicsCategory.Wall.rawValue
playerNode.physicsBody?.contactTestBitMask = PhysicsCategory.Wall.rawValue
initPlayer()
//scene.rootNode.addChildNode(playerNode)
//let wall = SCNNode(geometry: SCNBox(width: 20, height: 0.25, length: 20, chamferRadius: 0))
let wallScene = SCNScene(named: "art.scnassets/wallObject.scn")
let wall = wallScene!.rootNode.childNodeWithName("wall", recursively: true)!
wall.physicsBody = SCNPhysicsBody(type: .Kinematic, shape: nil)
wall.physicsBody?.categoryBitMask = PhysicsCategory.Wall.rawValue
wall.physicsBody?.collisionBitMask = PhysicsCategory.Player.rawValue
wall.physicsBody?.contactTestBitMask = PhysicsCategory.Player.rawValue
//scene.rootNode.addChildNode(wall)
let wall2 = scene.rootNode.childNodeWithName("wallObject reference", recursively: true)!
wall2.physicsBody = SCNPhysicsBody(type: .Kinematic, shape: nil)
wall2.physicsBody?.categoryBitMask = PhysicsCategory.Wall.rawValue
wall2.physicsBody?.collisionBitMask = PhysicsCategory.Player.rawValue
wall2.physicsBody?.contactTestBitMask = PhysicsCategory.Player.rawValue
scene.physicsWorld.contactDelegate = self
// retrieve the SCNView
let scnView = self.view as! SCNView
scnView.playing = true
scnView.scene = scene
scnView.allowsCameraControl = true
scnView.autoenablesDefaultLighting = true
scnView.showsStatistics = true
scnView.backgroundColor = UIColor.lightGrayColor()
// add a tap gesture recognizer
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
scnView.addGestureRecognizer(tapGesture)
}
func handleTap(gestureRecognize: UIGestureRecognizer) {
initPlayer()
}
func initPlayer() {
playerNode.position = SCNVector3Make(0, 12, 0)
playerNode.eulerAngles = SCNVector3Make(Float(drand48() * M_PI/2), Float(drand48() * M_PI/2), Float(drand48() * M_PI/2))
playerNode.physicsBody?.velocity = SCNVector3Zero
playerNode.physicsBody?.angularVelocity = SCNVector4Zero
}
}