When `CalculateLighting` notices that a ray has intersected with a surface point that has some degree of opacity, it calls `CalculateMatte` to figure out the intensity and color of light scattered from that point. Let's take a look at the source code for `CalculateMatte`, followed by a detailed discussion of how it works.

// Determines the contribution of the illumination of a point // based on matte (scatter) reflection based on light incident // to a point on the surface of a solid object. Color Scene::CalculateMatte(const Intersection& intersection) const { // Start at the location where the camera ray hit // a surface and trace toward all light sources. // Add up all the color components to create a // composite color value. Color colorSum(0.0, 0.0, 0.0); // Iterate through all of the light sources. LightSourceList::const_iterator iter = lightSourceList.begin(); LightSourceList::const_iterator end = lightSourceList.end(); for (; iter != end; ++iter) { // Each time through the loop, 'source' // will refer to one of the light sources. const LightSource& source = *iter; // See if we can draw a line from the intersection // point toward the light source without hitting any surfaces. if (HasClearLineOfSight(intersection.point, source.location)) { // Since there is nothing between this point on the object's // surface and the given light source, add this light source's // contribution based on the light's color, luminosity, // squared distance, and angle with the surface normal. // Calculate a direction vector from the intersection point // toward the light source point. const Vector direction = source.location - intersection.point; const double incidence = DotProduct( intersection.surfaceNormal, direction.UnitVector() ); // If the dot product of the surface normal vector and // the ray toward the light source is negative, it means // light is hitting the surface from the inside of the object, // even though we thought we had a clear line of sight. // If the dot product is zero, it means the ray grazes // the very edge of the object. Only when the dot product // is positive does this light source make the point brighter. if (incidence > 0.0) { const double intensity = incidence / direction.MagnitudeSquared(); colorSum += intensity * source.color; } } } return colorSum; }

Because actual matte surfaces scatter incident light in all directions regardless of the source, an ideal ray tracing algorithm would search in the infinite number of directions from the point for any light that might end up there from the surroundings. One of the limitations of the C++ code that accompanies this book is that it does not attempt such a thorough search. There are two reasons for this: avoiding code complexity and greatly reducing the execution time for rendering an image. In our simplified model `CalculateMatte` looks for direct routes to point light sources only. It iterates through all point light sources in the scene, looking for unblocked lines of sight. To make this line-of-sight determination, `CalculateMatte` calls `HasClearLineOfSight`, a helper function that uses `FindClosestIntersection` to figure out whether there is any blocking point between the two points passed as arguments to it:

