Crashalot
Crashalot

Reputation: 34523

SceneKit: Make blocks more lifelike or 3D-like

The code below is used to create a scene and create blocks in SceneKit. The blocks come out looking flat and not "3D enough" according to our users. Screenshots 1-2 show our app.

Screenshots 3-5 show what users expect the blocks to look like, that is more 3D-like.

After speaking to different people, there are different opinions about how to render blocks that look more like screenshots 3-5. Some people say use ambient occlusion, others say voxel lighting, some say use spot lighting and use shadows, or directional lighting.

We previously tried adding omni lighting, but that didn't work so it was removed. As you can see in the code, we also experimented with an ambient light node but that also didn't yield the right results.

What is the best way to render our blocks and achieve a comparable look to screenshots 3-5?

Note: we understand the code is not optimized for performance, i.e., that polygons are shown that should not be shown. That is okay. The focus is not on performance but rather on achieving more 3D-like rendering. You can assume some hard limit on nodes, like no more than 1K or 10K in a scene.

Code:

func createScene() {
    // Set scene view
    let scene = SCNScene()
    sceneView.jitteringEnabled = true
    sceneView.scene = scene

    // Add camera node
    sceneView.pointOfView = cameraNode

    // Make delegate to capture screenshots
    sceneView.delegate = self

    // Set ambient lighting
    let ambientLightNode = SCNNode()
    ambientLightNode.light = SCNLight()
    ambientLightNode.light!.type = SCNLightTypeAmbient
    ambientLightNode.light!.color = UIColor(white: 0.50, alpha: 1.0)
    //scene.rootNode.addChildNode(ambientLightNode)
    //sceneView.autoenablesDefaultLighting = true

    // Set floor
    setFloor()

    // Set sky
    setSky()

    // Set initial position for user node
    userNode.position = SCNVector3(x: 0, y: Float(CameraMinY), z: Float(CameraZoom))

    // Add user node
    scene.rootNode.addChildNode(userNode)

    // Add camera to user node
    // zNear fixes white triangle bug while zFar fixes white line bug
    cameraNode.camera = SCNCamera()
    cameraNode.camera!.zNear = Double(0.1)
    cameraNode.camera!.zFar = Double(Int.max)
    cameraNode.position = SCNVector3(x: 0, y: 0, z: 0) //EB: Add some offset to represent the head
    userNode.addChildNode(cameraNode)
}


private func setFloor() {
    // Create floor geometry
    let floorImage = UIImage(named: "FloorBG")!
    let floor = SCNFloor()
    floor.reflectionFalloffEnd = 0
    floor.reflectivity = 0
    floor.firstMaterial!.diffuse.contents = floorImage
    floor.firstMaterial!.diffuse.contentsTransform = SCNMatrix4MakeScale(Float(floorImage.size.width)/2, Float(floorImage.size.height)/2, 1)
    floor.firstMaterial!.locksAmbientWithDiffuse = true
    floor.firstMaterial!.diffuse.wrapS = .Repeat
    floor.firstMaterial!.diffuse.wrapT = .Repeat
    floor.firstMaterial!.diffuse.mipFilter = .Linear

    // Set node & physics
    // -- Must set y-position to 0.5 so blocks are flush with floor
    floorLayer = SCNNode(geometry: floor)
    floorLayer.position.y = -0.5
    let floorShape = SCNPhysicsShape(geometry: floor, options: nil)
    let floorBody = SCNPhysicsBody(type: .Static, shape: floorShape)
    floorLayer.physicsBody = floorBody
    floorLayer.physicsBody!.restitution = 1.0

    // Add to scene
    sceneView.scene!.rootNode.addChildNode(floorLayer)
}


private func setSky() {
    // Create sky geometry
    let sky = SCNFloor()
    sky.reflectionFalloffEnd = 0
    sky.reflectivity = 0
    sky.firstMaterial!.diffuse.contents = SkyColor
    sky.firstMaterial!.doubleSided = true
    sky.firstMaterial!.locksAmbientWithDiffuse = true
    sky.firstMaterial!.diffuse.wrapS = .Repeat
    sky.firstMaterial!.diffuse.wrapT = .Repeat
    sky.firstMaterial!.diffuse.mipFilter = .Linear
    sky.firstMaterial!.diffuse.contentsTransform = SCNMatrix4MakeScale(Float(2), Float(2), 1);

    // Set node & physics
    skyLayer = SCNNode(geometry: sky)
    let skyShape = SCNPhysicsShape(geometry: sky, options: nil)
    let skyBody = SCNPhysicsBody(type: .Static, shape: skyShape)
    skyLayer.physicsBody = skyBody
    skyLayer.physicsBody!.restitution = 1.0

    // Set position
    skyLayer.position = SCNVector3(0, SkyPosY, 0)

    // Set fog
    /*sceneView.scene?.fogEndDistance = 60
     sceneView.scene?.fogStartDistance = 50
     sceneView.scene?.fogDensityExponent = 1.0
     sceneView.scene?.fogColor = SkyColor */

    // Add to scene
    sceneView.scene!.rootNode.addChildNode(skyLayer)
}


