I have a floor node
, on which I need to cast shadow from directional light
. This node needs to be transparent (used in AR
environment).
And this works fine when I use ARKit
, but the same setup using SceneKit
shows no shadow or reflection. How can I cast a shadow in SceneKit
like this?
The problem with SceneKit is caused by the fact, that I set sceneView.backgroundColor = .clear
- but I need this behaviour in this app. Can this be somehow avoided?
Sample code, demonstrating this issue (works only on device, not in simulator):
@IBOutlet weak var sceneView: SCNView! {
didSet {
sceneView.scene = SCNScene()
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
sceneView.pointOfView = cameraNode
let testNode = SCNNode(geometry: SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0))
testNode.position = SCNVector3(x: 0, y: 0, z: -5)
sceneView.scene!.rootNode.addChildNode(testNode)
let animation = SCNAction.rotateBy(x: 0, y: .pi, z: 0, duration: 3.0)
testNode.runAction(SCNAction.repeatForever(animation), completionHandler: nil)
let floor = SCNFloor()
floor.firstMaterial!.colorBufferWriteMask = []
floor.firstMaterial!.readsFromDepthBuffer = true
floor.firstMaterial!.writesToDepthBuffer = true
floor.firstMaterial!.lightingModel = .constant
let floorNode = SCNNode(geometry: floor)
floorNode.position = SCNVector3(x: 0, y: -2, z: 0)
sceneView.scene!.rootNode.addChildNode(floorNode)
let light = SCNLight()
light.type = .directional
light.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.5)
light.color = UIColor.white
light.castsShadow = true
light.automaticallyAdjustsShadowProjection = true
light.shadowMode = .deferred
let sunLightNode = SCNNode()
sunLightNode.position = SCNVector3(x: 1_000, y: 1_000, z: 0)
sunLightNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: .pi * 1.5)
sunLightNode.light = light
sceneView.scene!.rootNode.addChildNode(sunLightNode)
let omniLightNode: SCNNode = {
let omniLightNode = SCNNode()
let light: SCNLight = {
let light = SCNLight()
light.type = .omni
return light
}()
omniLightNode.light = light
return omniLightNode
}()
sceneView.scene!.rootNode.addChildNode(omniLightNode)
}
}
override func viewDidLoad() {
super.viewDidLoad()
let tapGR = UITapGestureRecognizer(target: self, action: #selector(toggleTransparent))
view.addGestureRecognizer(tapGR)
}
@objc func toggleTransparent() {
transparent = !transparent
}
var transparent = false {
didSet {
sceneView.backgroundColor = transparent ? .clear : .white
}
}
Here is the same example for macOS, build on top of SceneKit game project:
import SceneKit
import QuartzCore
class GameViewController: NSViewController {
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)
let testNode = SCNNode(geometry: SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0))
testNode.position = SCNVector3(x: 0, y: 0, z: -5)
scene.rootNode.addChildNode(testNode)
let animation = SCNAction.rotateBy(x: 0, y: .pi, z: 0, duration: 3.0)
testNode.runAction(SCNAction.repeatForever(animation), completionHandler: nil)
let floor = SCNFloor()
floor.firstMaterial!.colorBufferWriteMask = []
floor.firstMaterial!.readsFromDepthBuffer = true
floor.firstMaterial!.writesToDepthBuffer = true
floor.firstMaterial!.lightingModel = .constant
let floorNode = SCNNode(geometry: floor)
floorNode.position = SCNVector3(x: 0, y: -2, z: 0)
scene.rootNode.addChildNode(floorNode)
let light = SCNLight()
light.type = .directional
light.shadowColor = NSColor(red: 0, green: 0, blue: 0, alpha: 0.5)
light.color = NSColor.white
light.castsShadow = true
light.automaticallyAdjustsShadowProjection = true
light.shadowMode = .deferred
let sunLightNode = SCNNode()
sunLightNode.position = SCNVector3(x: 1_000, y: 1_000, z: 0)
sunLightNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: .pi * 1.5)
sunLightNode.light = light
scene.rootNode.addChildNode(sunLightNode)
let omniLightNode: SCNNode = {
let omniLightNode = SCNNode()
let light: SCNLight = {
let light = SCNLight()
light.type = .omni
return light
}()
omniLightNode.light = light
return omniLightNode
}()
scene.rootNode.addChildNode(omniLightNode)
// 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
// configure the view
scnView.backgroundColor = .clear
// scnView.backgroundColor = .white // shadow works in this mode, but I need it to be clear
}
}
Sample projects:
MacOS: https://www.dropbox.com/s/1o50mbgzg4gc0fg/Test_macOS.zip?dl=1
iOS: https://www.dropbox.com/s/fk71oay1sopc1vp/Test.zip?dl=1
In macOS you can change backgroundColor in last line of ViewController - I need it to be clear, so I can show camera preview under it.
On pictures below you can see what it looks like when sceneView.backgroundColor is white, and below - clear. On clear version there is no shadow.
First : You need to connect it as a
node
to thescene
, not as ageometry type
.Shadow on invisible SCNFloor():
Shadow on visible SCNPlane() and our camera is under SCNFloor():
Second : A
shadow color
must be set like this for macOS:...and for iOS it looks like this:
Alpha component here (
alpha: 0.5
) is anopacity
of the shadow and RGB components (white: 0
) is black color of the shadow.P.S.
In this particular case I can't catch a robust shadow when
sceneView.backgroundColor = .clear
, because you need to switch betweenRGBA=1,1,1,1
(white mode: white colour, alpha=1) andRGBA=0,0,0,0
(clear mode: black colour, alpha=0).In order to see semi-transparent shadow on a background the components should be
RGB=1,1,1
andA=0.5
, but these values are whitening the image due to internal compositing mechanism of SceneKit. But when I setRGB=1,1,1
andA=0.02
the shadow is very feeble.Here's a tolerable workaround for now (look for solution below in SOLUTION section):
If I set
light.shadowColor = UIColor(white: 0, alpha: 1)
I'll get satisfactory shadow on BG image but solid black shadow on white.Here's a formula for
OVER
operation :