bool Scene::HasClearLineOfSight( const Vector& point1, const Vector& point2) const { // Subtract point2 from point1 to obtain the direction // from point1 to point2, along with the square of // the distance between the two points. const Vector dir = point2 - point1; const double gapDistanceSquared = dir.MagnitudeSquared(); // Iterate through all the solid objects in this scene. SolidObjectList::const_iterator iter = solidObjectList.begin(); SolidObjectList::const_iterator end = solidObjectList.end(); for (; iter != end; ++iter) { // If any object blocks the line of sight, // we can return false immediately. const SolidObject& solid = *(*iter); // Find the closest intersection from point1 // in the direction toward point2. Intersection closest; if (0 != solid.FindClosestIntersection(point1, dir, closest)) { // We found the closest intersection, but it is only // a blocker if it is closer to point1 than point2 is. // If the closest intersection is farther away than // point2, there is nothing on this object blocking // the line of sight. if (closest.distanceSquared < gapDistanceSquared) { // We found a surface that is definitely blocking // the line of sight. No need to keep looking! return false; } } } // We would not find any solid object that blocks the line of sight. return true; }

Any light source with a clear line of sight adds to the intensity and color of the point based on its distance and angle of incidence, as described in detail below. We therefore miss light reflected from other objects or lensed through other objects, but we do see realistic gradations of light and shadow across curved matte surfaces.

When `CalculateMatte` finds a light source that has a clear line of sight to an intersection point, it uses two vectors to determine how slanted the light from that source is at the surface, which in turn determines how bright the light appears there. The first vector is the surface normal vector $\mathbf{\hat{n}}$, which is the unit vector pointing outward from the solid object at the intersection point, perpendicular to the surface. The second is $\mathbf{\hat{\lambda}}$, a unit vector from the intersection point toward the light source. See Figure 8.1.

Then `CalculateMatte` calculates the dot product of these two vectors, $\mathbf{\hat{n}} \cdot \mathbf{\hat{\lambda}}$, and assigns the result to the variable `incidence`:

const double incidence = DotProduct( intersection.surfaceNormal, direction.UnitVector() );

Notice that this code explicitly converts `direction` to a unit vector, but assumes that the surface normal vector is already a unit vector; it is the responsibility of every class derived from `SolidObject` to fill in every intersection struct with a surface normal vector whose magnitude is 1.

As discussed in the earlier chapter about vectors, we can use the dot product of two unit vectors to determine how aligned the vectors are. There are three broad categories for the dot product value:

**Dot product is negative**: The light source is shining on the inside surface of the solid, away from the camera's view. The portion of a light ray subject to matte reflection experiences the object as opaque, so none of the matte-scattered light travels through the object's surface. Any light shining on the inside surface is therefore assumed to be invisible to the camera, both because it should not have arrived there in the first place and because the object itself blocks the camera's view of interior surfaces.**Dot product is zero**: The light is grazing the surface exactly parallel to it. This causes the light to be spread out infinitely thin. This is the critical angle beyond which the surface point is in shade, and less than which the light makes a contribution to the illumination of that point.**Dot product is positive**: The light source makes the point on the object's surface brighter. The contribution of the light source is maximized when the dot product is equal to 1, which is the greatest possible value of the dot product of two unit vectors. This happens when the surface normal vector and the illumination vector are pointing in exactly the same direction, i.e., when the light is shining down on the surface point exactly perpendicular to the plane tangent to the surface at that point.

So this is why `CalculateMatte` has the conditional statement

if (incidence > 0.0)

When `incidence` is positive, we add a contribution from the given light source based on the inverse-square law and the incidence value:

const double intensity = incidence / direction.MagnitudeSquared();

So `intensity` holds a positive number that becomes larger as the light angle is closer to being perpendicular to the surface or as the light source gets nearer to the surface. Finally, the red, green, and blue color components of the local variable `colorSum` are updated based the intensity and color of the light source shining on the intersection point:

colorSum += intensity * source.color;

The C++ code uses the overloaded operator `*` to multiply the intensity (a double-precision floating point number) with the source color (a `Color` struct), to produce a result color whose red, green, and blue values have been scaled in proportion to the calculated intensity. This multiplication operator appears in `imager.h` as the line

inline Color operator * (double scalar, const Color &color)

The scaled color value is then added to `colorSum` using the overloaded operator `+=`, which is implemented as the following member function inside `struct Color`:

Color& operator += (const Color& other)

After iterating through all light sources, `CalculateMatte` returns `colorSum`, which is the weighted sum of all incident light at the given point. The caller, `CalculateLighting`, further adjusts this return value to account for the opacity factor, the inherent matte color of the surface point, and by the parameter `rayIntensity`.

This last multiplier, `rayIntensity`, was mentioned in the previous chapter. As we discussed there, and as we will explore in more detail in the next two chapters, mirror reflection and refraction can cause a ray of light that has been traced from the camera to split into multiple weaker rays that bounce in many directions throughout the scene. Each weaker ray can split into more rays, causing a branching tree of rays spreading in a large number of diverging directions. To handle this complexity, `CalculateLighting` calls `CalculateReflection` and `CalculateRefraction`, and those two functions can call back into `TraceRay` as needed, which in turn calls `CalculateLighting`. This means that `CalculateLighting` is indirectly recursive: it doesn't call itself directly, but it calls other functions that call back into it. Each time a ray is reflected or refracted, it generally becomes weaker, and so even though the total number of rays keeps increasing, they each make a decreasing contribution to the pixel's color components as seen by the camera.

The `rayIntensity` parameter provides the means for the rays getting weaker each time they are reflected or refracted. The indirectly-recursive calls take the existing `rayIntensity` value that was passed to them, and diminish it even further before calling back into `CalculateLighting`. It is important to understand that when `CalculateMatte` is called, its return value also needs to be scaled by `rayIntensity` since the ray may have ricocheted many times before it arrived at that matte surface in the first place. Putting all of these multipliers together, we arrive at the code where `CalculateLighting` calls `CalculateMatte`:

if (opacity > 0.0) { // This object is at least a little bit opaque, // so calculate the part of the color caused by // matte (scattered) reflection. colorSum += opacity * optics.GetMatteColor() * rayIntensity * CalculateMatte(intersection); }

Copyright © 2013 by
Don Cross.
All Rights Reserved.