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.
Related
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)
I'm working on an ARKit project in swift.
In SWIFT I add objects to the scene. What I want to do with those objects is show a shadow beneath them to make them more realistic. I tried some things and those all didn't work. I'll explain that here.
Please understand that I'm still new to swift. I'm creating objects programmatically in SWIFT. I thought I could do it by creating a invisible plane underneath all the objects and place a light above the scene. I learned that I can cast a shadow on an invisible plane in the Scene editor from Xcode by unchecking the "write to color" values red, green, blue and alpha. I placed a spot light above it and it worked. Now I want to do this programatically.
In swift I created the light's and the plane as shown below. I don't use a spot anymore because the scene is so large. That's why I create a really large plane. I added those to light's ambient and directional. Ambient so it doesn't look black on the side and directional so the shadow is shown. This doesn't. The objects look weirdly lit and there are no shadows.
let worldGroundPlaneGeometry = SCNPlane(width: 1000, height: 1000)
worldGroundPlaneGeometry.firstMaterial?.colorBufferWriteMask = SCNColorMask(rawValue: 0)
let worldGroundPlane = SCNNode()
worldGroundPlane.geometry = worldGroundPlaneGeometry
worldGroundPlane.position = worldPosition
worldGroundPlane.castsShadow = true
worldGroundPlane.eulerAngles = SCNVector3(Float.pi / 2, 0, 0)
self.addChildNode(worldGroundPlane)
// Create a ambient light
let ambientLight = SCNNode()
ambientLight.light = SCNLight()
ambientLight.light?.color = UIColor.white
ambientLight.light?.type = SCNLight.LightType.ambient
ambientLight.position = SCNVector3(x: 0,y: 5,z: 0)
// Create a directional light node with shadow
let directionalNode = SCNNode()
directionalNode.light = SCNLight()
directionalNode.light?.type = SCNLight.LightType.directional
directionalNode.light?.color = UIColor.white
directionalNode.light?.castsShadow = true
directionalNode.light?.automaticallyAdjustsShadowProjection = true
directionalNode.light?.shadowSampleCount = 64
directionalNode.light?.shadowMode = .deferred
directionalNode.light?.shadowMapSize = CGSize(width: 2048, height: 2048)
directionalNode.light?.shadowColor = UIColor.black.withAlphaComponent(0.75)
directionalNode.position = SCNVector3(x: 0,y: 5,z: 0)
// Add the lights to the container
self.addChildNode(ambientLight)
self.addChildNode(directionalNode)
It needs to look like this in the whole scene:
P.S. The object and shadow look the same if I render it on my phone inside the app.
I Hope you can help me find a solution to achieve the goal described above.
I hope you can help me! Thanks 😊
EDIT:
The solution provided below is not the solution. Now my scene looks like this with still no shadow:
the code:
let worldGroundPlaneGeometry = SCNPlane(width: 1000, height: 1000)
let worldGroundPlane = SCNNode()
worldGroundPlane.geometry?.firstMaterial?.lightingModel = .constant
worldGroundPlane.geometry?.firstMaterial?.writesToDepthBuffer = true
worldGroundPlane.geometry?.firstMaterial?.colorBufferWriteMask = []
worldGroundPlane.geometry = worldGroundPlaneGeometry
worldGroundPlane.position = worldPosition
worldGroundPlane.castsShadow = true
worldGroundPlane.eulerAngles = SCNVector3(Float.pi / 2, 0, 0)
self.addChildNode(worldGroundPlane)
// Create a ambient light
let ambientLight = SCNNode()
ambientLight.light = SCNLight()
ambientLight.light?.shadowMode = .deferred
ambientLight.light?.color = UIColor.white
ambientLight.light?.type = SCNLight.LightType.ambient
ambientLight.position = SCNVector3(x: 0,y: 5,z: 0)
// Create a directional light node with shadow
let directionalNode = SCNNode()
directionalNode.light = SCNLight()
directionalNode.light?.type = SCNLight.LightType.directional
directionalNode.light?.color = UIColor.white
directionalNode.light?.castsShadow = true
directionalNode.light?.automaticallyAdjustsShadowProjection = true
directionalNode.light?.shadowSampleCount = 64
directionalNode.light?.shadowMode = .deferred
directionalNode.light?.shadowMapSize = CGSize(width: 2048, height: 2048)
directionalNode.light?.shadowColor = UIColor.black.withAlphaComponent(0.75)
directionalNode.position = SCNVector3(x: 0,y: 5,z: 0)
// Add the lights to the container
self.addChildNode(ambientLight)
self.addChildNode(directionalNode)
I was playing around with your lighting setup and found the ambient light gave it a very washed out look... & the shadow seemed too unnaturally dark.
Anwway I tweaked your light setup configuration to the following. I got rid of the ambient light all together, and I added a constraint on the object node (timber box) is this case. I also controlled the lighting intensity with just one directional light.
let directionalNode = SCNNode()
let constraint = SCNLookAtConstraint(target:node)
directionalNode.light = SCNLight()
directionalNode.light?.type = .directional
directionalNode.light?.color = UIColor.white
directionalNode.light?.castsShadow = true
directionalNode.light?.intensity = 2000
directionalNode.light?.shadowRadius = 16
directionalNode.light?.shadowMode = .deferred
directionalNode.eulerAngles = SCNVector3(Float.pi/2,0,0)
directionalNode.light?.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.3)
directionalNode.position = SCNVector3((node?.position.x)! + 10,(node?.position.y)! + 30,(node?.position.z)!+30)
directionalNode.constraints = [constraint]
// Add the lights to the container
self.sceneView.scene.rootNode.addChildNode(directionalNode)
The problem was that I wasn't actually using the material. Working Code:
let worldGroundPlaneGeometry = SCNFloor()
let worldGroundPlane = SCNNode()
let worldGroundMaterial = SCNMaterial()
worldGroundMaterial.lightingModel = .constant
worldGroundMaterial.writesToDepthBuffer = true
worldGroundMaterial.colorBufferWriteMask = []
worldGroundMaterial.isDoubleSided = true
worldGroundPlaneGeometry.materials = [worldGroundMaterial]
worldGroundPlane.geometry = worldGroundPlaneGeometry
self.addChildNode(worldGroundPlane)
// Create a ambient light
let ambientLight = SCNNode()
ambientLight.light = SCNLight()
ambientLight.light?.shadowMode = .deferred
ambientLight.light?.color = UIColor.white
ambientLight.light?.type = SCNLight.LightType.ambient
ambientLight.position = SCNVector3(x: 0,y: 5,z: 0)
// Create a directional light node with shadow
let directionalNode = SCNNode()
directionalNode.light = SCNLight()
directionalNode.light?.type = SCNLight.LightType.directional
directionalNode.light?.color = UIColor.white
directionalNode.light?.castsShadow = true
directionalNode.light?.automaticallyAdjustsShadowProjection = true
directionalNode.light?.shadowSampleCount = 64
directionalNode.light?.shadowRadius = 16
directionalNode.light?.shadowMode = .deferred
directionalNode.light?.shadowMapSize = CGSize(width: 2048, height: 2048)
directionalNode.light?.shadowColor = UIColor.black.withAlphaComponent(0.75)
directionalNode.position = SCNVector3(x: 0,y: 5,z: 0)
directionalNode.eulerAngles = SCNVector3(-Float.pi / 2, 0, 0)
// Add the lights to the container
self.addChildNode(ambientLight)
self.addChildNode(directionalNode)
Through Vision Framework I'm able to detect a QR-code. The next thing I would like to do is to place a SCNPlane exactly on the QRCode using ARKit. I wrote the code below to find the position of the QRCode in the real world. But the SCNPlane keeps added in the wrong place. The barcode variable in the code below is of the VNBarcodeObservation type.
Maybe one of you guys knows what I'm doing wrong.
let rect = CGPoint(x: barcode.boundingBox.origin.x, y: barcode.boundingBox.origin.y)
let hitTestResults = sceneView.hitTest(rect, types: [.existingPlaneUsingExtent, .existingPlane])
if let hitResult = hitTestResults.first {
//TODO: change to width and height
let plane = SCNPlane(width: 0.1, height: 0.1)
let material = SCNMaterial()
material.diffuse.contents = UIColor.red
plane.materials = [material]
let node = SCNNode()
node.transform = SCNMatrix4MakeRotation(-Float.pi / 2, 1, 0, 0)
node.position = SCNVector3(hitResult.worldTransform.columns.3.x,
hitResult.worldTransform.columns.3.y,
hitResult.worldTransform.columns.3.z)
node.geometry = plane
sceneView.scene.rootNode.addChildNode(node)
}
Edit:
Added a picture of the current situation. The red plane is added in the upper left corner. However, this plane has to be added exactly on te QRCode.
Works for me using the center point instead of the origins:
let center2 = CGPoint(x: result.boundingBox.midX, y: result.boundingBox.midY)
let hitTestResults2 = frame.hitTest(center2, types: [.featurePoint/*, .estimatedHorizontalPlane, .existingPlane, .existingPlaneUsingExtent*/] )
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
}
}
}
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.