I have an AR app in which I have calculated current sun position from the user. I reduced this position to distance of 10,000 meters, so it kinda "stays" in place when I move inside the scene. Now I would like to cast shadows from this node on other nodes inside my scene. I tried few types of lightning, but none was successful. Some didn't drop shadow at all, others behave strange. What would be the best method to create a light source from such distant node to cast shadows on invisible floor node? Should I also add ambient light to the scene? Can it be added to camera node, or should be somewhere else?
Use directional light for Sun simulation (Sun has parallel rays that for us are primary rays from very distant light source) and use ambient light for simulating fake secondary rays (in case you do not use Global Illumination).
// DIRECTIONAL LIGHT for `primary light rays` simulation
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light!.type = .directional
lightNode.light!.castsShadow = true
lightNode.light!.shadowMode = .deferred
lightNode.light!.categoryBitMask = -1
lightNode.light!.automaticallyAdjustsShadowProjection = true
//lightNode.light!.maximumShadowDistance = 11000
lightNode.position = SCNVector3(x: 0, y: -5000, z: 0)
lightNode.rotation = SCNVector4(x: -1, y: 0, z: 0, w: .pi/2)
scene.rootNode.addChildNode(lightNode)
Tip: The position of directional light could be any (in the following code it is even under the plane y:-5000), the most important thing is direction of directional light!
Also directional light has no falloff parameter or, so called, quadratic light decay.
// AMBIENT LIGHT for `fake secondary light rays` simulation
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = .ambient
ambientLightNode.light!.intensity = 1000
ambientLightNode.light!.color = NSColor.white
scene.rootNode.addChildNode(ambientLightNode)
Do not mistakenly use this Boolean value:
lightNode.castsShadow = true
instead of this one:
lightNode.light!.castsShadow = true
The Boolean value lightNode.castsShadow determines whether SceneKit renders the node’s contents into shadow maps.
And here's a screenshot if you wish to enable shadows in manually created directional light:
Sometimes, you get some benefits if you attach a light to the camera. In that case object's surfaces with normals parallel to light's rays are lit.
Related
I'm trying to use a spotlight in my scene and add shadows to an object. However, I noticed that when I increase the spotInnerAngle, the shadow of the object changes significantly. Here's an example:
Both of shadows in these images look quite different – does anyone know why increasing the spot angle is causing the shadow to be less apparent?
This is the code I'm using to create a spotlight/add shadows to my scene:
let spotLight = SCNNode()
spotLight.light = SCNLight()
spotLight.light?.type = SCNLight.LightType.spot
spotLight.light?.spotInnerAngle = 120
spotLight.light?.spotOuterAngle = 120
spotLight.light?.color = UIColor.white
spotLight.light?.castsShadow = true
spotLight.light?.automaticallyAdjustsShadowProjection = true
spotLight.light?.shadowSampleCount = 32
spotLight.light?.shadowRadius = 8
spotLight.light?.shadowMode = .deferred
spotLight.light?.shadowMapSize = CGSize(width: 2048, height: 2048)
spotLight.light?.shadowColor = UIColor.black.withAlphaComponent(1)
spotLight.position = SCNVector3(x: 0,y: 5,z: 0)
spotLight.eulerAngles = SCNVector3(-Float.pi / 2, 0, 0)
SceneKit's engine calculates shadows slightly differently than 3D software apps do, like Maya or 3dsMax. In SceneKit framework the position and scale of your Spotlight as well as its value of cone angle are crucial for shadow generating. The main rule is the following: when area of spotlight's ray in SceneKit becomes greater, the shadow edges become more obscure (aka blurry).
Here's a properties you have to take into consideration when using spotlight:
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light!.type = .spot
lightNode.rotation = SCNVector4(x: 0, y: 0, z: 0, w: 1)
lightNode.castsShadow = true
/* THESE SEVEN SPOTLIGHT PROPERTIES AFFECT SHADOW'S APPEARANCE */
lightNode.position = SCNVector3(0, 10, 0)
lightNode.scale = SCNVector3(7, 7, 7)
lightNode.light?.spotOuterAngle = 120
lightNode.light?.shadowRadius = 10
lightNode.light?.zNear = 0
lightNode.light?.zFar = 1000000
lightNode.light?.shadowSampleCount = 20
lightNode.light?.shadowColor = UIColor(write: 0, alpha: 0.75)
lightNode.light?.shadowMode = .deferred
scene.rootNode.addChildNode(lightNode)
Also, I recommend you use Ambient Light with very low Intensity for lighting up dark areas on your 3D models.
Hope this helps.
From Apple's documentation:
"[spotInnerAngle] determines the width of the fully illuminated area."
The default value of this property is 0, which means only the center of the area illuminated by the spotlight is lit at full intensity.
Increasing the inner angle will increase the area that is lit at full intensity, thus adding more light to the scene. In your case, this additional light was decreasing the visible shadow from that angle.
I have an ARKit scene with an invisible SCNPlane:
plane.geometry?.firstMaterial?.colorBufferWriteMask = []
This plane is placed on the ground and is used to render deferred shadows from other objects placed in the scene.
I want to render another SCNPlane which should be on the same level as the invisible plane (same Z-coordinate). The problem is, that every time the new object is under the invisible plane, it is not rendered at all.
Is there any way to render the object when it is under the invisible plane?
You can achieve it using the following lines of code:
shadowsPlane.geometry?.materials.first?.writesToDepthBuffer = true
shadowsPlane.geometry?.materials.first?.readsFromDepthBuffer = true
Choose one of two instance properties for .colorBufferWriteMask:
shadowsPlane.geometry?.materials.first?.colorBufferWriteMask = []
Set a rendering order for your objects like:
shadowsPlane.renderingOrder = -1 // the nearest layer
And, of course, use an appropriate .lightingModel instance property:
shadowsPlane.geometry?.materials.first?.lightingModel = .constant
Remember, there will be some tiny air gap between two planes:
shadowsPlane.position = SCNVector3(x: 0, y: 0, z: 0)
floorPlane.position = SCNVector3(x: 0, y: -0.01, z: 0)
Ok, what I am trying to do is create physics body/colliding boundaries for my character, my SCNNode, in my SceneKit game Im building with ARKit. This is so my node cannot move out of the user's vision/go so far away that it isn't visible as it is currently doing. My SCNNode is moved by user input, so I need to make "world boundaries" while ARKit still doesn't have vertical wall detection
I know you can place an object some set distance ahead of you in the real world as stated here Understand coordinate spaces in ARKit
and I have done that with this, just making a box with physics body here -
let box = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0) //change to be VERY TALL - need to make it a giant room
node.physicsBody = SCNPhysicsBody(type: SCNPhysicsBodyType.static, shape: nil)
box.firstMaterial?.diffuse.contents = UIColor.red
box.firstMaterial?.isDoubleSided = true
node = SCNNode(geometry: box)
node.position = SCNVector3(view.pointOfView.simdWorldFront + float3(0, 0, -0.5)) //random distance ahead
And this works, and I could add it as a child node of camera so it moves as user moves, but I don't think Im doing this correctly.
Essentially I need 4 walls to box the user/SCNNode in (corral the character) that are infinitely high, and at the VERY edge of the horizontal plane that the user can see. Really I don't know what this distance should be in the x plane:
+ float3(0, 0, -0.5)
How would you create AR boundaries like this?
Generally what I'm trying to achieve: we have map data that historically was all 2D, and the coordinate system we use is the origin point (0,0) at the top left, positive x goes right, positive y goes down. We have now added 3D data by adding a z axis, positive z coming out of the screen towards you (think top-down map view). This is a left handed coordinate system, but SceneKit is a right handed coordinate system. I would like to apply some transform at the top level of my SceneKit Scene that will convert the Scene into a left handed coordinate system such that I can modify/position/add nodes to the scene in terms of our custom mapping coordinate system and things will just work.
So far I have this:
let scene = SCNScene()
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.scale = SCNVector3(1,-1,1)
scene.rootNode.addChildNode(cameraNode)
This achieves exactly what I want, but has one big problem. It inverts all of the geometry faces, so my geometry's disappear unless I change their material's cullMode:
let mapLength = 1000 //max X axis
let mapWidth = 800 //max Y axis
let mapHeight = 100 //max Z axis
cameraNode.position = SCNVector3(mapLength / 2, mapWidth / 2, 2000)
let mapPlane = SCNNode()
mapPlane.position = SCNVector3(mapLength / 2, mapWidth / 2, 0)
mapPlane.geometry = SCNPlane(width: mapLength, height: mapWidth)
mapPlane.geometry?.firstMaterial?.diffuse.contents = UIColor.blackColor()
scene.rootNode.addChildNode(mapPlane)
mapPlane doesn't show at all! You have to rotate the camera to the underside of mapPlane in order to see it. You can easily fix this by adding a single line:
mapPlane.geometry?.firstMaterial?.cullMode = .Front
But I don't want to have to change the cullMode for every geometry/material. Is there a way to achieve this without requiring extra code at each geometry/material? Some transform that would invert the geometry face normals for all child nodes of rootNode? Ideally this would be achieved entirely by settings on the actual Scene, or by transforms on rootNode or the camera.
The documentation for SceneKit's collisionBitMask property of SCNPhysicsBody states the following:
When two physics bodies contact each other, a collision may occur.
SceneKit compares the body’s collision mask to the other body’s
category mask by performing a bitwise AND operation. If the result is
a nonzero value, then the body is affected by the collision. Each body
independently chooses whether it wants to be affected by the other
body.
That last line indicates that if I have two objects, I can set it up so that when they collide, only one of them should be affected by the collision.
let CollisionCategoryPlane = 1 << 0
let CollisionCategorySphere1 = 1 << 1
let CollisionCategorySphere2 = 1 << 2
let plane = SCNNode(geometry: SCNPlane(width: 10, height: 10))
plane.position = SCNVector3(x: 0, y: -10, z: 0)
plane.eulerAngles = SCNVector3(x: Float(-M_PI/2), y: 0, z: 0)
plane.physicsBody = SCNPhysicsBody.staticBody()
plane.physicsBody?.categoryBitMask = CollisionCategoryPlane
plane.physicsBody?.collisionBitMask = CollisionCategorySphere1 | CollisionCategorySphere2
// the plane should be affected by collisions with both spheres (but the plane is static so it doesn't matter)
scene.rootNode.addChildNode(plane)
let sphere1 = SCNNode(geometry: SCNSphere(radius: 1))
sphere1.physicsBody = SCNPhysicsBody.dynamicBody()
sphere1.physicsBody?.categoryBitMask = CollisionCategorySphere1
sphere1.physicsBody?.collisionBitMask = CollisionCategoryPlane
// sphere1 should only be affected by collisions with the plane, not with sphere2
scene.rootNode.addChildNode(sphere1)
let sphere2 = SCNNode(geometry: SCNSphere(radius: 1))
sphere2.position = SCNVector3(x: 1, y: 10, z: 0)
sphere2.physicsBody = SCNPhysicsBody.dynamicBody()
sphere2.physicsBody?.categoryBitMask = CollisionCategorySphere2
sphere2.physicsBody?.collisionBitMask = CollisionCategoryPlane | CollisionCategorySphere1
// sphere2 should be affected by collisions with the plane and sphere1
scene.rootNode.addChildNode(sphere2)
Sphere1 should fall onto the plane, then sphere2 should fall onto sphere1 and bounce off, and sphere1 should be unaffected by the collision with sphere2. However, the observed behaviour is both spheres falling onto the plane and coming to rest inside each other - no collision event between the two spheres is registered.
What is going on here?
On related notes, some even stranger behaviour is observed when I make a couple small modifications to the above code.
If remove the line that defines the plane's collsionBitMask, leaving it as the default SCNPhysicsCollisionCategoryAll, sphere1 no longer collides with the plane.
If I move the lines that define the objects' physics bodies, categoryBitMasks, and collisionBiMasks to after the objects have each been added to the the scene, all the objects will collide with every other object. Even if I set every collisionBitMask to zero.