GLSL Programming/Unity/Brushed Metal

This tutorial covers anisotropic specular highlights.

Brushed aluminium. Note the form of the specular highlights, which is far from being round.

It is one of several tutorials about lighting that go beyond the Phong reflection model. However, it is based on lighting with the Phong reflection model as described in Section “Specular Highlights” (for per-vertex lighting) and Section “Smooth Specular Highlights” (for per-pixel lighting). If you haven't read those tutorials yet, you should read them first.

While the Phong reflection model is reasonably good for paper, plastics, and some other materials with isotropic reflection (i.e. round highlights), this tutorial looks specifically at materials with anisotropic reflection (i.e. non-round highlights), for example brushed aluminium as in the photo to the left.

In addition to most of the vectors used by the Phong reflection model, we require the normalized halfway vector H, which is the direction exactly between the direction to the viewer V and the direction to the light source L.

Ward's Model of Anisotropic Reflection

edit

Gregory Ward published a suitable model of anisotropic reflection in his work “Measuring and Modeling Anisotropic Reflection”, Computer Graphics (SIGGRAPH ’92 Proceedings), pp. 265–272, July 1992. (A copy of the paper is available online.) This model describes the reflection in terms of a BRDF (bidrectional reflectance distribution function), which is a four-dimensional function that describes how a light ray from any direction is reflected into any other direction. His BRDF model consists of two terms: a diffuse reflectance term, which is  , and a more complicated specular reflectance term.

