Use case of convexSweepTest(with:from:to:options) - ios

The Apple Documentation gave a seemingly straightforward example of how to use the convexSweepTest in Objective C. Unfortunately the example code in the documentation does not (as of now) exist in Swift.
I can move it over to Swift and compile without error, but I cannot get the 'contacts.count' to ever be anything other than zero no matter how many objects (all with a physicsBody) I add to the scene, including the one I'm doing the convexSweepTest with.
I have a layer of static objects with a physicsBody along the x-z axis, with my object I'm doing the convexSweepTest with a positive y value.
For some reason I'm required to add a custom physicsShape in the SCNPhysicsBody(type:shape) constructor (opposed to leaving it as nil as described in the documentation should work) in order to get the convexSweepTest to recognize the physicsShape so it will compile.
Here's a snippet of the code that compiles without compile error but does not work:
let physicsShape = SCNPhysicsShape(node: selectedBlock, options: nil)
selectedBlock.physicsBody = SCNPhysicsBody(type: .dynamic, shape: physicsShape) // why can't I use nil? who knows
// note: 'selectedBlock' has a positive y value ... and there are objects that can be collided with at every y=0 point
let current = selectedBlock.transform
let downBelow = SCNMatrix4Translate(current, 0, -selectedBlock.position.y, 0)
let physicsWorld = SCNPhysicsWorld()
let physicsContacts = physicsWorld.convexSweepTest(with: (selectedBlock.physicsBody?.physicsShape)!, from: current, to: downBelow, options: nil)
print("count \(physicsContacts.count)") // ALWAYS prints zero
I'm looking for a working use case example in Swift with the convexSweepTest method.

You can't create SCNPhysicsWorld yourself, but use the one in the current scene (scene.physicsWorld).

