9.5 - Combining Ambient, Diffuse, and Specular Lighting¶
Now that you understand the three basic light models: ambient, diffuse, and specular, we can combine the three models into a general model for realistic light modeling. When we combine the light models the only question is how the three separate colors can be combined into a single color. The answer turns out to be very simple. Since colors from light are additive, all we have to do is add the three colors together. Besides the addition of the colors, there is no new math in this lesson.
An Ambient, Diffuse, and Specular Lighting Model¶
For a point light source that is inside a scene, we need the following data to model the light:
- The light’s position
- The light’s color
- The ambient percentages for background light
Light interacts with the surfaces of an object. To model light we also need basic properties of the surfaces, such as:
- The surface’s color
- The surface’s location
- The surface’s orientation (its normal vector)
- The surface’s shininess exponent
The Math for a Combined Lighting Model¶
There is no new math to discuss. The ambient, diffuse, and specular light calculations are performed as we discussed in the previous lessons and then the resulting colors are added together to get the final color.
A WebGL Demo Program¶
Experiment with the following WebGL program. There are many values to experiment with, so take your time and figure out how the settings interact with each other.
Manipulate the properties of a light source and a camera.
The left canvas shows the relative location of the light source, the camera, and an object.The right canvas shows the scene from the camera's vantage point with the light source used to render the model using ambient, diffuse, and specular lighting combined.
Manipulate the camera: | Ambient light: | Light properties: |
eye (0.0, 0.0, 5.0) | ambient percentages (0.2, 0.2, 0.2) | light location (3.0, 3.0, 3.0) |
X: -5.0 +5.0 | Red : 0.0 1.0 | X: -5.0 +5.0 |
Y: -5.0 +5.0 | Green: 0.0 1.0 | Y: -5.0 +5.0 |
Z: -5.0 +5.0 | Blue : 0.0 1.0 | Z: -5.0 +5.0 |
center (0.0, 0.0, 0.0) | ambient percentage = 0.2 0.0 1.0 |
light color = (1.0, 1.0, 1.0) |
X: -5.0 +5.0 | Red: 0.0 1.0 | |
Y: -5.0 +5.0 | Green: 0.0 1.0 | |
Z: -5.0 +5.0 | Blue: 0.0 1.0 | |
shininess = 30.0 0.1 128.0 |
Open this webgl program in a new tab or window
As you experiment with the demonstration program, please make sure you observe the following characteristics of combining ambient, diffuse, and specular lighting.
- If no direct light is shining on a face, the face’s color is based solely on the ambient light calculation.
- If the light source does not reflect into the camera, the colors are based on the sum of the ambient and diffuse calculations.
- If the light source does reflect into the camera, the colors are based on the sum of the ambient, diffuse, and specular calculations.
Shader Programs for a Combined Lighting Model¶
Please study the following shader programs. Then compare the programs to the comments below.
Vertex Shader¶
// Vertex Shader
precision mediump int;
precision mediump float;
// Scene transformations
uniform mat4 u_PVM_transform; // Projection, view, model transform
uniform mat4 u_VM_transform; // View, model transform
// Light model
uniform vec3 u_Light_position;
uniform vec3 u_Light_color;
uniform float u_Shininess;
uniform vec3 u_Ambient_color;
// Original model data
attribute vec3 a_Vertex;
attribute vec3 a_Color;
attribute vec3 a_Vertex_normal;
// Data (to be interpolated) that is passed on to the fragment shader
varying vec3 v_Vertex;
varying vec4 v_Color;
varying vec3 v_Normal;
void main() {
// Perform the model and view transformations on the vertex and pass this
// location to the fragment shader.
v_Vertex = vec3( u_VM_transform * vec4(a_Vertex, 1.0) );
// Perform the model and view transformations on the vertex's normal vector
// and pass this normal vector to the fragment shader.
v_Normal = vec3( u_VM_transform * vec4(a_Vertex_normal, 0.0) );
// Pass the vertex's color to the fragment shader.
v_Color = vec4(a_Color, 1.0);
// Transform the location of the vertex for the rest of the graphics pipeline
gl_Position = u_PVM_transform * vec4(a_Vertex, 1.0);
}
- Notice the light model is now defined by 4 values: u_Light_position, u_Light_color, u_Shininess, u_Ambient_color.
Fragment Shader¶
// Fragment shader program
precision mediump int;
precision mediump float;
// Light model
uniform vec3 u_Light_position;
uniform vec3 u_Light_color;
uniform float u_Shininess;
uniform vec3 u_Ambient_color;
// Data coming from the vertex shader
varying vec3 v_Vertex;
varying vec4 v_Color;
varying vec3 v_Normal;
void main() {
vec3 to_light;
vec3 vertex_normal;
vec3 reflection;
vec3 to_camera;
float cos_angle;
vec3 diffuse_color;
vec3 specular_color;
vec3 ambient_color;
vec3 color;
// Calculate the ambient color as a percentage of the surface color
ambient_color = u_Ambient_color * vec3(v_Color);
// Calculate a vector from the fragment location to the light source
to_light = u_Light_position - v_Vertex;
to_light = normalize( to_light );
// The vertex's normal vector is being interpolated across the primitive
// which can make it un-normalized. So normalize the vertex's normal vector.
vertex_normal = normalize( v_Normal );
// Calculate the cosine of the angle between the vertex's normal vector
// and the vector going to the light.
cos_angle = dot(vertex_normal, to_light);
cos_angle = clamp(cos_angle, 0.0, 1.0);
// Scale the color of this fragment based on its angle to the light.
diffuse_color = vec3(v_Color) * cos_angle;
// Calculate the reflection vector
reflection = 2.0 * dot(vertex_normal,to_light) * vertex_normal - to_light;
// Calculate a vector from the fragment location to the camera.
// The camera is at the origin, so negating the vertex location gives the vector
to_camera = -1.0 * v_Vertex;
// Calculate the cosine of the angle between the reflection vector
// and the vector going to the camera.
reflection = normalize( reflection );
to_camera = normalize( to_camera );
cos_angle = dot(reflection, to_camera);
cos_angle = clamp(cos_angle, 0.0, 1.0);
cos_angle = pow(cos_angle, u_Shininess);
// The specular color is from the light source, not the object
if (cos_angle > 0.0) {
specular_color = u_Light_color * cos_angle;
diffuse_color = diffuse_color * (1.0 - cos_angle);
} else {
specular_color = vec3(0.0, 0.0, 0.0);
}
color = ambient_color + diffuse_color + specular_color;
gl_FragColor = vec4(color, v_Color.a);
}
The calculated vector to_light is used for both the diffuse calculation and to calculate the reflection vector.
The specular_color is combined with a percentage of the surface’s diffuse color. This is the same idea we used in lesson 9.3, but this time we take a percentage of the diffuse color instead of the face’s color.
Notice that the final color of a pixel is simply the sum of the ambient, diffuse and specular calculations. This is a component-wise vector addition. That is, if a = <a0,a1,a2> and b = <b0,b1,b2>. Then a + b is equal to a 3-component vector <a0+b0, a1+b1, a2+b2>.
This code can be written in less lines if you combine the operations into single statements. For example, the diffuse calculation could be written like this in a single line:
diffuse_color = vec3(v_Color) * clamp(dot(vertex_normal, to_light), 0.0, 1.0);
You are encouraged to not write complex statements like this until much later in your learning. The GLSL compiler will optimize your code, so write your code as clearly as possible for humans! Use descriptive variable names and multiple, distinct statements for clarity.
Type of Light Source¶
The example WebGL program above was based on a point light source. If you had a different type of light source, such as a sun light source, the shader programs would have to be changed because the definition of your light source would change, but the fundamental math would be the same.