Project: Phong Lighting
Lighting
The framework as you downloaded it comes with a very basic lighting model that only exists to make the resulting shapes appear three-dimensional. The way this achieved is to scale the surface color of the impact point depending on the direction the light source is in. In other words, if a point directly faces the light it will be more brightly illuminated than a point that the line shines on at an angle. Let us take a look at the implementation of this lighting model:
color getColor(RayHit hit, Scene sc, PVector viewer)
{
color surfacecol = lights.get(0).shine(hit.material.getColor(hit.u, hit.v));
PVector tolight = PVector.sub(lights.get(0).position, hit.location).normalize();
float intensity = PVector.dot(tolight, hit.normal);
return lerpColor(color(0), surfacecol, intensity);
}
The method getColor
is what you call to determine which color to use for a particular ray hit. Note that this lighting model ignores the second and third parameter, but they will become relevant for the (more elaborate) Phong Lighting Model that you will implement.
What this simple lighting model does is to (only) take the first light source, and determine how it will appear on the surface of the hit object. The shine
method of a Light
performs an elementwise scaling of the surface color, depending on the color of the light. For example, if a (purely) red light shines on a white surface, it will appear red, while a “gray” light will make any surface appear equally dimmer in all three colors. Say you have a light source with color (128,255,0)
(a light green), and a surface with color (128,64,255)
(a relatively bright purple). Then the resulting appearance of this light shining on this surface would be (128 * (128/255), 64 * (255/255), 255*(0/255)) = (64,64,0)
, a dark-ish yellow.
However, if we only applied shine
, the entire surface of the sphere would look the same, as this does not take into account which direction the light is coming from. The last three lines of code take care of this: First we determine a (normalized) vector indicating the direction from the impact location to the light source. Then we calculate the dot product of this direction and the normal vector (recall: The dot product is 1 if the two normalized vectors point in the same direction, and 0 if they are perpendicular). The result of this operation is the intensity
of the light at the impact location. Finally, we return a scaled down version of the color depending on this intensity (lerpColor
performs a linear interpolation, in this case between black/no light, and the surface color we determined earlier).
While this basic lighting model will make your spheres (and other objects) appear three-dimensional, it has some obvious limitations: First, it only takes the first light source into account, even if there may be multiple different light sources. Second, it does not allow for different materials to be selected (apart from the surface color), i.e. all spheres will have the same surface properties. And third, there are no shadows. You will address all three of these limitations in this task. The only two files you need to change is Lighting.pde
.
The Phong Lighting Model
The Phong Lighting Model calculates the appearance of a surface point as the sum of three components:
\[I = k_a \cdot i_a + \sum_m \left(k_d (L_m \cdot N) i_d + k_s (R_m \cdot V)^\alpha i_s \right)\]Before we discuss the individual components, note that all parameters with the letter k
(as well as $\alpha$ represent properties of the material, while the i
-values are (color!) properties of the light sources (or environment). The Material
class has members ka
, kd
, ks
and alpha
for exactly this purpose, while $i_a$ the result of scaling the surface color of the impact location with the ambient
value in the PhongLightingModel
object, and $i_d$ and $i_s$ are the result of shining each individual light source onto the surface. Each light source has two methods shine
and spec
that return these values as colors when given the surface color.
To get the resulting color, we need to add the three terms: ambient, diffuse and specular. The ambient light term consists of a scaling the surface color by the ambient
color and multiplying the result with the ka
value of the material. The diffuse and specular terms are a summation over all light sources, i.e. for each light source we calculate the diffuse and specular values, and add both of them to our result.
The diffuse term for a light source (almost) corresponds to what the basic lighting model does: $i_d$ is the result of shine
-ing the light onto the surface, which is then multiplied with kd
, and the dot product of the normal vector and a vector pointing to the light source (this, as before, causes a light that shines directly onto the surface to make it appear brighter than one that shines on it from an angle). Finally, the specular term uses $i_s$ which is the result of spec
-ing the light onto the surface (a light source may have different colors for the diffuse and specular components), multiplied with ks
and another dot product. This time, we calculate the dot product of a vector V
which points from the impact location to the viewer/camera, and a vector R
, which points into the direction the light would be reflected if the surface was a mirror. To calculate R
, you can use $2\vec{N}(\vec{N}\cdot \vec{L}) - \vec{L}$, where N
is the normal vector and L
the vector that points to the light source. Don’t forget to normalize both vectors, L
and R
! The final detail we have on the specular component is that the result of this dot product is actually raised to the power $\alpha$ which defines how “shiny” the surface is, i.e. how concentrated the specular highlight will appear.
Once you have calculated these three components, you add them together and return them, and your spheres should appear shiny (with the appropriate material properties). However, one piece that is missing are shadows.
Note: util.pde
contains functions addColors
to add two color values and multColor
to multiply a color value by a floating point value. However, note that these operations clamp their results to be within the valid range of colors.
Shadow Rays
So far, we have assumed that all light sources affect all surfaces, and the only difference was the angle at which the light would hit the surface. In reality that is not how lights work (You can try the following experiment: Go to our classroom at solar noon and check if the sun really affects the desks as it should, given that it will be directly in the direction of the normal vector). What we need to do is to check if a light source can actually reach a surface point. Fortunately, there is a straightforward way to do this: We shoot a ray from the impact location towards the light source, and we only take this light source into account in our summation from above if that ray does not hit anything. How do we shoot a ray? Our scene already has an intersect
-method! We just need to set up the ray properly: The origin is the impact location, and the direction is our L
-vector from before. If we hit anything, we just skip this particular light. However, this has one slight problem: This ray will always hit something (well, it depends on numerical precision, but it should always hit something): The surface that it started at will be hit at distance t=0
. You might say that you just ignore hits at t=0
, but then you might take into account light sources that are obscured by the object itself. Instead, it is better to start this so-called shadow ray a bit “in front” of the surface (say by the value EPS
, which is conveniently declared in util.pde
). Note: If you implement transparency you may want to consider the transparency of an object to determine if it obscures a light source or not …