Recreate Phong Shading Model In Unity


Phong shading model is one of the most classic realistic shading models. It captures the basic features of a lit object using a very concise and straightforward theory. It matches people’s observation in daily life, and also easy to describe using math. Therefore, Phong shading model is always used as the base for other artistic shading effects to be built on.

Shading Model Breakdown

Visual illustration of the Phong equation.


The theory is relatively simple. The final illumination of an object is a sum of ambient lighting and all the diffuse lighting & specular that caused by all the light sources. Ambient lighting and diffuse lighting are usually applied globally, and specular is for object that is not matte.



  • ks, is specular reflection constant, the ratio of reflection of the specular term of incoming light,
  • kd, is diffuse reflection constant, the ratio of reflection of the diffuse term of incoming light (Lambertian reflectance),
  • ka, is ambient reflection constant, the ratio of reflection of the ambient term present in all points in the scene rendered,
  • α, is shininess constant, which is larger for surfaces that are smoother and more mirror-like. When this constant is large the specular highlight is small.
  • Lm, is the direction vector from the point on the surface toward each light source,
  • N, is the normal at this point on the surface,
  • Rm, is the direction of the reflected ray of light at this point on the surface,
  • V, is the direction pointing towards the viewer (such as a virtual camera).

Rm which is the reflected ray is essential for the specular effect. It is calculated with incoming ray vector, surface normal vector, and view direction vector using dot product.



Vectors for calculating Phong (or Blinn–Phong) shading.


Shader code

Here is the shader script for Phong shading using Cg:

Pass{
  Tags{ "LightMode" = "ForwardBase" }
  // pass for ambient light and first light source

  CGPROGRAM

  #pragma vertex vert  
  #pragma fragment frag

  #include "UnityCG.cginc"
  uniform float4 _LightColor0;
  // color of light source (from "Lighting.cginc")

  // User-specified properties
  uniform float4 _Color;
  uniform float4 _SpecColor;
  uniform float _Shininess;

  struct vertexInput {
    float4 vertex : POSITION;
    float3 normal : NORMAL;
  };

  struct vertexOutput {
    float4 pos : SV_POSITION;
    float4 posWorld : TEXCOORD0;
    float3 normalDir : TEXCOORD1;
  };

  vertexOutput vert(vertexInput input)
  {
    vertexOutput output;

    float4x4 modelMatrix = _Object2World;
    float4x4 modelMatrixInverse = _World2Object;

    output.posWorld =
      mul(modelMatrix, input.vertex);
    output.normalDir =
      normalize(mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
    output.pos =
      mul(UNITY_MATRIX_MVP, input.vertex);
    return output;
  }

  float4 frag(vertexOutput input) : COLOR
  {
    float3 normalDirection = normalize(input.normalDir);
    float3 viewDirection = normalize(_WorldSpaceCameraPos - input.posWorld.xyz);
    float3 lightDirection;
    float attenuation;

    if (0.0 == _WorldSpaceLightPos0.w)
    // directional light?
    {
      attenuation = 1.0; // no attenuation
      lightDirection = normalize(_WorldSpaceLightPos0.xyz);
    }
    else
    // point or spot light
    {
      float3 vertexToLightSource = _WorldSpaceLightPos0.xyz - input.posWorld.xyz;
      float distance = length(vertexToLightSource);
      attenuation = 1.0 / distance; // linear attenuation
      lightDirection = normalize(vertexToLightSource);
    }

    float3 ambientLighting =
      UNITY_LIGHTMODEL_AMBIENT.rgb * _Color.rgb;

    float3 diffuseReflection =
      attenuation * _LightColor0.rgb * _Color.rgb
      * max(0.0, dot(normalDirection, lightDirection));

    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)
        // terminator optimization
        * dot(lightDirection, normalDirection);
    }

    float4 final = float4(
    ambientLighting   
    +
    diffuseReflection
    +
    specularReflection
    , 1.0);

    return final;
  }
  ENDCG
} //END PASS

If there are more than one light source, we can add one more pass for the additional lighting effects:

Pass{
    Tags{ "LightMode" = "ForwardAdd" }
    // pass for additional light sources
    Blend One One
    // additive blending

    //...
    // same with the first pass

    float4 final = float4(
    diffuseReflection
    +
    specularReflection
    , 1.0);
    // no need for ambient lighting any more!

    //...
    // same with the first pass

} //END PASS

Final Result

This is the testing effect:

Ambient only.


Diffuse only.


Specular only.


Final.


However, if statement is not optimal in shader. This is because shader is running on GPU and GPU has a highly parallel structure. I will document the optimization for the shader in future post.