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

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
- calculate luminance (using formula from here) of the
_lightingContribution.diffuse
color (in case the lighting is not pure white)
- subtract it from one to get luminance of the "dark side"
- 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
- 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;