GLSL Programming/GLUT/Specular Highlights

“Apollo the Lute Player” (Badminton House version) by Michelangelo Merisi da Caravaggio, ca. 1596.

This tutorial covers per-vertex lighting (also known as Gouraud shading) using the Phong reflection model.

It extends the shader code in the tutorial on diffuse reflection by two additional terms: ambient lighting and specular reflection. Together, the three terms constitute the Phong reflection model. If you haven't read the tutorial on diffuse reflection, this would be a very good opportunity to read it.

Ambient LightEdit

Consider the painting by Caravaggio to the left. While large parts of the white shirt are in shadows, no part of it is completely black. Apparently there is always some light being reflected from walls and other objects to illuminate everything in the scene — at least to a certain degree. In the Phong reflection model, this effect is taken into account by ambient lighting, which depends on a general ambient light intensity I_\text{ambient light} and the material color k_\text{diffuse} for diffuse reflection. In an equation for the intensity of ambient lighting I_\text{ambient}:

I_\text{ambient} = I_\text{ambient light}\,k_\text{diffuse}

Analogously to the equation for diffuse reflection in the tutorial on diffuse reflection, this equation can also be interpreted as a vector equation for the red, green, and blue components of light.

In the OpenGL compatibility profile, this color is available as gl_LightModel.ambient, one of the pre-defined uniforms of mentioned in the tutorial on shading in view space. We'll specify it as scene_ambient.

The computation of the specular reflection requires the surface normal vector N, the direction to the light source L, the reflected direction to the light source R, and the direction to the viewer V.

Specular HighlightsEdit

If you have a closer look at Caravaggio's painting, you will see several specular highlights: on the nose, on the hair, on the lips, on the lute, on the violin, on the bow, on the fruits, etc. The Phong reflection model includes a specular reflection term that can simulate such highlights on shiny surfaces; it even includes a parameter n_\text{shininess} to specify a shininess of the material. The shininess specifies how small the highlights are: the shinier, the smaller the highlights.

A perfectly shiny surface will reflect light from the light source only in the geometrically reflected direction R. For less than perfectly shiny surfaces, light is reflected to directions around R: the smaller the shininess, the wider the spreading. Mathematically, the normalized reflected direction R is defined by:

\mathbf{R} = 2 \mathbf{N} (\mathbf{N}\cdot\mathbf{L}) - \mathbf{L}

for a normalized surface normal vector N and a normalized direction to the light source L. In GLSL, the function vec3 reflect(vec3 I, vec3 N) (or vec4 reflect(vec4 I, vec4 N)) computes the same reflected vector but for the direction I from the light source to the point on the surface. Thus, we have to negate our direction L to use this function.

The specular reflection term computes the specular reflection in the direction of the viewer V. As discussed above, the intensity should be large if V is close to R, where “closeness” is parametrized by the shininess n_\text{shininess}. In the Phong reflection model, the cosine of the angle between R and V to the n_\text{shininess}-th power is used to generate highlights of different shininess. Similarly to the case of the diffuse reflection, we should clamp negative cosines to 0. Furthermore, the specular term requires a material color k_\text{specular} for the specular reflection, which is usually just white such that all highlights have the color of the incoming light I_\text{incoming}. For example, all highlights in Caravaggio's painting are white. The specular term of the Phong reflection model is then:

I_\text{specular} = I_\text{incoming}\,k_\text{specular} \max(0, \mathbf{R}\cdot \mathbf{V})^{n_\text{shininess}}

Analogously to the case of the diffuse reflection, the specular term should be ignored if the light source is on the “wrong” side of the surface; i.e., if the dot product N·L is negative.

Shader CodeEdit

The shader code for the ambient lighting is straightforward with a component-wise vector-vector product:

  vec3 ambientLighting = vec3(scene_ambient) * vec3(mymaterial.ambient);

For the implementation of the specular reflection, we require the direction to the viewer in world space, which we can compute as the difference between the camera position and the vertex position (both in world space). The camera position in view space is reasonably simple: it is at the origin (0,0,0) in view space, and can be transformed to world space by applying the inversed View matrix; see “Vertex Transformations”. The vertex position can be transformed to world space as discussed in the tutorial on diffuse reflection. The equation of the specular term in world space could then be implemented like this:

  vec3 viewDirection = normalize(vec3(v_inv * vec4(0.0, 0.0, 0.0, 1.0) - m * v_coord));
 
  vec3 specularReflection;
  if (dot(normalDirection, lightDirection) < 0.0) // light source on the wrong side?
    {
      specularReflection = vec3(0.0, 0.0, 0.0); // no specular reflection
    }
  else
    {
      specularReflection = attenuation * vec3(light0.specular) * vec3(mymaterial.specular)
	* pow(max(0.0, dot(reflect(-lightDirection, normalDirection), viewDirection)),
	      mymaterial.shininess);
    }