Let's have a look at the diffuse term   first:   is just a constant (about 3.14159) and   specifies the diffuse reflectance. In principle, a reflectance for each wave length is necessary; however, usually one reflectance for each of the three color components (red, green, and blue) is specified. If we include the constant  ,   just represents the diffuse material color  , which we have first seen in Section “Diffuse Reflection” but which also appears in the Phong reflection model (see Section “Specular Highlights”). You might wonder why the factor max(0, L·N) doesn't appear in the BRDF. The answer is that the BRDF is defined in such a way that this factor is not included in it (because it isn't really a property of the material) but it should be multiplied with the BRDF when doing any lighting computation.

Thus, in order to implement a given BRDF for opaque materials, we have to multiply all terms of the BRDF with max(0, L·N) and − unless we want to implement physically correct lighting − we can replace any constant factors by user-specified colors, which usually are easier to control than physical quantities.

For the specular term of his BRDF model, Ward presents an approximation in equation 5b of his paper. I adapted it slightly such that it uses the normalized surface normal vector N, the normalized direction to the viewer V, the normalized direction to the light source L, and the normalized halfway vector H which is (V + L) / |V + L|. Using these vectors, Ward's approximation for the specular term becomes:

 

Here,   is the specular reflectance, which describes the color and intensity of the specular highlights;   and   are material constants that describe the shape and size of the highlights. Since all these variables are material constants, we can combine them in one constant  . Thus, we get a slightly shorter version:

 

Remember that we still have to multiply this BRDF term with L·N when implementing it in a shader and set it to 0 if L·N is less than 0. Furthermore, it should also be 0 if V·N is less than 0, i.e., if we are looking at the surface from the “wrong” side.

There are two vectors that haven't been described yet: T and B. T is the brush direction on the surface and B is orthogonal to T but also on the surface. Unity provides us with a tangent vector on the surface as a vertex attribute (see Section “Debugging of Shaders”), which we will use as the vector T. Computing the cross product of N and T generates a vector B, which is orthogonal to N and T, as it should be.

Implementation of Ward's BRDF Model

edit

We base our implementation on the shader for per-pixel lighting in Section “Smooth Specular Highlights”. We need another varying variable tangentDirection for the tangent vector T (i.e. the brush direction) and we compute two more directions: halfwayVector for the halfway vector H and binormalDirection for the binormal vector B. The properties are _Color for  , _SpecColor for  , _AlphaX for  , and _AlphaY for  .

The fragment shader is then very similar to the version in Section “Smooth Specular Highlights” except that it normalizes tangentDirection, computes halfwayVector and binormalDirection, and implements a different equation for the specular part. Furthermore, this shader computs the dot product L·N only once and stores it in dotLN such that it can be reused without having to recompute it. It looks like this:

        #ifdef FRAGMENT
         
         void main()
         {
            vec3 normalDirection = normalize(varyingNormalDirection);
            vec3 tangentDirection = normalize(varyingTangentDirection);

            vec3 viewDirection = 
               normalize(_WorldSpaceCameraPos - vec3(position));
            vec3 lightDirection;
            float attenuation;

            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               attenuation = 1.0; // no attenuation
               lightDirection = normalize(vec3(_WorldSpaceLightPos0));
            } 
            else // point or spot light
            {
               vec3 vertexToLightSource = 
                  vec3(_WorldSpaceLightPos0 - position);
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
            }
            
            vec3 halfwayVector = 
               normalize(lightDirection + viewDirection);
	    vec3 binormalDirection = 
               cross(normalDirection, tangentDirection);
            float dotLN = dot(lightDirection, normalDirection); 
               // compute this dot product only once
            
            vec3 ambientLighting = vec3(gl_LightModel.ambient) 
               * vec3(_Color);

            vec3 diffuseReflection = attenuation * vec3(_LightColor0) 
               * vec3(_Color) * max(0.0, dotLN);
            
            vec3 specularReflection;
            if (dotLN < 0.0) // light source on the wrong side?
            {
               specularReflection = vec3(0.0, 0.0, 0.0); 
                  // no specular reflection
            }
            else // light source on the right side
            {
               float dotHN = dot(halfwayVector, normalDirection);
               float dotVN = dot(viewDirection, normalDirection);
               float dotHTAlphaX = 
                  dot(halfwayVector, tangentDirection) / _AlphaX;
               float dotHBAlphaY = dot(halfwayVector, 
                  binormalDirection) / _AlphaY;

               specularReflection = attenuation * vec3(_SpecColor) 
                  * sqrt(max(0.0, dotLN / dotVN)) 
                  * exp(-2.0 * (dotHTAlphaX * dotHTAlphaX 
                  + dotHBAlphaY * dotHBAlphaY) / (1.0 + dotHN));
            }

            gl_FragColor = vec4(ambientLighting 
               + diffuseReflection + specularReflection, 1.0);
         }
         
         #endif

Note the term sqrt(max(0, dotLN / dotVN)) which resulted from   multiplied with  . This makes sure that everything is greater than 0.

Complete Shader Code

edit

The complete shader code just defines the appropriate properties and the tangent attribute. Also, it requires a second pass with additive blending but without ambient lighting for additional light sources.

