Chapter 8: Matte Reflection

8.1 Source code listing

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;
}

8.2 Clear lines of sight

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.

8.3 Brightness of incident light

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.

Figure 8.1: Matte reflection: a light source $\mathbf{S}$ illuminates the point $\mathbf{P}$ on the sphere with an intensity proportional to the dot product of the surface normal unit vector $\mathbf{\hat{n}}$ and the illumination unit vector $\mathbf{\hat{\lambda}}$.

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:

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)

8.4 Using CalculateMatte's return value

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.