How to get SceneKit SCNCameraController frameNodes to set target - ios

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)

Related

SKScene in SCNMaterial's contents appears upside down

I am trying to have an SCNNode with plane geometry, which presents 2d text inside it. I'm doing that through setting SKLabelNode as geometry's material (took that from here)
Here's my code:
let label = SKLabelNode(text: "Test Text 1")
label.fontColor = .white
label.fontName = UIFont.systemFont(ofSize: 32).fontName
label.fontSize = 32
let backgroundRectShape = SKShapeNode(rect: CGRect(origin: .zero, size: label.frame.size))
backgroundRectShape.fillColor = .orange
backgroundRectShape.addChild(label)
label.position = CGPoint(x: backgroundRectShape.frame.width / 2, y: 0)
let skScene = SKScene(size: label.frame.size)
skScene.addChild(backgroundRectShape)
backgroundRectShape.position = .zero
let plane = SCNPlane(width: label.frame.width / 10, height: label.frame.height / 10)
plane.firstMaterial?.diffuse.contents = skScene
let node = SCNNode(geometry: plane)
scene.rootNode.addChildNode(node)
Everything seems fine, but the SKLabelNode appears upside-down:
The SCNNode is surely properly aligned and not rotated, because when I set an image as material content, everything is ok:
I just can't understand why it is happening. The easiest solution would be setting SKLabelNode's yScale to -1, but I don't want to do that without understanding the reason why it appears upside down by default.
plane.firstMaterial?.diffuse.contentsTransform = SCNMatrix4Translate(SCNMatrix4MakeScale(1, -1, 1), 0, 1, 0)
It is seemingly a bug or one of the many limitations of SpriteKit/SceneKit framework.
I suppose SCNMaterial's content erroneously appears upside-down due to the origin of iOS coordinate system.
But instead of scaling the SCNNode itself you should better mirror a texture along Y-axis using contentsTransform instance property of the SCNMaterial. Like so:
material.diffuse.contentsTransform = SCNMatrix4MakeScale(1, -1, 1)
Hope this helps.

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)

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

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
}
}
}

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.

Scene Kit: projectPoint calculated is displaced

I am trying do draw an frame over a sphere on the parent view of the scene, but for some reason the points converted by SCNScene.projectPoint method are displaced.
To make it simple, i created a sphere at the center of the scene with 2 radius. The sphere is attached to the root node, so top left corner on world coordinates is at (-2,-2,0) point.
Here the complete code:
func sphereWithFrame(){
var v1 = SCNVector3(x: 0,y: 0,z: 0)
var v2 = SCNVector3(x: 0,y: 0,z: 0)
let topSphere = SCNSphere(radius: 2.0)
topSphere.firstMaterial!.diffuse.contents = UIColor.greenColor()
let topSphereNode = SCNNode(geometry: topSphere)
topSphereNode.position = SCNVector3Make(0, 0, 0)
scene.rootNode.addChildNode(topSphereNode)
topSphereNode.getBoundingBoxMin(&v1, max: &v2)
//world coordinates
let v1w = topSphereNode.convertPosition(v1, toNode: scene.rootNode)
let v2w = topSphereNode.convertPosition(v2, toNode: scene.rootNode)
//projected coordinates
let v1p = scnview.projectPoint(v1w)
let v2p = scnview.projectPoint(v2w)
//frame rectangle
var rect = CGRectMake(CGFloat(v1p.x), CGFloat(v2p.y), CGFloat(v2p.x - v1p.x), CGFloat(v1p.y - v2p.y))
let rectView = UIView(frame: rect)
rectView.alpha = 0.3
rectView.backgroundColor = UIColor.blueColor()
scnview.addSubview(rectView)
println("v1 \(v1.toString()), v2\(v2.toString())")
println("v1w \(v1w.toString()), v2w\(v2w.toString())")
println("v1p \(v1p.toString()), v2w\(v2p.toString())")
println("rect\(rect)")
}
The console:
v1 x:-2.0,y:-2.0,z:-2.0, v2x:2.0,y:2.0,z:2.0
v1w x:-2.0,y:-2.0,z:-2.0, v2wx:2.0,y:2.0,z:2.0
v1p x:90.718,y:309.282,z:0.925926, v2wx:263.923,y:136.077,z:0.883838
rect(90.718, 136.077, 173.205, 173.205)
The result on screen is a rectangle a little to the up right from where should be.
I try to follow the post "
How do I find my mouse point in a scene using SceneKit?".
What am i getting wrong?
have to set z to zero on both world coordinates:
v1w.z = 0
v2w.z = 0
explaining things help

Resources