12.1 Light Interface

The Light class defines the interface that light sources must implement. It is defined in the file base/light.h and all the light implementations in the following sections are in the files lights.h and lights.cpp.

<<Light Definition>>= 
class Light : public TaggedPointer<<<Light Source Types>> > { public: <<Light Interface>> 
using TaggedPointer::TaggedPointer; static Light Create(const std::string &name, const ParameterDictionary &parameters, const Transform &renderFromLight, const CameraTransform &cameraTransform, Medium outsideMedium, const FileLoc *loc, Allocator alloc); static Light CreateArea(const std::string &name, const ParameterDictionary &parameters, const Transform &renderFromLight, const MediumInterface &mediumInterface, const Shape shape, FloatTexture alpha, const FileLoc *loc, Allocator alloc); SampledSpectrum Phi(SampledWavelengths lambda) const; LightType Type() const; pstd::optional<LightLiSample> SampleLi(LightSampleContext ctx, Point2f u, SampledWavelengths lambda, bool allowIncompletePDF = false) const; Float PDF_Li(LightSampleContext ctx, Vector3f wi, bool allowIncompletePDF = false) const; std::string ToString() const; SampledSpectrum L(Point3f p, Normal3f n, Point2f uv, Vector3f w, const SampledWavelengths &lambda) const; SampledSpectrum Le(const Ray &ray, const SampledWavelengths &lambda) const; void Preprocess(const Bounds3f &sceneBounds); pstd::optional<LightBounds> Bounds() const;
};

This chapter will describe all 9 of the following types of light source.

<<Light Source Types>>= 

All lights must be able to return their total emitted power, normal upper Phi . Among other things, this makes it possible to sample lights according to their relative power in the forthcoming PowerLightSampler. Devoting more samples to the lights that make the largest contribution can significantly improve rendering efficiency.

<<Light Interface>>= 

The Light interface does not completely abstract away all the differences among different types of light source. While doing so would be desirable in principle, in practice pbrt’s integrators sometimes need to handle different types of light source differently, both for efficiency and for correctness. We have already seen an example of this issue in the RandomWalkIntegrator in Section 1.3.6. There, “infinite” lights received special handling since they must be considered for rays that escape the scene without hitting any geometry.

Another example is that 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. Lights therefore categorize themselves into one of a few different types; the Type() method returns which one a light is.

<<Light Interface>>+=  
LightType Type() const;

There are four different light categories:

  • DeltaPosition: lights that emit solely from a single point in space. (“Delta” refers to the fact that such lights can be described by Dirac delta distributions.)
  • DeltaDirection: lights that emit radiance along a single direction.
  • Area: lights that emit radiance from the surface of a geometric shape.
  • Infinite: lights “at infinity” that do not have geometry associated with them but provide radiance to rays that escape the scene.

<<LightType Definition>>= 
enum class LightType { DeltaPosition, DeltaDirection, Area, Infinite };

A helper function checks if a light is defined using a Dirac delta distribution.

<<Light Inline Functions>>= 
bool IsDeltaLight(LightType type) { return (type == LightType::DeltaPosition || type == LightType::DeltaDirection); }

Being able to sample directions at a point where illumination may be incident is an important sampling operation for rendering. Consider a diffuse surface illuminated by a small spherical area light source (Figure 12.1): sampling directions using the BSDF’s sampling distribution is likely to be very inefficient because the light is only visible within a small cone of directions from the point. A much better approach is to instead use a sampling distribution that is based on the light source. In this case, the sampling routine should choose from among only those directions where the sphere is potentially visible.

Figure 12.1: An effective sampling strategy for choosing an incident direction from a point for direct lighting computations is to allow the light source to define a distribution of directions with respect to solid angle at the point. Here, a small spherical light source is illuminating the point. The cone of directions that the sphere subtends is a much better sampling distribution to use than a uniform distribution over the hemisphere, for example.

