Workshop 3: Specular Lighting

For simulating shiny/polished surfaces the algorithm must also take into account the location of the viewer.

There are several lighting models for simulating specular reflectance. We will use the Phong specular lighting model developed by Bui Tong Phong. We will use a reflection vector that contains the direction of the light after it has bounced off the surface. This vector is calculated from the light direction vector and the normal of the surface:


The reflection vector

This reflection vector R is compared to the view direction vector. If the angles are similar, the surface will be lit brightly, if they are far off, no specular light is reflected from the surface into the camera and only the diffuse and ambient light will be visible on this pixel.

The angle between the reflection vector and view vector.

The reflection vector R is calculated with the following formula:

R = 2 * (N · L) * N ­ L

The view vector V is calculated by subtracting the vector world position from the camera position:

V = Camera Position - Vertex Position

This results in a vector pointing from the vertex to the camera (see "Subtracting Vectors" in Appendix B). As with diffuse lighting, we will use the dot product to calculate the cosine of the angle difference between the two vectors. This time we need the difference between the view vector and the reflection vector. We then raise the result to a power n which makes the highlight harder or softer.

Specular Light = (R · V)n

Our entire equation is now:

Final Color = (Diffuse Light + Ambient Light + Specular Light) * Diffuse Color

Implementation

// Tweakables: 
static const float AmbientIntensity  = 1.0f; // The intensity of the ambient light. 
static const float DiffuseIntensity = 1.0f;  // The intensity of the diffuse light. 
static const float SpecularIntensity = 2.0f; // The intensity of the specular light. 
static const float SpecularPower = 8.0f;     // The specular power, used as 'glossiness' factor. 
static const float4 SunColor = {0.9f, 0.9f, 0.5f, 1.0f}; // Color vector of the sunlight. 

// Application fed data: 
const float4x4 matWorldViewProj; // World*view*projection matrix. 
const float4x4 matWorld;   // World matrix. 
const float4 vecAmbient;   // Ambient color. 
const float4 vecSunDir;    // The sun light direction vector. 
const float4 vecViewPos;   // View position. 
texture entSkin1;  
  
// Color map. 
sampler ColorMapSampler = sampler_state // Color map sampler. 
{   
  Texture = <entSkin1>;   
  MipFilter = Linear;   // required for mipmapping
}; // Vertex Shader: void SpecularVS( in float4 InPos: POSITION, in float3 InNormal: NORMAL, in float2 InTex: TEXCOORD0, out float4 OutPos: POSITION, out float2 OutTex: TEXCOORD0, out float3 OutNormal: TEXCOORD1, out float3 OutViewDir: TEXCOORD2) { // Transform the vertex from object space to clip space: OutPos = mul(InPos, matWorldViewProj); // Transform the normal from object space to world space: OutNormal = normalize(mul(InNormal, matWorld));; // Pass the texture coordinate to the pixel shader: OutTex = InTex; // Calculate a vector from the vertex to the view: OutViewDir = vecViewPos - mul(InPos, matWorld); } // Pixel Shader: float4 SpecularPS( in float2 InTex: TEXCOORD0, in float3 InNormal: TEXCOORD1, in float4 InViewDir: TEXCOORD2) : COLOR { // Calculate the ambient term: float4 Ambient = AmbientIntensity * vecAmbient; // Calculate the diffuse term: InNormal = normalize(InNormal); float4 Diffuse = DiffuseIntensity * SunColor * saturate(dot(-vecSunDir, InNormal)); // Fetch the pixel color from the color map: float4 Color = tex2D(ColorMapSampler, InTex); // Calculate the reflection vector: float3 R = normalize(2 * dot(InNormal, -vecSunDir) * InNormal + vecSunDir); // Calculate the speculate component: float Specular = pow(saturate(dot(R, normalize(InViewDir))), SpecularPower) * SpecularIntensity; // Calculate final color: return (Ambient + Diffuse + Specular) * Color; } // Technique: technique SpecularTechnique { pass P0 { VertexShader = compile vs_2_0 SpecularVS(); PixelShader = compile ps_2_0 SpecularPS(); } }
We have added two new variables: SpecularIntensity and SpecularPower. These will be used in the pixel shader as an intensity and "glossiness" factor. We have also added a new constant that is passed by the engine. vecViewPos is the position of the camera in the world.
// Vertex Shader: 
void SpecularVS( 
  in float4 InPos: POSITION, 
  in float3 InNormal: NORMAL, 
  in float2 InTex: TEXCOORD0, 
  out float4 OutPos: POSITION, 
  out float2 OutTex: TEXCOORD0, 
  out float3 OutNormal: TEXCOORD1, 
  out float3 OutViewDir: TEXCOORD2) 
{ 
// Transform the vertex from object space to clip space: 
  OutPos = mul(InPos, matWorldViewProj); 
// Transform the normal from object space to world space: 
  OutNormal = normalize(mul(InNormal, matWorld));; 
// Pass the texture coordinate to the pixel shader: 
  OutTex = InTex; 
// Calculate a vector from the vertex to the view: 
  OutViewDir = vecViewPos - mul(InPos, matWorld); 
} 
We have added one output parameter to the vertex shader which will contain a vector pointing from the vertex to the view; we call it "OutViewDir". Like before, we transform the vertex and normal and pass the texture coordinate.

The specular highlights are dependent on the view direction so we have to calculate a vector that gives the view direction. We know the world position of the view (vecViewPos) and we know the objectspace position of the vertex. To calculate the view direction vector we need the worldspace position of the vertex, so we transform the vertex position by multiplying it with the world matrix. Subtracting the vertex world position vector from the view position vector results in a direction vector pointing to the view from the vertex (see "Subtracting Vectors" in Appendix B).
// Pixel Shader: 
float4 SpecularPS( 
  in float2 InTex: TEXCOORD0, 
  in float3 InNormal: TEXCOORD1, 
  in float4 InViewDir: TEXCOORD2) : COLOR 
{ 
// Calculate the ambient term: 
  float4 Ambient = AmbientIntensity * vecAmbient; 
// Calculate the diffuse term: 
  InNormal = normalize(InNormal); 
  float4 Diffuse = DiffuseIntensity * SunColor * saturate(dot(-vecSunDir, InNormal)); 
// Fetch the pixel color from the color map: 
  float4 Color = tex2D(ColorMapSampler, InTex); 
// Calculate the reflection vector: 
  float3 R = normalize(2 * dot(InNormal, -vecSunDir) * InNormal + vecSunDir); 
// Calculate the speculate component: 
  float Specular = pow(saturate(dot(R, normalize(InViewDir))), SpecularPower) * SpecularIntensity; 
// Calculate final color: 
  return (Ambient + Diffuse + Specular) * Color; 
} 

We receive the view direction vector in TEXCOORD2 where we put it in the pixel shader. The ambient and diffuse components are calculated like before. We calculate the reflection vector R with the formula discussed above. The specular component is calculated by taking the dot product of the reflection vector and the view direction, raised to the power given by the SpecularPower variable making the highlight harder or softer. The result is multiplied by SpecularIntensity to make the highlight brighter or darker.

We once again add up all the light, now including specular and multiply it by the color which is read from the color map. Once again, start SED, open speculardemo.c and run it:



By adding specular highlights, we see a view dependent highlight making the material look shiny and polished. To make the effect more clear we've added a line to the script that lets the object slowly rotate.

Next: Normalmapping