Old post but while I was playing with convexSweepTest I found out that if the collisionBitMask of the other physicbody doesn't have the first bit set it won't return the contact, which makes no sense as the convexSweepTest doesn't have a categorymask or it's defaulted to 1 without saying?
Run the code below in playground
First static node, green, is with a physic body, with its category NOT matching the collisionBitMask of the convexSweep option
Second static node, orange, is with a physic body, with its category matching the collisionBitMask of the convexSweep option
One node is in movement with no physic body, just to visually show where is the shape used in the convexSweep as I'm using the coordinate of the moving node
I move the 'moving' node that represent the convexSweep shape across the 2 static shapes, if it passes through both without contact I set the first bit of the collision mask of the 2nd node and rerun the movement, if it does contact I reset to what the values should be, runs for ever so that you can see the convexSweep in action. You'll see that the convexSweep honor it's collisionBitMask option as only the 2nd node gets a contact (only if the contact node has the first bit).
import SceneKit
import SpriteKit
import PlaygroundSupport
import simd
let sceneView = SCNView(frame: CGRect(x: 0, y: 0, width: 800, height: 600))
PlaygroundPage.current.liveView = sceneView
var scene = SCNScene()
sceneView.scene = scene
sceneView.backgroundColor = SKColor.lightGray
sceneView.debugOptions = .showPhysicsShapes
sceneView.allowsCameraControl = true
sceneView.autoenablesDefaultLighting = true
// a camera
var cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 5, z: 5)
scene.rootNode.addChildNode(cameraNode)
// setup a plane just for contrast and look at
let planeNode = SCNNode(geometry: SCNPlane(width: 10, height: 10))
planeNode.geometry?.firstMaterial?.diffuse.contents = UIColor.blue
planeNode.position = SCNVector3(0, -0.5, 0)
planeNode.rotation = SCNVector4Make(1, 0, 0, -.pi/2);
scene.rootNode.addChildNode(planeNode)
// add cmaera constraint to look at the center of the plane
let centerConstraint = SCNLookAtConstraint(target: planeNode)
cameraNode.constraints = [centerConstraint]
let BitMaskContact = 0x0010
let BitMaskMoving = 0x0100 // To show that this needs to be with first bit set to 1
let BitMaskPassThrough = 0x1000
// Add a box to be a contact body
var passthroughNode = SCNNode(geometry: SCNBox(width: 0.5, height: 1.0, length: 1.0, chamferRadius: 5))
passthroughNode.geometry?.firstMaterial?.diffuse.contents = SKColor.green
passthroughNode.name = "passthroughNode static"
passthroughNode.position = SCNVector3(x: 0, y: 0.5, z: 0)
passthroughNode.physicsBody = SCNPhysicsBody(type: .static, shape: nil)
passthroughNode.physicsBody?.categoryBitMask = BitMaskPassThrough
passthroughNode.physicsBody?.collisionBitMask = BitMaskMoving
scene.rootNode.addChildNode(passthroughNode)
// Add a box to be a contact body
var contactNode = SCNNode(geometry: SCNBox(width: 0.5, height: 1.0, length: 1.0, chamferRadius: 5))
contactNode.geometry?.firstMaterial?.diffuse.contents = SKColor.orange
contactNode.name = "contactNode static"
contactNode.position = SCNVector3(x: 1, y: 0.5, z: 0)
contactNode.physicsBody = SCNPhysicsBody(type: .static, shape: nil)
contactNode.physicsBody?.categoryBitMask = BitMaskContact
contactNode.physicsBody?.collisionBitMask = BitMaskMoving
scene.rootNode.addChildNode(contactNode)
// Add a box just to visually see the shape to be used with convexSweepTest
let sweepOriginalPosition = SCNVector3(x: -1, y: 0.5, z: 0)
let sweepGeo = SCNBox(width: 1, height: 0.5, length: 0.5, chamferRadius: 5)
let sweepNode = SCNNode(geometry: sweepGeo)
sweepNode.name = "sweepNode moving"
sweepNode.position = sweepOriginalPosition
let sweepShape = SCNPhysicsShape(geometry: sweepGeo, options: nil)
scene.rootNode.addChildNode(sweepNode)
//let rendererDelegate = RendererDelegate( sweepNode, physicsWorld:scene.physicsWorld)
//sceneView.delegate = rendererDelegate
//scene.physicsWorld.contactDelegate = rendererDelegate
let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
let start = sweepNode.simdWorldPosition
let velocityX:Float = 0.1
// Where the shape stands now
var from = matrix_identity_float4x4
from.position = start
// Where the shape should move to
var to: matrix_float4x4 = matrix_identity_float4x4
to.position = start + SIMD3<Float>(velocityX, 0, 0)
let options: [SCNPhysicsWorld.TestOption: Any] = [
SCNPhysicsWorld.TestOption.collisionBitMask: BitMaskContact
]
let contacts = scene.physicsWorld.convexSweepTest(
with: sweepShape,
from: SCNMatrix4(from),
to: SCNMatrix4(to),
options: options)
if !contacts.isEmpty {
print( "contact found. reseting contact node collision to normal")
contactNode.physicsBody?.collisionBitMask = BitMaskMoving // 4 <-- set it back to what it should be
passthroughNode.physicsBody?.collisionBitMask = BitMaskPassThrough // 8 <-- set it back to what it should be
sweepNode.position = sweepOriginalPosition
} else {
sweepNode.position.x = sweepNode.position.x + velocityX
if sweepNode.position.x > 2 {
print( "contact missed. reseting contact node collision to | x0001")
contactNode.physicsBody?.collisionBitMask = BitMaskMoving | 0x0001 // 5 <-- have the first bit to 1
passthroughNode.physicsBody?.collisionBitMask = BitMaskPassThrough | 0x0001 // 9 <-- have the first bit to 1, but with convexSweep opion set it won't matter
sweepNode.position = sweepOriginalPosition
}
}
}

Related

SceneKit SCNNode SNCAction.fadeIn not behaving as expected, it simply pops in

Im playing with SceneKit to get familiarised with it. Im trying to do fade in some nodes and move them down. Now all seems to work, except the fadein animation.
Here is the code
let arrowShape = SCNPlane(width: 1, height: 0.3)
let arrowMaterial = SCNMaterial()
arrowMaterial.lightingModel = .constant
arrowMaterial.isDoubleSided = false
arrowMaterial.transparencyMode = .singleLayer
arrowMaterial.diffuse.contents = UIImage(named:"unnamed.png")
arrowMaterial.blendMode = .add
arrowShape.materials = [arrowMaterial]
let arrowNode = SCNNode(geometry: arrowShape)
arrowNode.opacity = 0.0
arrowNode.pivot = SCNMatrix4MakeTranslation(0, 0.2, 0)
arrowNode.position = SCNVector3(0, 1, 0.1)
planeNode.addChildNode(arrowNode)
let apearAction = SCNAction.fadeIn(duration: 0.1)
let action = SCNAction.moveBy(x: 0, y: -1, z: 0, duration: 1)
let group = SCNAction.group([apearAction,action])
let removeAction = SCNAction.removeFromParentNode()
let sequence = SCNAction.sequence([group, removeAction])
arrowNode.runAction(sequence)
But for some reason the arrow simply appears and moves down. I have tried just adding the fadein without movement and same results. I believe it has to do with the materials but Im not sure.
I though this would be as easy as UIView.animate... methods.. :)
Thanks in advance, and forgive me if I asked some dummy question.