func createBlock(position: SCNVector3, animated: Bool) {
  ...

  // Create box geometry
  let box = SCNBox(width: 1.0, height: 1.0, length: 1.0, chamferRadius: 0.0)
  box.firstMaterial!.diffuse.contents = curStyle.getContents() // "curStyle.getContents()" either returns UIColor or UIImage
  box.firstMaterial!.specular.contents = UIColor.whiteColor()

  // Add new block
  let newBlock = SCNNode(geometry: box)
  newBlock.position = position
  blockLayer.addChildNode(newBlock)
}

Screenshots 1-2 (our app):

enter image description here enter image description here

Screenshots 3-5 (ideal visual representation of blocks):

enter image description here enter image description here enter image description here

Upvotes: 2

Views: 2801

Answers (3)

lock
lock

Reputation: 2897

I still think there's a few easy things you can do that will make a big difference to how your scene is rendered. Apologies for not using your code, this example is something I had lying around.

Right now your scene is only lit by an ambient light.

let aLight = SCNLight()
aLight.type = SCNLightTypeAmbient
aLight.color = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1.0)
let aLightNode = SCNNode()
aLightNode.light = aLight
scene.rootNode.addChildNode(aLightNode)

If I use only this light in my scene I see the following. Note how all faces are lit the same irrespective of the direction they face. Some games do pull off this aesthetic very well.

enter image description here

The following block of code adds a directional light to this scene. The transformation applied in this light won't be valid for your scene, it's important to orientate the light according to where you want the light coming from.

let dLight = SCNLight()
dLight.type = SCNLightTypeDirectional
dLight.color = UIColor(red: 0.6, green: 0.6, blue: 0.6, alpha: 1.0)

let dLightNode = SCNNode()
dLightNode.light = dLight
var dLightTransform = SCNMatrix4Identity
dLightTransform = SCNMatrix4Rotate(dLightTransform, -90 * Float(M_PI)/180, 1, 0, 0)
dLightTransform = SCNMatrix4Rotate(dLightTransform, 37 * Float(M_PI)/180, 0, 0, 1)
dLightTransform = SCNMatrix4Rotate(dLightTransform, -20 * Float(M_PI)/180, 0, 1, 0)
dLightNode.transform = dLightTransform
scene.rootNode.addChildNode(dLightNode)

Now we have shading on each of the faces based on their angle relative to the direction of the light.

enter image description here

Currently SceneKit only supports shadows if you're using the SCNLightTypeSpot. Using a spotlight means we need to both orientate (as per directional light) and position it. I use this as a replacement for the directional light.

let sLight = SCNLight()
sLight.castsShadow = true
sLight.type = SCNLightTypeSpot
sLight.zNear = 50
sLight.zFar = 120
sLight.spotInnerAngle = 60
sLight.spotOuterAngle = 90

let sLightNode = SCNNode()
sLightNode.light = sLight
var sLightTransform = SCNMatrix4Identity
sLightTransform = SCNMatrix4Rotate(sLightTransform, -90 * Float(M_PI)/180, 1, 0, 0)
sLightTransform = SCNMatrix4Rotate(sLightTransform, 65 * Float(M_PI)/180, 0, 0, 1)
sLightTransform = SCNMatrix4Rotate(sLightTransform, -20 * Float(M_PI)/180, 0, 1, 0)
sLightTransform = SCNMatrix4Translate(sLightTransform, -20, 50, -10)
sLightNode.transform = sLightTransform
scene.rootNode.addChildNode(sLightNode)

In the above code we first tell the spotlight to cast a shadow, by default all nodes in your scene will then cast a shadow (this can be changed). The zNear and zFar settings are also important and must be specified so that the nodes casting shadows are within this range of distance from the light source. Nodes outside this range will not cast a shadow.