This important task is the responsibility of implementations of the SampleLi() method. Its caller passes a LightSampleContext that provides information about a reference point in the scene, and the light optionally returns a LightLiSample that encapsulates incident radiance, information about where it is being emitted from, and the value of the probability density function (PDF) for the sampled point. If it is impossible for light to reach the reference point or if there is no valid light sample associated with u, an invalid sample can be returned. Finally, allowIncompletePDF indicates whether the sampling routine may skip generating samples for directions where the light’s contribution is small. This capability is used by integrators that apply MIS compensation (Section 2.2.3).

<<Light Interface>>+=  
pstd::optional<LightLiSample> SampleLi(LightSampleContext ctx, Point2f u, SampledWavelengths lambda, bool allowIncompletePDF = false) const;

The LightSampleContext takes the usual role of encapsulating just as much information about the point receiving illumination as the various sampling routines need.

<<LightSampleContext Definition>>= 
class LightSampleContext { public: <<LightSampleContext Public Methods>> 
LightSampleContext(const SurfaceInteraction &si) : pi(si.pi), n(si.n), ns(si.shading.n) {} LightSampleContext(const Interaction &intr) : pi(intr.pi) {} LightSampleContext(Point3fi pi, Normal3f n, Normal3f ns) : pi(pi), n(n), ns(ns) {} Point3f p() const { return Point3f(pi); }
<<LightSampleContext Public Members>> 
Point3fi pi; Normal3f n, ns;
};

The context just stores a point in the scene, a surface normal, and a shading normal. The point is provided as a Point3fi that makes it possible to include error bounds around the computed ray intersection point. Some of the following sampling routines will need this information as part of their sampling process. If the point is in a scattering medium and not on a surface, the two normals are left at their default left-parenthesis 0 comma 0 comma 0 right-parenthesis values.

Note that the context does not include a time—pbrt’s light sources do not support animated transformations. An exercise at the end of the chapter discusses issues related to extending them to do so.

<<LightSampleContext Public Members>>= 
Point3fi pi; Normal3f n, ns;

As with the other Context classes, a variety of constructors make it easy to create a LightSampleContext.

<<LightSampleContext Public Methods>>= 
LightSampleContext(const SurfaceInteraction &si) : pi(si.pi), n(si.n), ns(si.shading.n) {} LightSampleContext(const Interaction &intr) : pi(intr.pi) {} LightSampleContext(Point3fi pi, Normal3f n, Normal3f ns) : pi(pi), n(n), ns(ns) {}

A convenience method provides the point as a regular Point3f for the routines that would prefer to access it as such.

<<LightSampleContext Public Methods>>+= 
Point3f p() const { return Point3f(pi); }

Figure 12.2: The Light::SampleLi() method returns incident radiance from the light at a point and also returns the direction vector omega Subscript normal i that gives the direction from which radiance is arriving.

Light samples are bundled up into instances of the LightLiSample structure. The radiance L is the amount of radiance leaving the light toward the receiving point; it does not include the effect of extinction due to participating media or occlusion, if there is an object between the light and the receiver. wi gives the direction along which light arrives at the point that was specified via the LightSampleContext (see Figure 12.2) and the point from which light is being emitted is provided by pLight. Finally, the PDF value for the light sample is returned in pdf. This PDF should be measured with respect to solid angle at the receiving point.

<<LightLiSample Definition>>= 
struct LightLiSample { <<LightLiSample Public Methods>> 
LightLiSample() = default; PBRT_CPU_GPU LightLiSample(const SampledSpectrum &L, Vector3f wi, Float pdf, const Interaction &pLight) : L(L), wi(wi), pdf(pdf), pLight(pLight) {} std::string ToString() const;
SampledSpectrum L; Vector3f wi; Float pdf; Interaction pLight; };

Just as we saw for perfect specular reflection and transmission with BSDFs, light sources that are defined in terms of delta distributions fit naturally into this sampling framework, although they require care on the part of the routines that call their sampling methods, since there are implicit delta distributions in the radiance and PDF values that they return. For the most part, these delta distributions naturally cancel out when estimators are evaluated, although multiple importance sampling code must be aware of this case, just as with BSDFs. For samples taken from delta distribution lights, the pdf value in the returned LightLiSample should be set to 1.