What is causing SCNNodes not to touch each others when using physics with ARKit?

I'm trying to place objects on top of each others, but for some reason the physics are not working as they should. When I drop items on top of other items there's always some space which remains between the items. What is that space and how can I get rid of it?
As you can see in the picture below, there're three objects stacked on top of each others. This is what always happens when I place them on top of each others.
This is how I create the plane and add physics to it:
private func createPlaneWithPhysics(planeCenter pos: vector_float3) -> SCNNode {
let planeMaterial = SCNMaterial()
planeMaterial.diffuse.contents = UIColor.white.withAlphaComponent(0)
let planeGeometry = SCNBox(width: defaultPlaneWidth, height: 0.01, length: defaultPlaneHeight, chamferRadius: 0)
planeGeometry.materials = [planeMaterial]
let planeNode = SCNNode(geometry: planeGeometry)
planeNode.position = SCNVector3(pos.x, pos.y - DimensionCorrection.yAxisError, pos.z)
let shape = SCNPhysicsShape(geometry: planeGeometry, options: nil)
let physicsBody = SCNPhysicsBody(type: .kinematic, shape: shape)
planeNode.physicsBody = physicsBody
planeNode.physicsBody?.categoryBitMask = Int(SCNPhysicsCollisionCategory.static.rawValue)
planeNode.physicsBody?.contactTestBitMask = Int(SCNPhysicsCollisionCategory.default.rawValue)
return planeNode
}
And in the VirtualContainer I add physics for the VirtualItem(.physicsBody) in the following way:
private func physicsForvirtualItem() -> SCNPhysicsBody {
let shape = SCNPhysicsShape(node: virtualItem, options: [SCNPhysicsShape.Option.type : SCNPhysicsShape.ShapeType.boundingBox])
let physics = SCNPhysicsBody(type: .dynamic, shape: shape)
physics.mass = virtualItem.mass
physics.isAffectedByGravity = true
physics.velocityFactor = SCNVector3(0, 1, 0)
physics.angularVelocityFactor = SCNVector3(0, 1, 0)
physics.categoryBitMask = Int(SCNPhysicsCollisionCategory.default.rawValue)
return physics
}
The physicShape wasn’t moving when I used node: instead of geometry: in the physicsShape method... see my approach below - set a primitive geometry to the physicShape node.
let shape = SCNPhysicsShape(node: virtualItem, options: [SCNPhysicsShape.Option.type : SCNPhysicsShape.ShapeType.convexHull])
When you import geometry created in external 3D authoring packages, the default pivots & bounding boxes can be very unpredictable. To troubleshoot you might just create a simple box geometry and apply it to your imported or irregular geometry.
let box = SCNBox(width: 0.05, height: 0.05, length: 0.05, chamferRadius: 0)
let shape = SCNPhysicsShape(geometry: box, options: [SCNPhysicsShape.Option.type : SCNPhysicsShape.ShapeType.convexHull])
let physics = SCNPhysicsBody(type: .dynamic, shape: shape)
I think it might have something to do with using node instead of geometry
if the box is not centered on the geometry the pivot needs to be centred using a centerPivot function like...
func centerPivot(for node: SCNNode) {
var min = SCNVector3Zero
var max = SCNVector3Zero
node.__getBoundingBoxMin(&min, max: &max)
node.pivot = SCNMatrix4MakeTranslation(
min.x + (max.x - min.x)/2,
min.y + (max.y - min.y)/2,
min.z + (max.z - min.z)/2
)
}
to use just
centerPivot(for: node!)
Using nil or the standard shapes for the collision geometry is designed for speed -- not for correctness. Just use the actual geometry of the SCNNode instead of one of the standard shapes. Today's devices are fast so you are probably fine doing this in many cases.
Something like this, assuming you have your SCNNode already set up with its associated geometry, either from code, or by importing a .dae or .scn file:
let node = SCNNode()
// (add geometry via code or init node from scene loaded from file)
let pShape = SCNPhysicsShape(geometry: node.geometry)
let pBody = SCNPhysicsBody(type: .dynamic, shape: pShape)
// (add any other physicsBody setup stuff)
node.physicsBody = pBody

How to get SceneKit SCNCameraController frameNodes to set target

