Reputation: 89
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 )
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
}
Upvotes: 5
Views: 2416
Reputation: 11
I like Lësha's answer, made a small modification to the shader so that it will work with lower light levels. Added a threshold (t) below which emission values will not show, and then between the threshold and zero it interpolates values between diffuse and diffuse + emission. Changing the value of t adusts the width of the band depicting the transition between night and day. I also appended a 0.5 multiplier on the emission formula, since the emission texture I'm using looked artificially bright without it.
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 * 0.5;
float t = 0.11; // no emission will show above this threshold
_output.color = vec4(
light.r > t ? _output.color.r : light.r/t * _output.color.r + (1-light.r/t) * (_output.color.r + emission.r),
light.g > t ? _output.color.g : light.g/t * _output.color.g + (1-light.g/t) * (_output.color.g + emission.g),
light.b > t ? _output.color.b : light.b/t * _output.color.b + (1-light.b/t) * (_output.color.b + emission.b),1);
"""
Upvotes: 1
Reputation: 1391
SceneKit's shader modifiers are a perfect fit for this kind of task.
You can see footage of the final result here.
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
_lightingContribution.diffuse
color (in case the lighting is not pure white)That's it for the shader part, now let's go though the Swift side of things.
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;
Upvotes: 10