ARKit – Rendering a 3D object under an invisible plane - swift

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)

Related

Why does SceneKit spotlight angle affect shadow?

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.

Cast shadows from distant light

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.

ARKit - create world boundaries/get position at edge of visible plane?

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?

SceneKit collisionBitMask not behaving as expected

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.

Swift - How to change the Pivot of a SCNNode object

I've been playing with the SCNNode object for a while now and I'm lost with the Pivot. How can I change the pivot of a SCNNode (SCNBox as a bar) and place the pivot on one of the edge of the bar?
A node's pivot is a transformation matrix, the inverse of which is applied to the node before its transform property takes effect. For example, take a look at this bit from the default SceneKit Game template in Xcode:
let boxNode = SCNNode()
boxNode.geometry = SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0.02)
If you set the boxNode's position, that point corresponds to the center of the cube, and if you rotate it (as the template does in an animation), it spins around its center.
To change the anchor point, set the pivot to a translation transform:
boxNode.pivot = SCNMatrix4MakeTranslation(0.5, 0.5, 0.5)
Now, when you set the position that point corresponds to the top-right-front corner of the cube, and when you rotate the cube it spins around that corner.
More generally, a pivot transforms the contents of a node relative to the node's own transform. Suppose you wanted to model the precession of the Earth's axis of rotation. You could do this by creating two animations: one that animates pivot to spin the node around its own Y axis, and another that animates rotation to move that axis relative to the space containing the node.
On the pivot topic:
Just in case you do not have dimensions for your geometry/node something like this might help (especially for SCNText).
var minVec = SCNVector3Zero
var maxVec = SCNVector3Zero
if node.getBoundingBoxMin(&minVec, max: &maxVec) {
let bound = SCNVector3(x: maxVec.x + minVec.x,
y: maxVec.y + minVec.y,
z: maxVec.z + minVec.z)
node.pivot = SCNMatrix4MakeTranslation(bound.x / 2,
bound.y / 2,
bound.z / 2)
}