The new SCNCameraController in iOS 11 looks very useful, but unfortunately there doesn't appear to be any documentation other than the header file and a short description in the WWDC2017 video. The frameNodes function looks particularly handy, but it doesn't seem to actually work or at least not reliably. Specifically the header comment for frameNodes says:
"Move the camera to a position where the bounding sphere of all nodes is fully visible. Also set the camera target has the center of the bounding sphere."
For an array of nodes with more than one entry, it usually adjusts the camera so they are all visible, but it almost never seems to set the camera controller target to the bounding sphere of the nodes. At least I have not been able to get it to work reliably. Has anyone figured out how to get it to work (I'm using iOS 11.2.1 and Xcode 9.2)?
I've enclosed a playground to demo the problem. If I don't set the cameraController.target manually, the camera rotates around the torus, i.e., the target appears to be set to SCNVector(0,0,0). If I set the target manually then the camera seems to rotate around the correct target, roughly between the torus and the cube. If this is just a bug that I can work around, can anyone suggest a (straightforward?) way to compute the bounding volume for an array of nodes?
import UIKit
import SceneKit
import PlaygroundSupport
var sceneView = SCNView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
var scene = SCNScene()
sceneView.scene = scene
PlaygroundPage.current.liveView = sceneView
sceneView.autoenablesDefaultLighting = true
var cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 10)
scene.rootNode.addChildNode(cameraNode)
sceneView.allowsCameraControl = true
sceneView.defaultCameraController.interactionMode = .orbitTurntable
sceneView.defaultCameraController.pointOfView = sceneView.pointOfView
var torus = SCNTorus(ringRadius: 1, pipeRadius: 0.5)
var torusNode = SCNNode(geometry: torus)
torusNode.position = SCNVector3(0, 0, 0)
scene.rootNode.addChildNode(torusNode)
torus.firstMaterial?.diffuse.contents = UIColor.red
torus.firstMaterial?.specular.contents = UIColor.white
var cube = SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0)
var cubeNode = SCNNode(geometry: cube)
cubeNode.position = SCNVector3(4, 0, 0)
scene.rootNode.addChildNode(cubeNode)
cube.firstMaterial?.diffuse.contents = UIColor.blue
cube.firstMaterial?.specular.contents = UIColor.white
SCNTransaction.begin()
SCNTransaction.animationDuration = 1.0
sceneView.defaultCameraController.frameNodes([torusNode, cubeNode])
//sceneView.defaultCameraController.target = SCNVector3(2, 0, 0)
SCNTransaction.commit()
the bounding volume of an array of nodes could be shown by creating a sphere of radius equal to the largest x,y or z value (float) in the x,y,z arrays.
pseudo-code below:
maxval = 0
for i in x[] {
if i > maxval {
maxval = i
}
}
for i in y[] {
if i > maxval {
maxval = i
}
}
for i in z[] {
if i > maxval {
maxval = i
}
}
let a = SCNSPhere(radius: maxval)

SceneKit - adding SCNNode on another SCNNode