enter image description here

After shading/shadows there's a number of other effects you can apply easily. Depth of field effects are available for the camera. Fog is similarly easy to include.

scene.fogColor = UIColor.blackColor()
scene.fogStartDistance = 10
scene.fogEndDistance = 110
scenekitView.backgroundColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1.0)

enter image description here

Update Turns out you can get shadows from a directional light. Modifying the spotlight code from above by changing its type and setting the orthographicScale. Default value for orthographicScale seems to be 1.0, obviously not suitable for scenes much larger than 1.

let dLight = SCNLight()
dLight.castsShadow = true
dLight.type = SCNLightTypeDirectional
dLight.zNear = 50
dLight.zFar = 120
dLight.orthographicScale = 30

let dLightNode = SCNNode()
dLightNode.light = dLight
var dLightTransform = SCNMatrix4Identity
dLightTransform = SCNMatrix4Rotate(dLightTransform, -90 * Float(M_PI)/180, 1, 0, 0)
dLightTransform = SCNMatrix4Rotate(dLightTransform, 65 * Float(M_PI)/180, 0, 0, 1)
dLightTransform = SCNMatrix4Rotate(dLightTransform, -20 * Float(M_PI)/180, 0, 1, 0)
dLightTransform = SCNMatrix4Translate(dLightTransform, -20, 50, -10)
dLightNode.transform = dLightTransform
scene.rootNode.addChildNode(dLightNode)

Produces the following image. Directional light with shadows

The scene size is 60x60, so in this case setting the orthographic scale to 30 produces shadows for the objects close to the light. The directional light shadows appear different to the spot light due to the difference in projections (orthographic vs perspective) used when rendering the shadow map.

Upvotes: 13

Pro Blaster
Pro Blaster

Reputation: 491

all answers above (or below) seem to be good ones (at the time of this writing) however,


what I use (just for setting up a simple scene) is one ambient light (lights everything in all directions) to make things visible.
And then one omnidirectional light positioned somewhere in the middle of your scene, the omni light can be raised up (Y up I mean) to light the whole of your scene. The omni light gives the user a sense of shading and the ambient light makes it more like a sun light.

for example:

Imagine sitting in a living room (like I am right now) and the sun-light peers through the window to your right.
You can obviously see a shadow of an area that the couch is not getting sun light, however you can still see details of what is in the shadow.



Now! all the sudden your wold gets rid of ambient light BOOM! The shadow is now pitch black, you can't anymore see what is in the shadow.

Or say the ambient light came back again (what a relief), but all the sudden the omni light stopped working. (probably my fault :( ) Everything now is lighted the same, no shadow, no difference, but if you lay a paper on the table, and look at it from above, there is no shadow! So you think it is part of the table! In a world like this your rely on the contour of something in order to see it- you would have to look at the table from side view, to see the thickness of the paper.
lighting example


Hope this helps (at least a little)

Note: ambient lighting give a similar effect to emissive material

Upvotes: 1

Confused
Confused

Reputation: 6288

Ambient occlusion calculations will give you the best results, but is very expensive, particularly in a dynamically changing world, which it looks like this is.

There are several ways to cheat, and get the look of Ambient occlusion.

Here's one: place transparent, gradient shadow textures on geometry "placards" used to place/present the shadows at the places required. This will involve doing checks of geometry around the new block before determining what placards to place, with which desired texture for the shadowing. But this can be made to look VERY good, at a very low cost in terms of polygons, draw calls and filtrate. It's probably the cheapest way to do this, and have it look good/great, and can only really be done (with a good look) in a world of blocks. A more organic world rules this technique out. Please excuse the pun.

Or, another, similar: Place additional textures onto/into objects that have the shadow, and blend this with the other textures/materials in the object. This will be a bit fiddly, and I'm not an expert on the powers of materials in Scene Kit, so can't say for sure this is possible and/or easy in it.

Or: Use a blend of textures with a vertex shader that's adding a shadow from the edges that touch or otherwise need/desire a shadow based on your ascertaining what and where you want shadows and to what extent. Will still need the placards trick on the floors/walls unless you add more vertices inside flat surfaces for the purpose of vertex shading for shadows.

Here's something I did for a friend's CD cover... shows the power of shadows. It's orthographic, not true 3D perspective, but the shadows give the impression of depths and create the illusions of space:

enter image description here

Upvotes: 5

Related Questions