This code snippet uses the same variables as the shader code in the tutorial on diffuse reflection and additionally the variables mymaterial.specular and mymaterial.shininess. pow(a, b) computes a^b.

If the ambient lighting and the specular reflection is added the full vertex shader of the tutorial on diffuse reflection, it looks like this:

attribute vec4 v_coord;
attribute vec3 v_normal;
uniform mat4 m, v, p;
uniform mat3 m_3x3_inv_transp;
uniform mat4 v_inv;
varying vec4 color;
 
struct lightSource
{
  vec4 position;
  vec4 diffuse;
  vec4 specular;
  float constantAttenuation, linearAttenuation, quadraticAttenuation;
  float spotCutoff, spotExponent;
  vec3 spotDirection;
};
lightSource light0 = lightSource(
  vec4(0.0,  1.0,  2.0, 1.0),
  vec4(1.0,  1.0,  1.0, 1.0),
  vec4(1.0,  1.0,  1.0, 1.0),
  0.0, 1.0, 0.0,
  180.0, 0.0,
  vec3(0.0, 0.0, 0.0)
);
vec4 scene_ambient = vec4(0.2, 0.2, 0.2, 1.0);
 
struct material
{
  vec4 ambient;
  vec4 diffuse;
  vec4 specular;
  float shininess;
};
material mymaterial = material(
  vec4(0.2, 0.2, 0.2, 1.0),
  vec4(1.0, 0.8, 0.8, 1.0),
  vec4(1.0, 1.0, 1.0, 1.0),
  5.0
);
 
void main(void)
{
  mat4 mvp = p*v*m;
  vec3 normalDirection = normalize(m_3x3_inv_transp * v_normal);
  vec3 viewDirection = normalize(vec3(v_inv * vec4(0.0, 0.0, 0.0, 1.0) - m * v_coord));
  vec3 lightDirection;
  float attenuation;
 
  if (light0.position.w == 0.0) // directional light
    {
      attenuation = 1.0; // no attenuation
      lightDirection = normalize(vec3(light0.position));
    }
  else // point or spot light (or other kind of light)
    {
      vec3 vertexToLightSource = vec3(light0.position - m * v_coord);
      float distance = length(vertexToLightSource);
      lightDirection = normalize(vertexToLightSource);
      attenuation = 1.0 / (light0.constantAttenuation
			   + light0.linearAttenuation * distance
			   + light0.quadraticAttenuation * distance * distance);
 
      if (light0.spotCutoff <= 90.0) // spotlight
	{
	  float clampedCosine = max(0.0, dot(-lightDirection, normalize(light0.spotDirection)));
	  if (clampedCosine < cos(radians(light0.spotCutoff))) // outside of spotlight cone
	    {
	      attenuation = 0.0;
	    }
	  else
	    {
              attenuation = attenuation * pow(clampedCosine, light0.spotExponent);
	    }
	}
    }
 
  vec3 ambientLighting = vec3(scene_ambient) * vec3(mymaterial.ambient);
 
  vec3 diffuseReflection = attenuation
    * vec3(light0.diffuse) * vec3(mymaterial.diffuse)
    * max(0.0, dot(normalDirection, lightDirection));
 
  vec3 specularReflection;
  if (dot(normalDirection, lightDirection) < 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
    {
      specularReflection = attenuation * vec3(light0.specular) * vec3(mymaterial.specular)
	* pow(max(0.0, dot(reflect(-lightDirection, normalDirection), viewDirection)),
	      mymaterial.shininess);
    }
 
  color = vec4(ambientLighting + diffuseReflection + specularReflection, 1.0);
  gl_Position = mvp * v_coord;
}

The fragment shader is still:

varying vec4 color;
 
void main(void)
{
  gl_FragColor = color;
}

In your C++ code, update the v_inv uniform:

  glm::mat4 v_inv = glm::inverse(world2camera);
  glUniformMatrix4fv(uniform_v_inv, 1, GL_FALSE, glm::value_ptr(v_inv));

SummaryEdit

Congratulation, you just learned how to implement the Phong reflection model. In particular, we have seen:

  • What the ambient lighting in the Phong reflection model is.
  • What the specular reflection term in the Phong reflection model is.
  • How these terms can be implemented in GLSL.

Further ReadingEdit

If you still want to know more


< GLSL Programming/GLUT

Unless stated otherwise, all example source code on this page is granted to the public domain.
Back to OpenGL Programming - Lighting section Back to GLSL Programming - GLUT section
Last modified on 19 May 2012, at 19:57