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
}
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;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With