The shader is almost complete. We have one more issue to address: the kick light looks nice when it’s controlled properly and stays at the edges, but in video games, there’s a good chance of the camera catching an object at the wrong angle. This causes the model to look like it’s under a bright spotlight and look washed out.
The choice of having the kick light should be there, but it would also be nice if we could make it so it looks like a rim light no matter what angle the camera looks at the object from.
You may recall that we used a fresnel when we made the dynamic outline; the theory is the same, but we’ll give more control of the light source.
As usual, we need uniforms and constants: the thickness of the outline as a fresnel exponent, a color, and a way to control the rim light’s softness. Note that we’re not using the kick light color. To give the most control possible, we have both the kick light and the rim light, and controlling how much one shows is based on its color. When black, it’s turned off.
uniform float rim_light_softness : hint_range(0, 1) = 0.5; uniform vec4 rim_light_color : hint_color = vec4(0, 0, 0, 1); uniform float rim_fresnel_power = 3.0;
Right before adding the kick light, we calculate the rim light fresnel. Since we want the edges to light up, we use 1 - the dot product of the normal to the view. Controlling how thin the edges should be uses the fresnel power inside of a pow
.
float rim_value = pow(1.0 - dot(NORMAL, VIEW), rim_fresnel_power);
We use the step
and smoothstep
functions to create a sharp and soft version.
float hard_rim = step(RIM_SHARPNESS, rim_value); float soft_rim = smoothstep(RIM_SOFTNESS_MIN, RIM_SOFTNESS_MAX, rim_value);
With accompanying constants:
const float RIM_SHARPNESS = 0.4; const float RIM_SOFTNESS_MIN = 0.2; const float RIM_SOFTNESS_MAX = 0.7;
The result is a mix between soft and sharp, multiplied by the rim light color.
vec3 out_rim_light = vec3(mix(hard_rim, soft_rim, rim_light_softness)) * rim_light_color.rgb;
Which we add to the out color. We multiply it by the kick light value from the light viewport so we can control the angle of the rim light.
out_color += out_rim_light * kick_light_value;
We wrap this code inside of an if statement based on the color:
if(rim_light_color.r > 0.0 || rim_light_color.g > 0.0 || rim_light_color.b > 0.0) { float rim_value = pow(1.0 - dot(NORMAL, VIEW), rim_fresnel_power); float hard_rim = step(RIM_SHARPNESS, rim_value); float soft_rim = smoothstep(RIM_SOFTNESS_MIN, RIM_SOFTNESS_MAX, rim_value); vec3 out_rim_light = vec3(mix(hard_rim, soft_rim, rim_light_softness)) * rim_light_color.rgb; out_color += out_rim_light * kick_light_value; }
Since we’re using the NORMAL
built-in to affect our rim light, we could add a couple of uniforms that lets us alter it. Maybe you want the rim light to be a bit thicker horizontally, just not vertically.
uniform float rim_normal_offset_x = 0.0; uniform float rim_normal_offset_y = 0.0;
If we add this to the NORMAL
when we calculate the fresnel, we can affect it on two axis. The dot product is no longer accurate because NORMAL
is no longer a vector of length 1, but we can fix it by normalizing it.
float rim_value = pow(1.0 - dot(normalize(NORMAL + vec3(0.0, rim_normal_offset_y, rim_normal_offset_x)), VIEW), rim_fresnel_power);
Here’s some food for thought for you: what if you added a similar offset to the NORMAL
of a vertex shader in the specular viewport?