Specular Shading Optimization For A More Natural Looking

Low poly water mesh with unity standard specular material.


When learning and practicing shader programming for Unity, I found the Cg Wiki website extremely benevolent on getting a well-rounded understanding of how to shade an object step by step. Apart from the magic configuration parts(Shader Properties/Passes/Tags etc.) of a typical Unity shader, the main function-related part of the shader only lies between CGPROGRAM and ENDCG. According to my practice, I believe Cg Wiki and Unity Shader Reference can cover almost all the basic tricks of Unity shader programming. ;)

However, when I went through the Specular Highlights tutorial trying to achieve a Phong Shading effect from scratch, I found the code snippet that the tutorial provided has some artifacts.

Different areas of a typical shaded sphere.
Note that the terminator line should be a soft gradient.


After implementing the shading algorithm, the visual looks pretty promising. But when I rotate the light source or move the view direction I notice the terminator could sometimes becomes a sharp boundary between lit and shadow areas:

Artifact of the specular shader.
(The terminator line gets unnatural at some angles.)


float3 specularReflection;
if (dot(normalDirection, lightDirection) < 0.0){
  // light source on the wrong side.
  specularReflection = float3(0.0, 0.0, 0.0); // no specular reflection.
}
else {
  // light source on the right side
  specularReflection =
    attenuation *
    _LightColor0.rgb *
    _SpecColor.rgb *
    pow(max(0.0, dot(reflect(-lightDirection, normalDirection), viewDirection)),
    _Shininess);
}

return float4(
  ambientLighting +
  diffuseReflection +
  specularReflection,
  1.0);

The specular is calculated by the dot product of the reflect vector and the view direction vector. However, this angle can be larger than 90 degrees. According to how dot product is calculated,

When the angle between view direction and lighting direction is larger than 90 degrees, the cosine of it will be negative, and the dot product is also going to be negative.

Color is in the type of float4 / half4 / fixed4 in Cg and can not has negative elements. The negative values are simply treated as pitch black and cause this “hard terminator” artifact.

One of the ways to fix it is to multiply the final color result with the same dot product result again.

specularReflection = attenuation * _LightColor0.rgb * _SpecColor.rgb *
  pow(max(0.0, dot(reflect(-lightDirection, normalDirection), viewDirection)),
  _Shininess);
  * dot(lightDirection, normalDirection); // fixed!

Since the length of direction vectors is always 1, so this can get rid off the negative values. And this is the optimized result:

Now the terminator is smooth! :D