SceneKit: slow loading of UIView after contact between two nodes - ios

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.

Related

How to animate the background color of a scene view (SCNView). Note, not a scene (SCNScene), or conventional UIView

Is it possible to animate the .backgroundColor property of an SCNView?
Please note, it is easy to animate the background of an actual scene (SCNScene) and I know how to do that. It is also easy to animate the background of a conventional UIView.
I've not been able to figure out how to animate the .backgroundColor property of an SCNView.
Assuming you take the default SceneKit Game Template (the one with the rotating Jet) I got it working by doing this:
Here is my viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
// create a new scene
let scene = SCNScene() // SCNScene(named: "art.scnassets/ship.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 = .omni
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 = .ambient
ambientLightNode.light!.color = UIColor.darkGray
scene.rootNode.addChildNode(ambientLightNode)
// retrieve the ship node
// let ship = scene.rootNode.childNode(withName: "ship", recursively: true)!
// animate the 3d object
// ship.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 2, z: 0, duration: 1)))
// 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 initial background color of the SCNView
scnView.backgroundColor = UIColor.red
// Setup a SCNAction that rotates i.Ex the HUE Value of the Background
let animColor = SCNAction.customAction(duration: 10.0) { _ , timeElapsed in
scnView.backgroundColor = UIColor.init(hue: timeElapsed/10, saturation: 1.0, brightness: 1.0, alpha: 1.0)
}
// Run the Action (here using the rootNode)
scene.rootNode.runAction(animColor)
// add a tap gesture recognizer
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
scnView.addGestureRecognizer(tapGesture)
}
This might not be the best solution, but using a SCNTransaction I had no luck. Hope I could help in some way.
Just an addendum to the amazing & correct answer of #ZAY.
You have to do a frame-by-frame animation of the color and,
For some reason,
You do the action on the scene view
But. You run the action on the root node of the scene.
So, it's a miracle.
Works perfectly.

I have added a Human 3d Model in Scene Kit , its background is black how to make it as white as front view?

I have added a Human 3d Model in Scene Kit. Its background is black how to make it as white as front view? I have used this in swift app, used scene kit and human 3d model, Please check image I have attached..
Back View
3d Model Settings
Code :-
//MARK: - Scene Related Methods
func loadScene() {
self.removeExistingNodes()
loadSceneLayer(fileName: "FinalBaseMesh.obj")
sceneView.allowsCameraControl = true
sceneView.autoenablesDefaultLighting = false
load3DScene()
//layerSelectionIndex = 0
sceneView.scene = scene
}
func load3DScene() {
sceneView.scene = scene
// Allow user to manipulate camera
sceneView.allowsCameraControl = true
sceneView.backgroundColor = UIColor.white
sceneView.cameraControlConfiguration.allowsTranslation = true
sceneView.cameraControlConfiguration.panSensitivity = 0.9
sceneView.delegate = self as SCNSceneRendererDelegate
// sceneView.isPlaying = true
for reco in sceneView.gestureRecognizers! {
if let panReco = reco as? UIPanGestureRecognizer {
panReco.maximumNumberOfTouches = 1
}
}
// add a tap gesture recognizer
let tapGesture = UITapGestureRecognizer(target: self, action:#selector(handleTap(_:)))
sceneView.addGestureRecognizer(tapGesture)
self.addSavedNode()
}
func loadSceneLayer(fileName: String) {
scene = SCNScene(named: fileName) ?? SCNScene()
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 6.5, z: 20)
scene.rootNode.addChildNode(cameraNode)
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light?.type = .omni
lightNode.position = SCNVector3(x: 0, y: 6.5, z: 20)
scene.rootNode.addChildNode(lightNode)
// 6: Creating and adding ambien light to scene
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light?.type = .ambient
ambientLightNode.light?.color = UIColor.darkGray
scene.rootNode.addChildNode(ambientLightNode)
}
I forgot to add last method please check loadstonelayer method
Remove all the camera control code
See How to rotate object in a scene with pan gesture - SceneKit for rotating an object
Put in a light node at a negative z facing the object

Using Scenekit sceneTime to scrub through animations iOS

I'm trying to modify Xcode's default game setup so that I can: program an animation into the geometry, scrub through that animation, and let the user playback the animation automatically.
I managed to get the scrubbing of the animation to work by setting the view's scene time based on the value of a scrubber. However, when I set the isPlaying boolean on the SCNSceneRenderer to true, it resets the time to 0 on every frame, and I can't get it to move off the first frame.
From the docs, I'm assuming this means it won't detect my animation and thinks the duration of all animations is 0.
Here's my viewDidLoad function in my GameViewController:
override func viewDidLoad() {
super.viewDidLoad()
// create a new scene
let scene = SCNScene(named: "art.scnassets/ship.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 = .omni
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 = .ambient
ambientLightNode.light!.color = UIColor.darkGray
scene.rootNode.addChildNode(ambientLightNode)
// retrieve the ship node
let ship = scene.rootNode.childNode(withName: "ship", recursively: true)!
// define the animation
//ship.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 2, z: 0, duration: 1)))
let positionAnimation = CAKeyframeAnimation(keyPath: "position.y")
positionAnimation.values = [0, 2, -2, 0]
positionAnimation.keyTimes = [0, 1, 3, 4]
positionAnimation.duration = 5
positionAnimation.usesSceneTimeBase = true
// retrieve the SCNView
let scnView = self.view as! SCNView
scnView.delegate = self
// add the animation
ship.addAnimation(positionAnimation, forKey: "position.y")
// 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.black
// add a tap gesture recognizer
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
scnView.addGestureRecognizer(tapGesture)
// play the scene
scnView.isPlaying = true
//scnView.loops = true
}
Any help is appreciated! :)
References:
sceneTime:
https://developer.apple.com/documentation/scenekit/scnscenerenderer/1522680-scenetime
isPlaying:
https://developer.apple.com/documentation/scenekit/scnscenerenderer/1523401-isplaying
related question:
SceneKit SCNSceneRendererDelegate - renderer function not called
I couldn't get it to work in an elegant way, but I fixed it by adding this Timer call:
Timer.scheduledTimer(timeInterval: timeIncrement, target: self, selector: (#selector(updateTimer)), userInfo: nil, repeats: true)
timeIncrement is a Double set to 0.01, and updateTimer is the following function:
// helper function updateTimer
#objc func updateTimer() {
let scnView = self.view.subviews[0] as! SCNView
scnView.sceneTime += Double(timeIncrement)
}
I'm sure there's a better solution, but this works.
sceneTime is automatically set to 0.0 after actions and animations are run on every frame.
Use can use renderer(_:updateAtTime:) delegate method to set sceneTime to the needed value before SceneKit runs actions and animations.
Make GameViewController comply to SCNSceneRendererDelegate:
class GameViewController: UIViewController, SCNSceneRendererDelegate {
// ...
}
Make sure you keep scnView.delegate = self inside viewDidLoad().
Now implement renderer(_:updateAtTime:) inside your GameViewController class:
// need to remember scene start time in order to calculate current scene time
var sceneStartTime: TimeInterval? = nil
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
// if startTime is nil assign time to it
sceneStartTime = sceneStartTime ?? time
// make scene time equal to the current time
let scnView = self.view as! SCNView
scnView.sceneTime = time - sceneStartTime!
}

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)

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

Resources