| CS 381 Fall 2012 > Lecture Notes for Thursday, October 18, 2012 |
The Phong Illumination Model (Bui Tuong Phong [1973]) improves on the Lambertian Model. The Phong Model is divided into three parts—ambient, diffuse, and specular—each of which involves a color computation. The three colors are then combined to produce the final color. For each part, we specify a light color and a surface color. Thus, there are 6 colors to be specified in all (however, as we will see, in practice we can simplify this).
Light reflected off the surface is travelling in a particular direction: the reflected light direction. If this points at the camera, then the viewer sees the specular highlight at that point on the surface. In practice, the light source is not a single point; realistically, the viewer should see the specular highlight when the reflected light direction vector points near the viewing direction. The Phong Model’s answer to this is a simple computation that “smears” the specular highlight so that it looks like a fuzzy blob.
To compute the reflected light direction, let \(\mathbf{n}\) be the (unit) surface normal vector, and let \(\mathbf{w}\) be the (unit) incoming light direction vector, that is, \(\mathbf{w}=-\mathbf{v}\), where \(\mathbf{v}\) is the (unit) vector giving the direction of the light source. Then the reflected light direction is
\[ \mathbf{w}-2(\mathbf{w}\cdot\mathbf{n})\mathbf{n}, \]
In GLSL, we can compute the reflected light direction using the
built-in function reflect.
[GLSL]
vec3 reflectlightdir = reflect(-lightdir, surfnorm);
The viewing direction is a vector that points from the vertex position to the camera position—the origin, in camera coordinates.
[GLSL]
vec3 viewdir = normalize(-vertpos);
To “smear” our specular highlight We need a function that is near \(1\) when the reflected light direction and the viewing direction are near each other, and which falls quickly to near \(0\) as these two vectors become farther apart. Then we can multiply this function by our specular light and surface colors.
The cosine of the
angle between the viewing direction
and the reflected light direction
(this angle is labeled “θ” in the figure below)
is \(1\) when the angle is zero.
The cosine falls as the angle grows, but it does not fall fast enough.
Our solution is to raise this cosine—or zero, if the cosine is negative—to a high power—say \(50\). This power is the shininess. The resulting number is our specular coefficient. As before, we can compute the cosine using a dot product.
[GLSL]
float specularcoeff = pow(max(0., dot(reflectlightdir, viewdir)), shininess);
The specular color is the result of multiplying the specular coefficient by the coordinate-wise product of the specular light color and the specular surface color.
[GLSL]
vec4 specularcolor = specularcoeff * specularlightcolor * specularsurfacecolor;
The shininess determines the size of the fuzzy-blob specular highlight; a higher shininess gives a smaller blob.
The final color in the Phong Model is computed by adding
the colors from the three parts above: ambient, diffuse, specular.
If any of the resulting color components is above \(1\),
then set it to \(1\).
In GLSL, we can use the built-in function clamp.
[GLSL]
vec4 phongcolor = clamp(ambientcolor + diffusecolor + specularcolor, 0., 1.); // Clamp values to range [0,1]
Again, in its full generality, the Phong Model requires six colors to be specified: the ambient, diffuse, and specular colors for both the light source and the surface. In practice, I usually set the ambient and diffuse surface colors to the paint color and the specular surface color to white (i.e., leave the surface color out of the specular computation). I set the diffuse and specular light colors to the usual light color and the ambient light color to the usual light color times some small-ish number—say \(0.1\) or \(0.2\)—which we might call the ambient fraction.
| Light-Source Color | Surface Color | |
| Ambient | ambient_fraction*light_color | paint_color |
| Diffuse | light_color | paint_color |
| Specular | light_color | WHITE |
So, for example, the specular color computation simplifies to the following.
[GLSL]
vec4 specularcolor = specularcoeff * lightcolor;
This simplified model only requires that we specify the light color and the surface paint color, as in the Lambertian model, along with two numbers: the ambient fraction and the shininess.
See
phongvert_shaders.zip for shaders that compute
per-vertex lighting using the Phong Illumination Model.
These shaders are intended to be used with
useshaders.cpp.
The Blinn-Phong Illumination Model (Jim Blinn [1977]) is a modification of the Phong Illumination Model that requires a little less computation to give results of essentially the same quality. This is the model that is used by default in the OpenGL FFP.
The Blinn-Phong Model is very similar to the Phong Model. It uses the same three parts: ambient, diffuse, and specular. The only difference is in the specular; ambient and diffuse colors are computed in the same way, and the three parts are combined in the same way.
The specular computation in the Blinn-Phong Model is based on the following observation: if the reflected light vector points directly at the camera, then the surface normal is halfway between the light direction and the viewing direction. Thus, the Blinn-Phong Model does not compute the reflected light vector. Instead, it computes a halfway vector, halfway between the light-source direction and the viewing direction. The halfway vector can be computed by adding the (normalized!) light-source direction and viewing direction vectors, and then normalizing the result.
[GLSL]
vec3 halfway = normalize(viewdir + lightdir);
The model finds the cosine of the angle between this halfway vector
and the surface normal
(this angle is θ in the figure below).
Then the specular coefficient is computing by raising this cosine—or zero if the cosine is negative—to the power of the shininess, just as in the Phong Model.
[GLSL]
float specularcoeff = pow(max(0., dot(halfway, surfnorm)), shininess);
To obtain results similar to the Phong Model, the Blinn-Phong Model requires a shininess that is twice as large.
See
bpvert_shaders.zip for shaders that compute
per-vertex lighting using the Blinn-Phong Illumination Model.
These shaders are intended to be used with
useshaders.cpp.
So far, we have done all our lighting computations at the vertex level. We can also do them at the fragment level, based on another idea of Bui Tuong Phong. We use the same illumination model, but instead of computing the vertex color, and then interpolating to find the fragment color, we interpolate the normal vector (Phong interpolation), and then do the lighting computation for each fragment using the interpolated normal.
Per-fragment lighting requires more computation than per-vertex lighting. However, it is not complex computation, and the operations done on separate fragments do not depend on each other. Modern graphics hardware, which includes facilities for processing many fragments simultaneously, generally offers good support for per-fragment lighting.
Consider what information needs to be communicated
by the vertex shader.
We assume that the light-source position is obtained from
the application, via a uniform variable.
We also assume that the light-source color
and shininess are hard-coded in the fragment shader.
As always, we must compute the vertex position,
in screen coordinates, in homogeneous form,
and write this to gl_Position.
In order to do per-fragment lighting using the illumination models
we have covered,
our vertex shader must send three additional pieces of information
on through the pipeline:
vec4).vec3).vec3).
The paint color can be communicated using a
GLSL built-in: gl_FrontColor.
The other two need to be sent to the fragment shader as
varying values.
To do this, declare a global varying variable
with the same name in both the vertex and fragment shaders.
Note that, while OpenGL wants the screen coordinates of our vertex
as a vec4,
we do our lighting computations in camera coordinates,
using a vec3.
[GLSL]
varying vec3 myvert; // Vertex position (camera coords) varying vec3 surfnorm_un; // Surface normal (camera coords) // Frag shader must normalize
The surface normal vector must be normalized
both before and after
sending it through to the fragment shader.
We normalize before, so that it will interpolate correctly,
and we normalize after, since vectors of length \(1\)
can be interpolated to obtain a vector that is too short.
(I named the variable “surfnorm_un” to remind myself
that it is UN-normalized when the fragment shader receives it.)
See
bpfrag_shaders.zip for shaders that compute
per-fragment lighting using the Blinn-Phong Illumination Model.
These shaders are intended to be used with
useshaders.cpp.
Comparing per-vertex and per-fragment implementations of the Blinn-Phong Illumination Model, we can see that the latter requires a much lower polygon count to produce a nice-looking specular highlight. We conclude:
With that in mind, see
lambertfrag_shaders.zip for shaders that compute
per-fragment lighting using the Lambertian Illumination Model.
These shaders are intended to be used with
useshaders.cpp.
To compute a facet normal, find two non-parallel vectors in the plane of the facet, take their cross product, and normalize this. For a triangle, we can find these two vectors by subtracting the coordinates of the vertices of the triangle.
For example, suppose we have a triangle with vertices \(\mathbf{p}_1\), \(\mathbf{p}_2\), and \(\mathbf{p}_3\). Then we can find a facet normal as follows.
\[ \mathbf{u} = \mathbf{p}_2 - \mathbf{p}_1 \] \[ \mathbf{v} = \mathbf{p}_3 - \mathbf{p}_1 \] \[ \mathbf{n} = \frac{\mathbf{u} \times \mathbf{v}} {||\mathbf{u} \times \mathbf{v}||} = \textrm{normalize}(\mathbf{u} \times \mathbf{v}) \]
When we compute normals in this manner, we need to be careful that they point in the right direction. If not, then reverse the normal [\(\mathbf{n} = -\mathbf{n}\)], or, equivalently, do the cross product in the reverse order [\(\mathbf{n} = \textrm{normalize}(\mathbf{v} \times \mathbf{u})\)].
We usually want smooth-looking surfaces, and so we prefer vertex normals to facet normals. If a surface is given by a formula, then we can generally compute vertex normals based on the formula (we cover this shortly). However, if a surface is described as a polygonal mesh, then the only normals we can compute directly are the facet normals.
However, we can compute reasonable vertex normals based on facet normals. One standard way to find a vertex normal is to find facet normals for all facets incident to a vertex, add these, and then normalize. Essentially, the vertex normal is the average of all the facet normals.
Recall that the above idea, together with lighting computations at the vertices, and interpolation of colors between vertices, is called Gouraud shading.
Again, if we have a surface described by a formula, then we can generally compute vertex normals based on the formula.
For example, we can parametrize a torus using two angles, \(a_1\) and \(a_2\), both going from \(0\) to \(2\pi\). Let \(r_1\) be the “small” radius of the torus, and let \(r_2\) be the “big” radius of the torus. If we center the torus at the origin, and let its main axis be the \(z\)-axis, then the coordinates of a point on the surface of the torus would be
\[ \Bigl(\,(r_2 + r_1\, \cos\, a_1)\, \cos\, a_2, (r_2 + r_1\, \cos\, a_1)\, \sin\, a_2, r_1\, \sin\, a_1\,\Bigr). \]
The associated normal vector would be
\[ (\cos\, a_1\, \cos\, a_2, \cos\, a_1\, \sin\, a_2, \sin\, a_1). \]
Application
useshaders.cpp
has been modified
so that it now can draw a cylinder,
with normals computed based on the surface formula.
Also see
torus_shaders.zip for shaders that
compute vertex positions, normals, and per-fragment lighting.
These shaders are intended to be used with
useshaders.cpp.
ggchappell@alaska.edu