I'm trying to put sphere on top of the box, but the position is strange: Ball is half hidden inside box. I tried to change box and sphere pivot, but it didn't help. Here's the code:
let cubeGeometry = SCNBox(width: 10, height: 10, length: 10,
chamferRadius: 0)
let cubeNode = SCNNode(geometry: cubeGeometry)
//cubeNode.pivot = SCNMatrix4MakeTranslation(0, 1, 0)
scene.rootNode.addChildNode(cubeNode)
let ballGeometry = SCNSphere(radius: 1)
let ballNode = SCNNode(geometry: ballGeometry)
ballNode.pivot = SCNMatrix4MakeTranslation(0.5, 0, 0.5)
ballNode.position = SCNVector3Make(0, 5, 0)
cubeNode.addChildNode(ballNode)`
and the result:
what I'm doing wrong? How to put the ball right on the top side of the box?
UPDATE: If I add Cube instead of ball, it looks good as I want
You need to translate on the Y-axis by cube-height/2 + sphere-radius. Thus you should have:
ballNode.position = SCNVector3Make(0, 6, 0)
Here is the screenshot:
The relevant complete code:
override func viewDidLoad() {
super.viewDidLoad()
// create a new scene
let scene = SCNScene()
let cubeGeometry = SCNBox(width: 10, height: 10, length: 10,
chamferRadius: 0)
cubeGeometry.firstMaterial?.diffuse.contents = UIColor.yellow
let cubeNode = SCNNode(geometry: cubeGeometry)
scene.rootNode.addChildNode(cubeNode)
let ballGeometry = SCNSphere(radius: 1)
ballGeometry.firstMaterial?.diffuse.contents = UIColor.green
let ballNode = SCNNode(geometry: ballGeometry)
ballNode.position = SCNVector3Make(0, 6, 0)
cubeNode.addChildNode(ballNode)
// retrieve the SCNView
let scnView = self.view as! SCNView
// set the scene to the view
scnView.scene = scene
// allows the user to manipulate the camera
scnView.allowsCameraControl = true
// show statistics such as fps and timing information
scnView.showsStatistics = true
// configure the view
scnView.backgroundColor = UIColor.gray
}
Update : Why radius and not radius / 2
See this screenshot from the Scene Editor in Xcode. The original position of cube is (0, 0, 0) and so is that of the ball; hence the ball needs to be moved by r and not r / 2; with r / 2 the lower section of the ball with height r/2 will be still inside the cube. You can just add a cube and sphere as in the scene below in the editor and that should help clarify.
Everything is running normally.
You added the ball node to the cube node. So the origin of the ball node is on the center of the cube.
Then you change the position of the ball to half the size of the cube on the y axis. So basically it pops off and you see just the half of the ball (its radius is 1).
So you have to add again half the size of the ball to put it just on the top of the cube :
ballNode.position = SCNVector3Make(0, 5.5, 0)

SceneKit: slow loading of UIView after contact between two nodes

I just found a really odd behavior with Scene Kit. I created a clean project to see if this would happen, and it did. Consider the following GameViewController class:
import UIKit
import QuartzCore
import SceneKit
class GameViewController: UIViewController, SCNPhysicsContactDelegate {
func physicsWorld(world: SCNPhysicsWorld, didBeginContact contact: SCNPhysicsContact) {
let label = UILabel(frame: CGRectMake(0, 0, 100, 100))
label.textColor = .whiteColor()
label.text = "Test"
self.view.addSubview(label)
}
override func viewDidLoad() {
// Controller
super.viewDidLoad()
// Scene
let scene = SCNScene()
scene.physicsWorld.contactDelegate = self
// Scene View
let scnView = self.view as! SCNView
scnView.scene = scene
// Camera
let camera = SCNCamera()
camera.usesOrthographicProjection = true
camera.orthographicScale = 8
// Camera Node
let cameraNode = SCNNode()
cameraNode.camera = camera
cameraNode.position = SCNVector3(x: -3, y: 7.5, z: 10)
cameraNode.eulerAngles = SCNVector3(x: -0.5, y: -0.5, z: 0)
scene.rootNode.addChildNode(cameraNode)
// Light
let light = SCNLight()
light.type = SCNLightTypeDirectional
// Light Node
let lightNode = SCNNode()
lightNode.light = light
lightNode.eulerAngles = SCNVector3Make(-1, -0.5, 0)
scene.rootNode.addChildNode(lightNode)
// Box Shape
let geometry = SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0)
geometry.materials.first?.diffuse.contents = UIColor(red: 255, green: 255, blue: 0, alpha: 1)
// Upper Box
let box = SCNNode(geometry: geometry)
box.physicsBody = SCNPhysicsBody(type: .Dynamic, shape: nil)
box.physicsBody?.categoryBitMask = 1
box.physicsBody?.contactTestBitMask = 2
scene.rootNode.addChildNode(box)
// Bottom Box
let box2 = SCNNode(geometry: geometry)
box2.position.y -= 5
box2.physicsBody = SCNPhysicsBody(type: .Static, shape: nil)
box2.physicsBody?.categoryBitMask = 2
box2.physicsBody?.contactTestBitMask = 1
box2.physicsBody?.affectedByGravity = false
scene.rootNode.addChildNode(box2)
}
}
What happens here is, we create a SCNScene, then we add the camera/lightning and create two boxes that will collide. We also set the controller as a delegate for the physics contact delegate.
But the important code is at the top, when the contact between the two objects occurs. We create an UILabel and position it at the top left of the screen. But here is the problem: It takes around seven seconds for the label to display. And it seems to always be this time. This seems rather odd, but you can try it yourself: just create a SceneKit project (using Swift), and replace the controller code with the one I provided.
Additional note: if you add print("a") in the physicsWorld() function, it will run immediately, so the code is being run at the right time, but for some reason the UILabel isn't displaying right away.
At first I thought your scene wasn't being rendered after the contact; hence not displaying the label, so put a scnView.playing = true line in. However, this resulted in the label never being displayed.
Next thought was that you're trying to do something on a thread you shouldn't be. Modifying UIKit views should really only be done on the main thread, and it's not clear what thread calls the SCNPhysicsContactDelegate functions. Given it's relatively easy to ensure it does run on the main thread...
func physicsWorld(world: SCNPhysicsWorld, didBeginContact contact: SCNPhysicsContact) {
dispatch_async(dispatch_get_main_queue()) {
let label = UILabel(frame: CGRectMake(0, 0, 100, 100))
label.textColor = .whiteColor()
label.text = "Test"
self.view.addSubview(label)
}
}
This fixes the delay for me, and also works with scnView.playing = true.

Resources