100 Days of Graphics: Days 15 – 21 4 August 2017

Favorite Result

This week took a surprising turn into participating in Ludum Dare 39. You can read about my experience in the previous post here. All of my work this week came out of that quick little weekend.

As a quick aside, I’m starting to discover a better frame of mind to understand how shader programs run. Typically, programs run from top to bottom through a series of commands that essentially modify state. Shaders are a whole new adventure where we color pixels on a screen and interpolate between inputs. It’s not worthwhile thinking of “What if we call this function with these inputs” since that’s one minuscule part of the output. Instead, imagine a wide range of inputs that the function will receive. Write a generalized function that’s founded on math, and it’ll all work out in the end.

Gem Shader

The game uses gems/rocks and I thought it’d be extremely cool to have a working gem shader in the game! I’ve never made one before, but considering this 100-days of graphics challenge it was as good a time as ever to learn.

The underlying principle behind a gem-shader is pretty straightforward. We want to use a brighter color at places where the camera’s viewpoint vector, when reflected off of the gem surface, points directly to the light source. The figure below shows the ideal situation that should create the maximum brightness reflection.

When the viewer’s viewing direction directly matches the same angle that the light is hitting the surface, the surface should be at a maximum brightness.

The way I coded this was to compare the dot-product of the viewing direction and the face’s normal, with the dot-product of the light’s direction and the face’s normal. If the absolute difference (subtract one value from the other and absolute the result) is nearly 0, it’s an ideal situation as describe above.

Since we’re working with unit vectors here, the worst case is a value of 1 indicating the angles are way off and there should be no shimmering.

Initially, I coded this just using the surface’s normal thinking that, if the normal faced the light then it should be brightly lit. This is true for general brightness, but lighting on shiny surfaces change as you change your own viewing position. The amount of shine seen changes. That’s why we need the viewing direction as well. The code block below does this.

half4 LightingCustom (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
    half NdotL = dot (s.Normal, lightDir);
    half NdotV = dot(s.Normal, viewDir);
    half difference = abs(NdotL - NdotV);
    if (difference < 0.05)
        // shine is the value to use at the ideal case.
        half4 shine = half4(lightness, lightness, lightness, 1) * .9;
        // shineRamp causes most difference values to not return a shine
        // at all. That's why we make sure the difference is less-than 0.05 first.
        // Doing this ensures only angles really close to ideal produce a shine, and that
        // it gets shiny really quickly or falls off quickly.
        // Notice how I use 0.05 here and in the condition above, also that 0.95 = 1 - 0.05
        // If we wanted to increase the ramp sliding, we would have to modify all 3 values
        float shineRamp = (1 - difference - 0.95) / 0.05;
        c = lerp(c, shine, shineRamp);

Custom Surface Shader

This is the same gem using the same material in both pictures and there is no post-processing. The only difference is the color of light being shine upon the gem! The gem will be colored based on how much the red channel is present in the light, and will be brighter based on how much blue channel is present in the light! Rather than doing an expected situation where you color the surface based on the color of light coming in, we just use that light color as information to decide if the output should be greyscale or not.

Here’s the ShaderLab code that produces this effect.

half4 LightingCustom (SurfaceOutput s, half3 lightDir, half atten)
    half lightness = (_LightColor0.b + _LightColor0.r) / 2;

    // Perform a Lambert surface shading. Simple and looks nice.
    half NdotL = dot (s.Normal, lightDir);
    half diff = NdotL * 0.5 + 0.5;

    // colored version of the surface.
    half4 c;
    c.rgb = s.Albedo * (diff * atten) * lightness;

    // greyscale version
    float average = (c.r + c.g + c.b) / 3;
    half4 greyscale = half4(average, average, average, 1);

    // Interpolate between the two based on the light's redness
    return lerp(greyscale, c, _LightColor0.r);

Anyway, I’m really happy without how this effect came out and it was my first play with Unity’s Surface shader with a custom Lighting function. I only wish there was better documentation. There’s good documentation, but I couldn’t find anything about where _LightColor comes from. It’s not defined in my shaders. I had the same problem in the gem shader for fetching the camera’s viewing direction. It turns out it’s the 3rd parameter. Who knew? I literally guessed-and-checked for that.

Post Metadata