Related to this, the PDF_Li() method returns the value of the PDF for sampling the given direction wi from the point represented by ctx. This method is particularly useful in the context of multiple importance sampling (MIS) where, for example, the BSDF may have sampled a direction and we need to compute the PDF for the light’s sampling that direction in order to compute the MIS weight. Implementations of this method may assume that a ray from ctx in direction wi has already been found to intersect the light source, and as with SampleLi(), the PDF should be measured with respect to solid angle. Here, the returned PDF value should be 0 if the light is described by a Dirac delta distribution.

<<Light Interface>>+=  
Float PDF_Li(LightSampleContext ctx, Vector3f wi, bool allowIncompletePDF = false) const;

If a ray happens to intersect an area light source, it is necessary to find the radiance that is emitted back along the ray. This task is handled by the L() method, which takes local information about the intersection point and the outgoing direction. This method should never be called for any light that does not have geometry associated with it.

<<Light Interface>>+=  
SampledSpectrum L(Point3f p, Normal3f n, Point2f uv, Vector3f w, const SampledWavelengths &lambda) const;

Another interface method that only applies to some types of lights is Le(). It enables infinite area lights to contribute radiance to rays that do not hit any geometry in the scene. This method should only be called for lights that report their type to be LightType::Infinite.

<<Light Interface>>+=  
SampledSpectrum Le(const Ray &ray, const SampledWavelengths &lambda) const;

Finally, the Light interface includes a Preprocess() method that is invoked prior to rendering. It takes the rendering space bounds of the scene as an argument. Some light sources need to know these bounds and they are not available when lights are initially created, so this method makes the bounds available to them.

<<Light Interface>>+=  
void Preprocess(const Bounds3f &sceneBounds);

There are three additional light interface methods that will be defined later, closer to the code that uses them. Light::Bounds() provides information that bounds the light’s spatial and directional emission distribution; one use of it is to build acceleration hierarchies for light sampling, as is done in Section 12.6.3. Light::SampleLe() and Light::PDF_Le() are used to sample rays leaving light sources according to their distribution of emission. They are cornerstones of bidirectional light transport algorithms and are defined in the online edition of the book along with algorithms that use them.

12.1.1 Photometric Light Specification

pbrt uses radiometry as the basis of its model of light transport. However, light sources are often described using photometric units—a light bulb package might report that it emits 1,000 lumens of light, for example. Beyond their familiarity, one advantage of photometric descriptions of light emission is that they also account for the variation of human visual response with wavelength. It is also more intuitive to describe lights in terms of the visible power that they emit rather than the power they consume in the process of doing so. (Related to this topic, recall the discussion of luminous efficacy in Section 4.4.)

Therefore, light sources in pbrt’s scene description files can be specified in terms of the luminous power that they emit. These specifications are then converted to radiometric quantities in the code that initializes the scene representation. Radiometric values are then passed to the constructors of the Light implementations in this chapter, often in the form of a base spectral distribution and a scale factor that is applied to it.

12.1.2 The LightBase Class

As there was with classes like CameraBase and FilmBase for Camera and Film implementations, there is a LightBase class that all of pbrt’s light sources inherit from. LightBase stores a number of values that are common to all of pbrt’s lights and is thus able to implement some of the Light interface methods. It is not required that a Light in pbrt inherit from LightBase, but lights must provide implementations of a few more Light methods if they do not.

<<LightBase Definition>>= 
class LightBase { public: <<LightBase Public Methods>> 
LightBase(LightType type, const Transform &renderFromLight, const MediumInterface &mediumInterface); LightType Type() const { return type; } SampledSpectrum L(Point3f p, Normal3f n, Point2f uv, Vector3f w, const SampledWavelengths &lambda) const { return SampledSpectrum(0.f); } SampledSpectrum Le(const Ray &, const SampledWavelengths &) const { return SampledSpectrum(0.f); }
protected: <<LightBase Protected Methods>> 
static const DenselySampledSpectrum *LookupSpectrum(Spectrum s);
<<LightBase Protected Members>> 
LightType type; Transform renderFromLight; MediumInterface mediumInterface; static InternCache<DenselySampledSpectrum> *spectrumCache;
};