Shader "GLSL anisotropic per-pixel lighting" {
   Properties {
      _Color ("Diffuse Material Color", Color) = (1,1,1,1) 
      _SpecColor ("Specular Material Color", Color) = (1,1,1,1) 
      _AlphaX ("Roughness in Brush Direction", Float) = 1.0
      _AlphaY ("Roughness orthogonal to Brush Direction", Float) = 1.0
   }
   SubShader {
      Pass {	
         Tags { "LightMode" = "ForwardBase" } 
            // pass for ambient light and first light source

         GLSLPROGRAM

         // User-specified properties
         uniform vec4 _Color; 
         uniform vec4 _SpecColor; 
         uniform float _AlphaX;
         uniform float _AlphaY;

         // The following built-in uniforms (except _LightColor0) 
         // are also defined in "UnityCG.glslinc", 
         // i.e. one could #include "UnityCG.glslinc" 
         uniform vec3 _WorldSpaceCameraPos; 
            // camera position in world space
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 _WorldSpaceLightPos0; 
            // direction to or position of light source
         uniform vec4 _LightColor0; 
            // color of light source (from "Lighting.cginc")
         
         varying vec4 position; 
            // position of the vertex (and fragment) in world space 
         varying vec3 varyingNormalDirection; 
            // surface normal vector in world space
         varying vec3 varyingTangentDirection; 
            // brush direction in world space

         #ifdef VERTEX

         attribute vec4 Tangent; // tangent vector provided 
            // by Unity (used as brush direction)
         
         void main()
         {				
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object; // unity_Scale.w 
               // is unnecessary because we normalize vectors
            
            position = modelMatrix * gl_Vertex;
            varyingNormalDirection = normalize(vec3(
               vec4(gl_Normal, 0.0) * modelMatrixInverse));
            varyingTangentDirection = normalize(vec3(
               modelMatrix * vec4(vec3(Tangent), 0.0)));

            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
         
         #endif

         #ifdef FRAGMENT
         
         void main()
         {
            vec3 normalDirection = normalize(varyingNormalDirection);
            vec3 tangentDirection = normalize(varyingTangentDirection);

            vec3 viewDirection = 
               normalize(_WorldSpaceCameraPos - vec3(position));
            vec3 lightDirection;
            float attenuation;

            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               attenuation = 1.0; // no attenuation
               lightDirection = normalize(vec3(_WorldSpaceLightPos0));
            } 
            else // point or spot light
            {
               vec3 vertexToLightSource = 
                  vec3(_WorldSpaceLightPos0 - position);
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
            }
            
            vec3 halfwayVector = 
               normalize(lightDirection + viewDirection);
	    vec3 binormalDirection = 
               cross(normalDirection, tangentDirection);
            float dotLN = dot(lightDirection, normalDirection); 
               // compute this dot product only once
            
            vec3 ambientLighting = 
               vec3(gl_LightModel.ambient) * vec3(_Color);

            vec3 diffuseReflection = attenuation * vec3(_LightColor0) 
               * vec3(_Color) * max(0.0, dotLN);
            
            vec3 specularReflection;
            if (dotLN < 0.0) // light source on the wrong side?
            {
               specularReflection = vec3(0.0, 0.0, 0.0); 
                  // no specular reflection
            }
            else // light source on the right side
            {
               float dotHN = dot(halfwayVector, normalDirection);
               float dotVN = dot(viewDirection, normalDirection);
               float dotHTAlphaX = 
                  dot(halfwayVector, tangentDirection) / _AlphaX;
               float dotHBAlphaY = 
                  dot(halfwayVector, binormalDirection) / _AlphaY;

               specularReflection = attenuation * vec3(_SpecColor) 
                  * sqrt(max(0.0, dotLN / dotVN)) 
                  * exp(-2.0 * (dotHTAlphaX * dotHTAlphaX 
                  + dotHBAlphaY * dotHBAlphaY) / (1.0 + dotHN));
            }

            gl_FragColor = vec4(ambientLighting 
               + diffuseReflection + specularReflection, 1.0);
         }
         
         #endif

         ENDGLSL
      }

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


         GLSLPROGRAM

         // User-specified properties
         uniform vec4 _Color; 
         uniform vec4 _SpecColor; 
         uniform float _AlphaX;
         uniform float _AlphaY;

         // The following built-in uniforms (except _LightColor0) 
         // are also defined in "UnityCG.glslinc", 
         // i.e. one could #include "UnityCG.glslinc" 
         uniform vec3 _WorldSpaceCameraPos; 
            // camera position in world space
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 _WorldSpaceLightPos0; 
            // direction to or position of light source
         uniform vec4 _LightColor0; 
            // color of light source (from "Lighting.cginc")
         
         varying vec4 position; 
            // position of the vertex (and fragment) in world space 
         varying vec3 varyingNormalDirection; 
            // surface normal vector in world space
         varying vec3 varyingTangentDirection; 
            // brush direction in world space

         #ifdef VERTEX

         attribute vec4 Tangent; // tangent vector provided 
            // by Unity (used as brush direction)
         
         void main()
         {				
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object; // unity_Scale.w 
               // is unnecessary because we normalize vectors
            
            position = modelMatrix * gl_Vertex;
            varyingNormalDirection = normalize(vec3(
               vec4(gl_Normal, 0.0) * modelMatrixInverse));
            varyingTangentDirection = normalize(vec3(
               modelMatrix * vec4(vec3(Tangent), 0.0)));

            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
         
         #endif

         #ifdef FRAGMENT
         
         void main()
         {
            vec3 normalDirection = normalize(varyingNormalDirection);
            vec3 tangentDirection = normalize(varyingTangentDirection);

            vec3 viewDirection = 
               normalize(_WorldSpaceCameraPos - vec3(position));
            vec3 lightDirection;
            float attenuation;

            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               attenuation = 1.0; // no attenuation
               lightDirection = normalize(vec3(_WorldSpaceLightPos0));
            } 
            else // point or spot light
            {
               vec3 vertexToLightSource = 
                  vec3(_WorldSpaceLightPos0 - position);
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
            }
            
            vec3 halfwayVector = 
               normalize(lightDirection + viewDirection);
	    vec3 binormalDirection = 
               cross(normalDirection, tangentDirection);
            float dotLN = dot(lightDirection, normalDirection); 
               // compute this dot product only once
            
            vec3 diffuseReflection = attenuation * vec3(_LightColor0) 
               * vec3(_Color) * max(0.0, dotLN);
            
            vec3 specularReflection;
            if (dotLN < 0.0) // light source on the wrong side?
            {
               specularReflection = vec3(0.0, 0.0, 0.0); 
                  // no specular reflection
            }
            else // light source on the right side
            {
               float dotHN = dot(halfwayVector, normalDirection);
               float dotVN = dot(viewDirection, normalDirection);
               float dotHTAlphaX = 
                  dot(halfwayVector, tangentDirection) / _AlphaX;
               float dotHBAlphaY = 
                  dot(halfwayVector, binormalDirection) / _AlphaY;

               specularReflection = attenuation * vec3(_SpecColor) 
                  * sqrt(max(0.0, dotLN / dotVN)) 
                  * exp(-2.0 * (dotHTAlphaX * dotHTAlphaX 
                  + dotHBAlphaY * dotHBAlphaY) / (1.0 + dotHN));
            }

            gl_FragColor = 
               vec4(diffuseReflection + specularReflection, 1.0);
         }
         
         #endif

         ENDGLSL
      }
   } 
   // The definition of a fallback shader should be commented out 
   // during development:
   // Fallback "Specular"
}

Summary

edit

Congratulations, you finished a rather advanced tutorial! We have seen:

  • What a BRDF (bidirectional reflectance distribution function) is.
  • What Ward's BRDF model for anisotropic reflection is.
  • How to implement Ward's BRDF model.

Further Reading

edit

If you still want to know more

  • about lighting with the Phong reflection model, you should read Section “Specular Highlights”.
  • about per-pixel lighting (i.e. Phong shading), you should read Section “Smooth Specular Highlights”.
  • about Ward's BRDF model, you should read his article “Measuring and Modeling Anisotropic Reflection”, Computer Graphics (SIGGRAPH ’92 Proceedings), pp. 265–272, July 1992. (A copy of the paper is available online.) Or you could read Section 14.3 of the book “OpenGL Shading Language” (3rd edition) by Randi Rost and others, published 2009 by Addison-Wesley, or Section 8 in the Lighting chapter of the book “Programming Vertex, Geometry, and Pixel Shaders” (2nd edition, 2008) by Wolfgang Engel, Jack Hoxley, Ralf Kornmann, Niko Suni, and Jason Zink (which is available online.)



< GLSL Programming/Unity

Unless stated otherwise, all example source code on this page is granted to the public domain.