Lambertian Shading

Raytracers are great because they allow for easy experimentation with different illumination ideas. One of the most basic techniques for lighting a surface uses Lambert's cosine law to simulate diffuse reflectance.

A perfectly diffuse surface reflects photons from light equally in all directions. Intuitively, this means that the brightness of a point on a lit surface is independent of the location of the viewer. A decent real-world example of a diffuse surface would be the matte paint used in houses.

An important distinction to make is that the location of the light relative to the surface still matters. Our diffuse lighting idea is based on the notion that a surface receives more light as the angle between its normal and the light vector decreases. This can be described by Lambert's cosine law:

// Light a surface proportional to the angle between the normal and light vector double dot = normal.dot(dst_to_light);
total_color += material->getDiffuse() * light->getDiffuse() * dot;

Diffuse lighting with quadratic falloffIn this code, the two calls to getDiffuse() return the colors associated with either the material (applied to the surface being intersected by my camera ray) or to the current light. Furthermore, we make use of a simple mathematical fact in this code: cos(Θ) = (AB) / (|A||B|).

As you can see in the associated image, something is clearly wrong with our output. We can trace this back to another rule of thumb: the quadratic dropoff of light. Basically, we can assume the effect a light has on a surface is proportional to the inverse of the square of the distance of that light to the surface being lit: Photons ∝ √r.

Diffuse lighting with no falloffAlthough far from realistic, this looks much better. Below is some basic code to include the dropoff of a light into our Lambertian lighting equation.

// Light drops off as inverse square of distance
double dropoff = dst_to_light.magnitude();
dropoff = dropoff > 0 ? 1/(dropoff*dropoff) : 1;
dropoff *= light->getPower();
. . .
total_color += material->getDiffuse() *
    light->getDiffuse() * dot * dropoff;

Finally, we can make a simple assumption about lighting in an effort to avoid superfluous computation: if a light is "behind" a point on a surface, it will not add to the color returned by our camera ray. In code, this can be done as a simple check once the dot product between the normal and the light vector is computed.

// Don't care if light is behind surface
if(dot <= 0.0f)
    // Stop computing for this light