The following three values are passed to the LightBase constructor, which stores them in these member variables:

  • type characterizes the light’s type.
  • renderFromLight is a transformation that defines the light’s coordinate system with respect to rendering space. As with shapes, it is 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 plus z axis). The rendering-from-light transformation makes it possible to place such lights at arbitrary positions and orientations in the scene.
  • A MediumInterface describes the participating medium on the inside and the outside of the light source. For lights that do not have “inside” and “outside” (e.g., a point light), the MediumInterface stores the same Medium on both sides.

<<LightBase Protected Members>>= 
LightType type; Transform renderFromLight; MediumInterface mediumInterface;

LightBase can thus take care of providing an implementation of the Type() interface method.

<<LightBase Public Methods>>= 
LightType Type() const { return type; }

It also provides default implementations of L() and Le() so that lights that are not respectively area or infinite lights do not need to implement these themselves.

<<LightBase Public Methods>>+=  
SampledSpectrum L(Point3f p, Normal3f n, Point2f uv, Vector3f w, const SampledWavelengths &lambda) const { return SampledSpectrum(0.f); }

<<LightBase Public Methods>>+= 
SampledSpectrum Le(const Ray &, const SampledWavelengths &) const { return SampledSpectrum(0.f); }

Most of the following Light implementations take a Spectrum value in their constructor to specify the light’s spectral emission but then convert it to a DenselySampledSpectrum to store in a member variable. By doing so, they enjoy the benefits of efficient sampling operations from tabularizing the spectrum and a modest performance benefit from not requiring dynamic dispatch to call Spectrum methods.

However, a DenselySampledSpectrum that covers the visible wavelengths uses approximately 2 kB of storage; for scenes with millions of light sources, the memory required may be significant. Therefore, LightBase provides a LookupSpectrum() method that helps reduce memory use by eliminating redundant copies of the same DenselySampledSpectrum. It uses the InternCache from Section B.4.2 to do so, only allocating storage for a single instance of each DenselySampledSpectrum provided. If many lights have the same spectral emission profile, the memory savings may be significant.

<<LightBase Method Definitions>>= 
const DenselySampledSpectrum *LightBase::LookupSpectrum(Spectrum s) { <<Initialize spectrumCache on first call>> 
static std::mutex mutex; mutex.lock(); if (!spectrumCache) spectrumCache = new InternCache<DenselySampledSpectrum>( #ifdef PBRT_BUILD_GPU_RENDERER Options->useGPU ? Allocator(&CUDATrackedMemoryResource::singleton) : #endif Allocator{}); mutex.unlock();
<<Return unique DenselySampledSpectrum from intern cache for s>> 
auto create = [](Allocator alloc, const DenselySampledSpectrum &s) { return alloc.new_object<DenselySampledSpectrum>(s, alloc); }; return spectrumCache->Lookup(DenselySampledSpectrum(s), create);
}

The <<Initialize spectrumCache on first call>> fragment, not included here, handles the details of initializing the spectrumCache, including ensuring mutual exclusion if multiple threads have called LookupSpectrum() concurrently and using an appropriate memory allocator—notably, one that allocates memory on the GPU if GPU rendering has been enabled.

LookupSpectrum() then calls the InternCache::Lookup() method that takes a callback function to create the object that is stored in the cache. In this way, it is able to pass the provided allocator to the DenselySampledSpectrum constructor, which in turn ensures that it is used to allocate the storage needed for its spectral samples.

<<Return unique DenselySampledSpectrum from intern cache for s>>= 
auto create = [](Allocator alloc, const DenselySampledSpectrum &s) { return alloc.new_object<DenselySampledSpectrum>(s, alloc); }; return spectrumCache->Lookup(DenselySampledSpectrum(s), create);

<<LightBase Protected Members>>+= 
static InternCache<DenselySampledSpectrum> *spectrumCache;