Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift SceneKit lighting and effecting emission texture

I'm developing an app which is about the solar system. I'm trying to turnoff the Emission Texture, where the light hits the surface of the planet. But the problem is that an emission texture by default, always shows the emission points regardless the absence or presence of the light.

My request in a nutshell: ( I wanna hide the emission points, on places where the light hits the surface ) enter image description here

override func viewDidLoad() {
    super.viewDidLoad()

    let scene = SCNScene()

    let earth = SCNSphere(radius: 1)
    let earthNode = SCNNode()
    let earthMaterial = SCNMaterial()
    earthMaterial.diffuse.contents = UIImage(named: "earth.jpg")
    earthMaterial.emission.contents = UIImage(named: "earthEmission.jpg")
    earth.materials = [earthMaterial]
    earthNode.geometry = earth
    scene.rootNode.addChildNode(earthNode)

    let lightNode = SCNNode()
    lightNode.light = SCNLight()
    lightNode.light?.type = .omni
    lightNode.position = SCNVector3(x: 0, y: 10, z: 5)
    scene.rootNode.addChildNode(lightNode)

    sceneView.scene = scene


}
like image 898
Joseph Avatar asked Dec 13 '17 22:12

Joseph


1 Answers

SceneKit's shader modifiers are a perfect fit for this kind of task.

You can see footage of the final result here.

Fragment shader modifier

visual representation of _lightingContribution.diffuse and the final result of the shader modifier

We can use _lightingContribution.diffuse (RGB (vec3) color representing lights that are applied to the diffuse) to determine areas of an object (in this case - Earth) that are illuminated and then use it to mask the emission texture in the fragment shader modifier.

The way you use it is really up to you. Here's the simplest solution I've come up with (using GLSL syntax, though it will be automatically converted to Metal at runtime if you are using it)

uniform sampler2D emissionTexture;

vec3 light = _lightingContribution.diffuse;
float lum = max(0.0, 1 - (0.2126*light.r + 0.7152*light.g + 0.0722*light.b)); // 1
vec4 emission = texture2D(emissionTexture, _surface.diffuseTexcoord) * lum; // 2, 3
_output.color += emission; // 4
  1. calculate luminance (using formula from here) of the _lightingContribution.diffuse color (in case the lighting is not pure white)
  2. subtract it from one to get luminance of the "dark side"
  3. get emission from a custom texture using diffuse UV coordinates (granted emission and diffuse textures have the same ones) and apply luminance to it by multiplication
  4. Add it to the final output color (the same way regular emission is applied)

That's it for the shader part, now let's go though the Swift side of things.


Swift setup

First-off, we are not going to use emission.contents property of a material, instead we would need to create a custom SCNMaterialProperty

let emissionTexture = UIImage(named: "earthEmission.jpg")!
let emission = SCNMaterialProperty(contents: emissionTexture)

and set it to the material using setValue(_:forKey:)

earthMaterial.setValue(emission, forKey: "emissionTexture")

Pay close attention to the key – it should be the same as the uniform in the shader modifier. Also you don't need to persist the material property yourself, setValue creates a strong reference.

All that is left to do is to set the fragment shader modifier to the material:

let shaderModifier =
"""
uniform sampler2D emissionTexture;

vec3 light = _lightingContribution.diffuse;
float lum = max(0.0, 1 - (0.2126*light.r + 0.7152*light.g + 0.0722*light.b));
vec4 emission = texture2D(emissionTexture, _surface.diffuseTexcoord) * lum;
_output.color += emission;
"""
earthMaterial.shaderModifiers = [.fragment: shaderModifier]

Here's footage of this shader modifier in motion.

Note that a light source has to be quite bright otherwise dim lights are going to be seen around the "globe". I had to set lightNode.light?.intensity to at least 2000 in your setup for it to work as expected. You might want to experiment with the way luminosity is calculated and applied to emission to get better results.


In case you might need it, _lightingContribution is a structure available in the fragment shader modifier that has also has ambient and specular members (below is Metal syntax):

struct SCNShaderLightingContribution {
    float3 ambient;
    float3 diffuse;
    float3 specular;
} _lightingContribution;
like image 59
Lësha Turkowski Avatar answered Oct 05 '22 09:10

Lësha Turkowski