In which you add shiny highlights to your surfaces.
Only rough surfaces can be rendered realistically with just diffuse lighting. Very smooth surfaces reflect light in a more concentrated direction. These bright spots of concentrated light are specular highlights. To model the illumination of smooth surfaces, you add a specular term.
When a ray of light reaches a smooth surface at at angle \(a\), it bounces off the surface at the same angle, but in a direction that is mirrored about the surface's normal. If the viewer is aligned with the direction of reflection, they will see the highlight. As with the diffuse term, the cosine between the two vectors measures their alignment, but it's faster to use the dot product:
$$ \mathrm{specularity} = \mathrm{reflectDirection} \cdot \mathrm{eyeDirection} $$
Again, you don't want to allow the specularity factor to go below 0:
$$ \mathrm{specularity} = \max(0, \mathrm{reflectDirection} \cdot \mathrm{eyeDirection}) $$
The specularity will be a proportion between 0 and 1. To control the size of the highlights, you raise this value to some power:
$$ \mathrm{specularity} = \max(0, \mathrm{reflectDirection} \cdot \mathrm{eyeDirection})^\mathrm{shininess} $$
The shininess affects how quickly the curve drops to 0. The larger the number, the more quickly the specularity curve plummets and the smaller the highlight. Consider this profile of five different shininess values:
The light direction can be flipped about the normal using the builtin reflect
function in GLSL. It expects the incident vector to be pointing from the light source to the fragment, rather than from the fragment to the light source. The lightDirection
computed earlier must therefore be negated:
vec3 reflectDirection = reflect(-lightDirection, normal);
The eye direction is a vector from the fragment to the eye. Since you're lighting in eye space, the eye is at the origin, and this subtraction yields the eye direction:
vec3 eyeDirection = vec3(0.0) - mixPosition;
Subtracting from the origin is the same as negating, so you can simplify this statement:
vec3 eyeDirection = -mixPosition;
Recall that the vectors must have unit length if you're going to use the dot product as a measure of their alignment. The reflect direction will already be normalized if the light direction is normalized. However, the eye direction must be normalized:
vec3 eyeDirection = normalize(-mixPosition);
This fragment shader calculates the specular term and adds it to the ambient and diffuse terms calculated earlier:
uniform vec3 specularColor;
uniform float shininess;
// ...
void main() {
// ...
vec3 eyeDirection = normalize(-mixPosition);
vec3 reflectDirection = reflect(-lightDirection, normal);
float specularity = pow(max(0.0, dot(reflectDirection, eyeDirection)), shininess);
vec3 specular = specularity * specularColor;
vec3 rgb = ambient + diffuse + specular;
fragmentColor = vec4(rgb, 1.0);
}
Note that the specular term doesn't reference the material's albedo as did the diffuse and ambient terms. Albedo describes how much light a surface reflects. A surface with albedo \(\begin{bmatrix}0.5&1&0\end{bmatrix}\) reflects half of the red light, all of the green, and none of the blue. A material with specular highlights is very reflective and doesn't absorb any incoming light, so the albedo is omitted.
The combination of ambient, diffuse, and specular terms is called Phong illumination. This model was described in 1975 by graduate student Bui Tuong Phong at the University of Utah. Phong illumination is used in this renderer:
Try changing the shininess and the specular color.
The Phong illumination model cuts off the specular contribution when the angle between the reflection direction and eye direction is greater than 90 degrees. In certain situations, such as when the sun is setting over a body of water, this cutoff leads to an unrealistic lack of specular highlights. The Phong model was adapted by graphics researcher Jim Blinn to tolerate a wider alignment.
Blinn's model replaces the reflect direction with a vector that points halfway between the eye direction and the light direction. The degree of alignment between this half direction and the normal determines the specularity.
Note that Blinn's modifications are not grounded in physical reality. In interactive computer graphics, real isn't nearly as important as fast and convincing.
This fragment shader uses the Blinn-Phong illumination model:
uniform vec3 specularColor;
uniform float shininess;
// ...
void main() {
// ...
vec3 eyeDirection = normalize(-mixPosition);
vec3 halfDirection = normalize(eyeDirection + lightDirection);
float specularity = pow(max(0.0, dot(halfDirection, normal)), shininess);
vec3 specular = specularity * specularColor;
vec3 rgb = ambient + diffuse + specular;
fragmentColor = vec4(rgb, 1.0);
}
This revised model is commonly used and was the default in OpenGL before shaders were added. Now that shaders have been added, there is no default. You get to pick your lighting model. Pick means implement.
This renderer uses the Blinn-Phong illumination model:
Contrast this renderer to the Phong renderer above.