12.2 Light Interface
The core lighting routines and interfaces are in core/light.h and core/light.cpp. Implementations of particular lights are in individual source files in the lights/ directory.
All lights share four common parameters:
- The flags parameter indicates the fundamental light source type—for instance, whether or not the light is described by a delta distribution. (Examples of such lights include point lights, which emit illumination from a single point, and directional lights, where all light arrives from the same direction.) The Monte Carlo algorithms that sample illumination from light sources need to be aware of which lights are described by delta distributions, since this affects some of their computations.
- A transformation that defines the light’s coordinate system with respect to world space. As with shapes, it’s often handy to be able to implement a light assuming a particular coordinate system (e.g., that a spotlight is always located at the origin of its light space, shining down the axis). The light-to-world transformation makes it possible to place such lights at arbitrary positions and orientations in the scene.
- A MediumInterface that describes the participating medium on the inside and the outside of the light source. For lights that don’t have “inside” and “outside” per se (e.g., a point light), the same Medium is on both sides. (A value of nullptr for both Medium pointers represents a vacuum.)
- The nSamples parameter is used for area light sources where it may be desirable to trace multiple shadow rays to the light to compute soft shadows; it allows the user to have finer-grained control of the number of samples taken on a per-light basis. The default number of light source samples taken is 1; thus, only the light implementations for which taking multiple samples is sensible need to pass an explicit value to the Light constructor. Not all Integrators pay attention to this value.
The only other job for the constructor is to warn if the light-to-world transformation has a scale factor; many of the Light methods will return incorrect results in this case.
The flags, nSamples, and mediumInterface member variables are widely used outside of Light implementations so it’s worth making them available as public members.
The LightFlags enumeration represents flags for the flags mask field characterizing various kinds of light sources; we’ll see examples of all of these in the remainder of the chapter.
Although storing both the light-to-world and the world-to-light transformations is redundant, having both available simplifies code elsewhere by eliminating the need for calls to Inverse().
A key method that lights must implement is Sample_Li(). The caller passes an Interaction that provides the world space position of a reference point in the scene and a time associated with it, and the light returns the radiance arriving at that point at that time due to that light, assuming there are no occluding objects between them (Figure 12.5). Light implementations in pbrt do not currently support being animated—the lights themselves are at fixed positions in the scene. (Addressing this limitation is left as an exercise.) However, the time value from the Interaction is needed to set the time parameter in the traced visibility ray so that light visibility in the presence of moving objects is resolved correctly.
The Light implementation is also responsible for both initializing the incident direction to the light source and initializing the VisibilityTester object, which holds information about the shadow ray that must be traced to verify that there are no occluding objects between the light and the reference point. The VisibilityTester, which will be described in Section 12.2.1, need not be initialized if the returned radiance value is black—for example, due to the reference point being outside of the cone of illumination of a spotlight. Visibility is irrelevant in this case.
For some types of lights, light may arrive at the reference point from many directions, not just from a single direction as with a point light source, for example. For these types of light sources, the Sample_Li() method samples a point on the light source’s surface, so that Monte Carlo integration can be used to find the reflected light at the point due to illumination from the light. (The implementations of Sample_Li() for such lights will be introduced later, in Section 14.2.) The Point2f u parameter is used by these methods, and the pdf output parameter stores the probability density for the light sample that was taken. For all of the implementations in this chapter, the sample value is ignored and the pdf is set to 1. The pdf value’s role in the context of Monte Carlo sampling is discussed in Section 14.2.
All lights must also be able to return their total emitted power; this quantity is useful for light transport algorithms that may want to devote additional computational resources to lights in the scene that make the largest contribution. Because a precise value for emitted power isn’t needed elsewhere in the system, a number of the implementations of this method later in this chapter will compute approximations to this value rather than expending computational effort to find a precise value.
Finally, Light interface includes a method Preprocess() that is invoked prior to rendering. It includes the Scene as an argument so that the light source can determine characteristics of the scene before rendering starts. The default implementation is empty, but some implementations (e.g., DistantLight) use it to record a bound of the scene extent.
12.2.1 Visibility Testing
The VisibilityTester is a closure—an object that encapsulates a small amount of data and some computation that is yet to be done. It allows lights to return a radiance value under the assumption that the reference point and the light source are mutually visible. The integrator can then decide if illumination from the incident direction is relevant before incurring the cost of tracing the shadow ray—for example, light incident on the back side of a surface that isn’t translucent contributes nothing to reflection from the other side. If the actual amount of arriving illumination is in fact needed, a call to one of the visibility tester’s methods causes the necessary shadow ray to be traced.
VisibilityTesters are created by providing two Interaction objects, one for each end point of the shadow ray to be traced. Because an Interaction is used here, no special cases are needed for computing visibility to reference points on surfaces versus reference points in participating media.
Some of the light transport routines find it useful to be able to retrieve the two end points from an initialized VisibilityTester.
There are two methods that determine the visibility between the two points. The first, Unoccluded(), traces a shadow ray between them and returns a Boolean result. Some ray tracers include a facility for casting colored shadows from partially transparent objects and would return a spectrum from a method like this, but pbrt does not include this facility, since this feature generally requires a nonphysical hack. Scenes where illumination passes through a transparent object should be rendered with an integrator that supports this kind of effect; any of the bidirectional integrators from Chapter 16 is a good choice.
Because it only returns a Boolean value, Unoccluded() also ignores the effects of any scattering medium that the ray passes through on the radiance that it carries. When Integrators need to account for that effect, they use the VisibilityTester’s Tr() method instead. VisibilityTester::Tr() computes the beam transmittance, Equation (11.1), the fraction of radiance transmitted along the segment between the two points. It accounts for both attenuation in participating media as well as any surfaces that block the ray completely.
If an intersection is found along the ray segment and the hit surface is opaque, then the ray is blocked and the transmittance is zero. Our work here is done. (Recall from Section 11.3 that surfaces with a nullptr material pointer should be ignored in ray visibility tests, as those surfaces are only used to bound the extent of participating media.)
Otherwise, the Tr() method accumulates the ray’s transmittance, either to the surface intersection point or to the endpoint p1. (If there was an intersection with a non-opaque surface, the Ray::tMax value has been updated accordingly; otherwise it corresponds to p1.) In either case, Medium::Tr() computes the beam transmittance up to Ray::tMax, using the multiplicative property of beam transmittance from Equation (11.2).
If no intersection was found, the ray made it to p1 and we’ve accumulated the full transmittance. Otherwise, the ray intersected an invisible surface and the loop runs again, tracing a ray from that intersection point onward toward p1.