12.5 Infinite Area Lights

Another useful kind of light is an infinitely far-away area light source that surrounds the entire scene. One way to visualize this type of light is as an enormous sphere that casts light into the scene from every direction. One important use of infinite area lights is for environment lighting, where an image of the illumination in an environment is used to illuminate synthetic objects as if they were in that environment. Figure 12.16 compares illuminating a car model with standard area lights to illuminating it with two environment maps that simulate illumination from the sky at different times of day. The increase in realism is striking.

Figure 12.16: Car model (a) illuminated with a few area lights, (b) illuminated with midday skylight from an environment map, (c) using a sunset environment map. (Model courtesy of Yasutoshi Mori.)

pbrt provides three implementations of infinite area lights of progressive complexity. The first describes an infinite light with uniform emitted radiance; the second instead takes an image that represents the directional distribution of emitted radiance, and the third adds capabilities for culling parts of such images that are occluded at the reference point, which can substantially improve sampling efficiency.

12.5.1 Uniform Infinite Lights

A uniform infinite light source is fairly easy to implement; some of the details will be helpful for understanding the infinite light variants to follow.

<<UniformInfiniteLight Definition>>= 
class UniformInfiniteLight : public LightBase { public: <<UniformInfiniteLight Public Methods>> 
UniformInfiniteLight(const Transform &renderFromLight, Spectrum Lemit, Float scale); void Preprocess(const Bounds3f &sceneBounds) { sceneBounds.BoundingSphere(&sceneCenter, &sceneRadius); } SampledSpectrum Phi(SampledWavelengths lambda) const; PBRT_CPU_GPU SampledSpectrum Le(const Ray &ray, const SampledWavelengths &lambda) const; PBRT_CPU_GPU pstd::optional<LightLiSample> SampleLi(LightSampleContext ctx, Point2f u, SampledWavelengths lambda, bool allowIncompletePDF) const; PBRT_CPU_GPU Float PDF_Li(LightSampleContext, Vector3f, bool allowIncompletePDF) const; PBRT_CPU_GPU pstd::optional<LightLeSample> SampleLe(Point2f u1, Point2f u2, SampledWavelengths &lambda, Float time) const; PBRT_CPU_GPU void PDF_Le(const Ray &, Float *pdfPos, Float *pdfDir) const; PBRT_CPU_GPU void PDF_Le(const Interaction &, Vector3f w, Float *pdfPos, Float *pdfDir) const { LOG_FATAL("Shouldn't be called for non-area lights"); } pstd::optional<LightBounds> Bounds() const { return {}; } std::string ToString() const;
private: <<UniformInfiniteLight Private Members>> 
const DenselySampledSpectrum *Lemit; Float scale; Point3f sceneCenter; Float sceneRadius;
};

Emitted radiance is specified as usual by both a spectrum and a separate scale. (The straightforward constructor that initializes these is not included in the text.)

<<UniformInfiniteLight Private Members>>= 
const DenselySampledSpectrum *Lemit; Float scale;

All the infinite light sources, including UniformInfiniteLight, store a bounding sphere of the scene that they use when computing their total power and for sampling rays leaving the light.

<<UniformInfiniteLight Private Members>>+= 
Point3f sceneCenter; Float sceneRadius;

Infinite lights must implement the following Le() method to return their emitted radiance for a given ray. Since the UniformInfiniteLight emits the same amount for all rays, the implementation is trivial.

<<UniformInfiniteLight Method Definitions>>= 
SampledSpectrum UniformInfiniteLight::Le(const Ray &ray, const SampledWavelengths &lambda) const { return scale * Lemit->Sample(lambda); }

We can see the use of the allowIncompletePDF parameter for the first time in the SampleLi() method. If it is true, then UniformInfiniteLight immediately returns an unset sample. (And its PDF_Li() method, described a bit later, will return a PDF of zero for all directions.) To understand why it is implemented in this way, consider the direct lighting integral

integral Underscript script upper S squared Endscripts f Subscript Baseline left-parenthesis normal p Subscript Baseline comma omega Subscript normal o Baseline comma omega Subscript normal i Baseline right-parenthesis upper L Subscript normal i Baseline left-parenthesis normal p Subscript Baseline comma omega Subscript normal i Baseline right-parenthesis StartAbsoluteValue cosine theta Subscript normal i Baseline EndAbsoluteValue normal d omega Subscript normal i Baseline period

For a uniform infinite light, the incident radiance function is a constant times the visibility term; the constant can be pulled out of the integral, leaving

c integral Underscript script upper S squared Endscripts f Subscript Baseline left-parenthesis normal p Subscript Baseline comma omega Subscript normal o Baseline comma omega Subscript normal i Baseline right-parenthesis StartAbsoluteValue cosine theta Subscript normal i Baseline EndAbsoluteValue normal d omega Subscript normal i Baseline period

There is no reason for the light to participate in sampling this integral, since BSDF sampling accounts for the remaining factors well. Furthermore, recall from Section 2.2.3 that multiple importance sampling (MIS) can increase variance when one of the sampling techniques is much more effective than the others. This is such a case, so as long as calling code is sampling the BSDF and using MIS, samples should not be generated here. (This is an application of MIS compensation, which was introduced in Section 2.2.3.)

<<UniformInfiniteLight Method Definitions>>+=  
pstd::optional<LightLiSample> UniformInfiniteLight::SampleLi(LightSampleContext ctx, Point2f u, SampledWavelengths lambda, bool allowIncompletePDF) const { if (allowIncompletePDF) return {}; <<Return uniform spherical sample for uniform infinite light>> 
Vector3f wi = SampleUniformSphere(u); Float pdf = UniformSpherePDF(); return LightLiSample(scale * Lemit->Sample(lambda), wi, pdf, Interaction(ctx.p() + wi * (2 * sceneRadius), &mediumInterface));
}

If sampling is to be performed, the light generates a sample so that valid Monte Carlo estimates can still be computed. This task is easy—all directions are sampled with uniform probability. Note that the endpoint of the shadow ray is set in the same way as it was by the DistantLight: by computing a point that is certainly outside of the scene’s bounds.

<<Return uniform spherical sample for uniform infinite light>>= 
Vector3f wi = SampleUniformSphere(u); Float pdf = UniformSpherePDF(); return LightLiSample(scale * Lemit->Sample(lambda), wi, pdf, Interaction(ctx.p() + wi * (2 * sceneRadius), &mediumInterface));

The PDF_Li() method must account for the value of allowIncompletePDF so that the PDF values it returns are consistent with its sampling method.

<<UniformInfiniteLight Method Definitions>>+=  
Float UniformInfiniteLight::PDF_Li(LightSampleContext ctx, Vector3f w, bool allowIncompletePDF) const { if (allowIncompletePDF) return 0; return UniformSpherePDF(); }

The total power from an infinite light can be found by taking the product of the integral of the incident radiance over all directions times an integral over the area of the disk, along the lines of DistantLight::Phi().

<<UniformInfiniteLight Method Definitions>>+= 
SampledSpectrum UniformInfiniteLight::Phi(SampledWavelengths lambda) const { return 4 * Pi * Pi * Sqr(sceneRadius) * scale * Lemit->Sample(lambda); }

12.5.2 Image Infinite Lights

ImageInfiniteLight is a useful infinite light variation that uses an Image to define the directional distribution of emitted radiance. Given an image that represents the distribution of incident radiance in a real-world environment (sometimes called an environment map), this light can be used to render objects under the same illumination, which is particularly useful for applications like visual effects for movies, where it is often necessary to composite rendered objects with film footage. (See the “Further Reading” section for information about techniques for capturing this lighting data from real-world environments.) Figure 12.17 shows the image radiance maps used in Figure 12.16.

Figure 12.17: Environment Maps Used for Illumination in Figure 12.16. All use the octahedral mapping and equal-area parameterization of the sphere from Section 3.8.3. (a) Midday and (b) sunset sky. (Midday environment map courtesy of Sergej Majboroda, sunset environment map courtesy of Greg Zaal, both via Poly Haven.)

<<ImageInfiniteLight Definition>>= 
class ImageInfiniteLight : public LightBase { public: <<ImageInfiniteLight Public Methods>> 
ImageInfiniteLight(Transform renderFromLight, Image image, const RGBColorSpace *imageColorSpace, Float scale, std::string filename, Allocator alloc); void Preprocess(const Bounds3f &sceneBounds) { sceneBounds.BoundingSphere(&sceneCenter, &sceneRadius); } SampledSpectrum Phi(SampledWavelengths lambda) const; PBRT_CPU_GPU Float PDF_Li(LightSampleContext, Vector3f, bool allowIncompletePDF) const; PBRT_CPU_GPU pstd::optional<LightLeSample> SampleLe(Point2f u1, Point2f u2, SampledWavelengths &lambda, Float time) const; PBRT_CPU_GPU void PDF_Le(const Ray &, Float *pdfPos, Float *pdfDir) const; PBRT_CPU_GPU void PDF_Le(const Interaction &, Vector3f w, Float *pdfPos, Float *pdfDir) const { LOG_FATAL("Shouldn't be called for non-area lights"); } std::string ToString() const; SampledSpectrum Le(const Ray &ray, const SampledWavelengths &lambda) const { Vector3f wLight = Normalize(renderFromLight.ApplyInverse(ray.d)); Point2f uv = EqualAreaSphereToSquare(wLight); return ImageLe(uv, lambda); } pstd::optional<LightLiSample> SampleLi(LightSampleContext ctx, Point2f u, SampledWavelengths lambda, bool allowIncompletePDF) const { <<Find left-parenthesis u comma v right-parenthesis sample coordinates in infinite light texture>> 
Float mapPDF = 0; Point2f uv; if (allowIncompletePDF) uv = compensatedDistribution.Sample(u, &mapPDF); else uv = distribution.Sample(u, &mapPDF); if (mapPDF == 0) return {};
<<Convert infinite light sample point to direction>>  <<Compute PDF for sampled infinite light direction>> 
Float pdf = mapPDF / (4 * Pi);
<<Return radiance value for infinite light direction>> 
return LightLiSample(ImageLe(uv, lambda), wi, pdf, Interaction(ctx.p() + wi * (2 * sceneRadius), &mediumInterface));
} pstd::optional<LightBounds> Bounds() const { return {}; }
private: <<ImageInfiniteLight Private Methods>> 
SampledSpectrum ImageLe(Point2f uv, const SampledWavelengths &lambda) const { RGB rgb; for (int c = 0; c < 3; ++c) rgb[c] = image.LookupNearestChannel(uv, c, WrapMode::OctahedralSphere); RGBIlluminantSpectrum spec(*imageColorSpace, ClampZero(rgb)); return scale * spec.Sample(lambda); }
<<ImageInfiniteLight Private Members>> 
Image image; const RGBColorSpace *imageColorSpace; Float scale; Point3f sceneCenter; Float sceneRadius; PiecewiseConstant2D distribution; PiecewiseConstant2D compensatedDistribution;
};

The image that specifies the emission distribution should use the equal-area octahedral parameterization of directions that was defined in Section 3.8.3. The LightBase::renderFromLight transformation can be used to orient the environment map.

<<ImageInfiniteLight Private Members>>= 
Image image; const RGBColorSpace *imageColorSpace; Float scale;

Like UniformInfiniteLights, ImageInfiniteLights also need the scene bounds; here again, the Preprocess() method (this one not included in the text) stores the scene’s bounding sphere after all the scene geometry has been created.

<<ImageInfiniteLight Private Members>>+=  
Point3f sceneCenter; Float sceneRadius;

The ImageInfiniteLight constructor contains a fair amount of boilerplate code that we will skip past. (For example, it verifies that the provided image has channels named “R,” “G,” and “B” and issues an error if it does not.) The interesting parts of it are gathered in the following fragment.

<<ImageInfiniteLight constructor implementation>>= 
<<Initialize sampling PDFs for image infinite area light>> 
Array2D<Float> d = image.GetSamplingDistribution(); Bounds2f domain = Bounds2f(Point2f(0, 0), Point2f(1, 1)); distribution = PiecewiseConstant2D(d, domain, alloc);
<<Initialize compensated PDF for image infinite area light>> 
Float average = std::accumulate(d.begin(), d.end(), 0.) / d.size(); for (Float &v : d) v = std::max<Float>(v - average, 0); compensatedDistribution = PiecewiseConstant2D(d, domain, alloc);

The image maps used with ImageInfiniteLights often have substantial variation along different directions: consider, for example, an environment map of the sky during daytime, where the relatively small number of directions that the sun subtends are thousands of times brighter than the rest of the directions. Therefore, implementing a sampling method for ImageInfiniteLights that matches the illumination distribution can significantly reduce variance in rendered images compared to sampling directions uniformly. To this end, the constructor initializes a PiecewiseConstant2D distribution that is proportional to the image pixel values.

<<Initialize sampling PDFs for image infinite area light>>= 
Array2D<Float> d = image.GetSamplingDistribution(); Bounds2f domain = Bounds2f(Point2f(0, 0), Point2f(1, 1)); distribution = PiecewiseConstant2D(d, domain, alloc);

<<ImageInfiniteLight Private Members>>+=  
PiecewiseConstant2D distribution;

A second sampling distribution is computed based on a thresholded version of the image where the average pixel value is subtracted from each pixel’s sampling weight. The use of both of these sampling distributions will be discussed in more detail shortly, with the implementation of the SampleLi() method.

<<Initialize compensated PDF for image infinite area light>>= 
Float average = std::accumulate(d.begin(), d.end(), 0.) / d.size(); for (Float &v : d) v = std::max<Float>(v - average, 0); compensatedDistribution = PiecewiseConstant2D(d, domain, alloc);

<<ImageInfiniteLight Private Members>>+= 
PiecewiseConstant2D compensatedDistribution;

Before we get to the sampling methods, we will provide an implementation of the Le() method that is required by the Light interface for infinite lights. After computing the 2D coordinates of the provided ray’s direction in image coordinates, it defers to the ImageLe() method.

<<ImageInfiniteLight Public Methods>>= 
SampledSpectrum Le(const Ray &ray, const SampledWavelengths &lambda) const { Vector3f wLight = Normalize(renderFromLight.ApplyInverse(ray.d)); Point2f uv = EqualAreaSphereToSquare(wLight); return ImageLe(uv, lambda); }

ImageLe() returns the emitted radiance for a given point in the image.

<<ImageInfiniteLight Private Methods>>= 
SampledSpectrum ImageLe(Point2f uv, const SampledWavelengths &lambda) const { RGB rgb; for (int c = 0; c < 3; ++c) rgb[c] = image.LookupNearestChannel(uv, c, WrapMode::OctahedralSphere); RGBIlluminantSpectrum spec(*imageColorSpace, ClampZero(rgb)); return scale * spec.Sample(lambda); }

There is a bit more work to do for sampling an incident direction at a reference point according to the light’s emitted radiance.

<<ImageInfiniteLight Public Methods>>+=  
pstd::optional<LightLiSample> SampleLi(LightSampleContext ctx, Point2f u, SampledWavelengths lambda, bool allowIncompletePDF) const { <<Find left-parenthesis u comma v right-parenthesis sample coordinates in infinite light texture>> 
Float mapPDF = 0; Point2f uv; if (allowIncompletePDF) uv = compensatedDistribution.Sample(u, &mapPDF); else uv = distribution.Sample(u, &mapPDF); if (mapPDF == 0) return {};
<<Convert infinite light sample point to direction>>  <<Compute PDF for sampled infinite light direction>> 
Float pdf = mapPDF / (4 * Pi);
<<Return radiance value for infinite light direction>> 
return LightLiSample(ImageLe(uv, lambda), wi, pdf, Interaction(ctx.p() + wi * (2 * sceneRadius), &mediumInterface));
}

The first step is to generate an image sample with probability proportional to the image pixel values, which is a task that is handled by the PiecewiseConstant2D Sample() method. If SampleLi() is called with allowIncompletePDF being true, then the second sampling distribution that was based on the thresholded image is used. The motivation for doing so is the same as when UniformInfiniteLight::SampleLi() does not generate samples at all in that case: here, there is no reason to spend samples in parts of the image that have a relatively low contribution. It is better to let other sampling techniques (e.g., BSDF sampling) generate samples in those directions when they are actually important for the full function being integrated. Light samples are then allocated to the bright parts of the image, where they are more useful.

<<Find left-parenthesis u comma v right-parenthesis sample coordinates in infinite light texture>>= 
Float mapPDF = 0; Point2f uv; if (allowIncompletePDF) uv = compensatedDistribution.Sample(u, &mapPDF); else uv = distribution.Sample(u, &mapPDF); if (mapPDF == 0) return {};

It is a simple matter to convert from image coordinates to a rendering space direction wi.

<<Convert infinite light sample point to direction>>= 

The PDF returned by PiecewiseConstant2D::Sample() is with respect to the image’s left-bracket 0 comma 1 right-bracket squared domain. To find the corresponding PDF with respect to direction, the change of variables factor for going from the unit square to the unit sphere 1 slash left-parenthesis 4 pi right-parenthesis must be applied.

<<Compute PDF for sampled infinite light direction>>= 
Float pdf = mapPDF / (4 * Pi);

Finally, as with the DistantLight and UniformInfiniteLight, the second point for the shadow ray is found by offsetting along the wi direction far enough until that resulting point is certainly outside of the scene’s bounds.

<<Return radiance value for infinite light direction>>= 
return LightLiSample(ImageLe(uv, lambda), wi, pdf, Interaction(ctx.p() + wi * (2 * sceneRadius), &mediumInterface));

Figure 12.18 illustrates how much error is reduced by sampling image infinite lights well. It compares three images of a dragon model illuminated by the morning skylight environment map from Figure 12.17. The first image was rendered using a simple uniform spherical sampling distribution for selecting incident illumination directions, the second used the full image-based sampling distribution, and the third used the compensated distribution—all rendered with 32 samples per pixel. For the same number of samples taken and with negligible additional computational cost, both importance sampling methods give a much better result with much lower variance.

Figure 12.18: Dragon Model Illuminated by the Morning Skylight Environment Map. All images were rendered with 32 samples per pixel. (a) Rendered using a uniform sampling distribution. (b) Rendered with samples distributed according to environment map image pixels. (c) Rendered using the compensated distribution that skips sampling unimportant parts of the image. All images took essentially the same amount of time to render, though (b) has over 38,000 times lower MSE than (a), and (c) further improves MSE by a factor of 1.52. (Dragon model courtesy of the Stanford Computer Graphics Laboratory.)

Most of the work to compute the PDF for a provided direction is handled by the PiecewiseConstant2D distribution. Here as well, the PDF value it returns is divided by 4 pi to account for the area of the unit sphere.

<<ImageInfiniteLight Method Definitions>>= 
Float ImageInfiniteLight::PDF_Li(LightSampleContext ctx, Vector3f w, bool allowIncompletePDF) const { Vector3f wLight = renderFromLight.ApplyInverse(w); Point2f uv = EqualAreaSphereToSquare(wLight); Float pdf = 0; if (allowIncompletePDF) pdf = compensatedDistribution.PDF(uv); else pdf = distribution.PDF(uv); return pdf / (4 * Pi); }

The ImageInfiniteLight::Phi() method, not included here, integrates incident radiance over the sphere by looping over all the image pixels and summing them before multiplying by a factor of 4 pi to account for the area of the unit sphere as well as by the area of a disk of radius sceneRadius.

12.5.3 Portal Image Infinite Lights

ImageInfiniteLights provide a handy sort of light source, though one shortcoming of that class’s implementation is that it does not account for visibility in its sampling routines. Samples that it generates that turn out to be occluded are much less useful than those that do carry illumination to the reference point. While the expense of ray tracing is necessary to fully account for visibility, accounting for even some visibility effects in light sampling can significantly reduce error.

Figure 12.19: Watercolor Scene Illuminated by a Daytime Sky Environment Map. This is a challenging scene to render since the direct lighting calculation only finds illumination when it samples a ray that passes through the window. (a) When rendered with the ImageInfiniteLight and 16 samples per pixel, error is high because the environment map includes a bright sun, though it does not illuminate all parts of the room. For such points, none of the many samples taken toward the sun has any contribution. (b) When rendered using the PortalImageInfiniteLight, results are much better with the same number of samples because the light is able to sample just the part of the environment map that is visible through the window. In this case, MSE is reduced by a factor of 2.82. (Scene courtesy of Angelo Ferretti.)

Consider the scene shown in Figure 12.19, where all the illumination is coming from a skylight environment map that is visible only through the windows. Part of the scene is directly illuminated by the sun, but much of it is not. Those other parts are still illuminated, but by much less bright regions of blue sky. Yet because the sun is so bright, the ImageInfiniteLight ends up taking many samples in its direction, though all the ones where the sun is not visible through the window will be wasted. In those regions of the scene, light sampling will occasionally choose a part of the sky that is visible through the window and occasionally BSDF sampling will find a path to the light through the window, so that the result is still correct in expectation, but many samples may be necessary to achieve a high-quality result.

The PortalImageInfiniteLight is designed to handle this situation more effectively. Given a user-specified portal, a quadrilateral region through which the environment map is potentially visible, it generates a custom sampling distribution at each point being shaded so that it can draw samples according to the region of the environment map that is visible through the portal. For an equal number of samples, this can be much more effective than the ImageInfiniteLight’s approach, as shown in Figure 12.19(b).

<<PortalImageInfiniteLight Definition>>= 
class PortalImageInfiniteLight : public LightBase { public: <<PortalImageInfiniteLight Public Methods>> 
PortalImageInfiniteLight(const Transform &renderFromLight, Image image, const RGBColorSpace *imageColorSpace, Float scale, const std::string &filename, std::vector<Point3f> portal, Allocator alloc); void Preprocess(const Bounds3f &sceneBounds) { sceneBounds.BoundingSphere(&sceneCenter, &sceneRadius); } SampledSpectrum Phi(SampledWavelengths lambda) const; PBRT_CPU_GPU SampledSpectrum Le(const Ray &ray, const SampledWavelengths &lambda) const; PBRT_CPU_GPU pstd::optional<LightLiSample> SampleLi(LightSampleContext ctx, Point2f u, SampledWavelengths lambda, bool allowIncompletePDF) const; PBRT_CPU_GPU Float PDF_Li(LightSampleContext, Vector3f, bool allowIncompletePDF) const; PBRT_CPU_GPU pstd::optional<LightLeSample> SampleLe(Point2f u1, Point2f u2, SampledWavelengths &lambda, Float time) const; PBRT_CPU_GPU void PDF_Le(const Ray &, Float *pdfPos, Float *pdfDir) const; PBRT_CPU_GPU void PDF_Le(const Interaction &, Vector3f w, Float *pdfPos, Float *pdfDir) const { LOG_FATAL("Shouldn't be called for non-area lights"); } pstd::optional<LightBounds> Bounds() const { return {}; } std::string ToString() const;
private: <<PortalImageInfiniteLight Private Methods>> 
PBRT_CPU_GPU SampledSpectrum ImageLookup(Point2f uv, const SampledWavelengths &lambda) const; pstd::optional<Point2f> ImageFromRender(Vector3f wRender, Float *duv_dw = nullptr) const { Vector3f w = portalFrame.ToLocal(wRender); if (w.z <= 0) return {}; <<Compute Jacobian determinant of mapping normal d left-parenthesis u comma v right-parenthesis slash normal d omega if needed>> 
if (duv_dw) *duv_dw = Sqr(Pi) * (1 - Sqr(w.x)) * (1 - Sqr(w.y)) / w.z;
Float alpha = std::atan2(w.x, w.z), beta = std::atan2(w.y, w.z); return Point2f(Clamp((alpha + Pi / 2) / Pi, 0, 1), Clamp((beta + Pi / 2) / Pi, 0, 1)); } Vector3f RenderFromImage(Point2f uv, Float *duv_dw = nullptr) const { Float alpha = -Pi / 2 + uv[0] * Pi, beta = -Pi / 2 + uv[1] * Pi; Float x = std::tan(alpha), y = std::tan(beta); Vector3f w = Normalize(Vector3f(x, y, 1)); <<Compute Jacobian determinant of mapping normal d left-parenthesis u comma v right-parenthesis slash normal d omega if needed>> 
if (duv_dw) *duv_dw = Sqr(Pi) * (1 - Sqr(w.x)) * (1 - Sqr(w.y)) / w.z;
return portalFrame.FromLocal(w); } pstd::optional<Bounds2f> ImageBounds(Point3f p) const { pstd::optional<Point2f> p0 = ImageFromRender(Normalize(portal[0] - p)); pstd::optional<Point2f> p1 = ImageFromRender(Normalize(portal[2] - p)); if (!p0 || !p1) return {}; return Bounds2f(*p0, *p1); } Float Area() const { return Length(portal[1] - portal[0]) * Length(portal[3] - portal[0]); }
<<PortalImageInfiniteLight Private Members>> 
pstd::array<Point3f, 4> portal; Frame portalFrame; Image image; WindowedPiecewiseConstant2D distribution; const RGBColorSpace *imageColorSpace; Float scale; Float sceneRadius; std::string filename; Point3f sceneCenter;
};

Figure 12.20: Given a scene with a portal (opening in the box), for each point in the scene we can find the set of directions that pass through the portal. To sample illumination efficiently, we would like to only sample from the corresponding visible region of the environment map (thick segment on the sphere).

Given a portal and a point in the scene, there is a set of directions from that point that pass through the portal. If we can find the corresponding region of the environment map, then our task is to sample from it according to the environment map’s emission. This idea is illustrated in Figure 12.20. With the equal-area mapping, the shape of the visible region of the environment map seen from a given point can be complex. The problem is illustrated in Figure 12.21(a), which visualizes the visible regions from two points in the scene from Figure 12.19.

Figure 12.21: Shapes of Visible Regions of an Environment Map as Seen through a Portal. These images illustrate the visible regions of the environment map as seen through the window for a point on the floor and a point on one of the paintings on the wall for the scene in Figure 12.19. (a) The equal-area mapping used by the ImageInfiniteLight is convenient for sampling the entire environment map, but it leads to the portal-visible regions having complex shapes. (b) With the directional parameterization used by the PortalImageInfiniteLight, the visible region is always rectangular, which makes it feasible to sample from just that part of it. (Environment map courtesy of Sergej Majboroda, via Poly Haven.)

The PortalImageInfiniteLight therefore uses a different parameterization of directions that causes the visible region seen through a portal to always be rectangular. Later in this section, we will see how this property makes efficient sampling possible.

The directional parameterization used by the PortalImageInfiniteLight is based on a coordinate system where the x and y axes are aligned with the edges of the portal. Note that the position of the portal is not used in defining this coordinate system—only the directions of its edges. As a first indication that this idea is getting us somewhere, consider the vectors from a point in the scene to the four corners of the portal, transformed into this coordinate system. It should be evident that in this coordinate system, vectors to adjacent vertices of the portal only differ in one of their x or y coordinate values and that the four directions thus span a rectangular region in x y . (If this is not clear, it is a detail worth pausing to work through.) We will term directional representations that have this property as rectified.

The x y components of vectors in this coordinate system still span left-parenthesis negative normal infinity comma normal infinity right-parenthesis , so it is necessary to map them to a finite 2D domain if the environment map is to be represented using an image. It is important that this mapping does not interfere with the axis-alignment of the portal edges and that rectification is preserved. This requirement rules out a number of possibilities, including both the equirectangular and equal-area mappings. Even normalizing a vector and taking the x and y coordinates of the resulting unit vector is unsuitable given this requirement.

A mapping that does work is based on the angles alpha and beta that the x and y coordinates of the vector respectively make with the z axis, as illustrated in Figure 12.22. These angles are given by

left-parenthesis alpha comma beta right-parenthesis equals left-parenthesis arc tangent StartFraction x Over z EndFraction comma arc tangent StartFraction y Over z EndFraction right-parenthesis period

We can ignore vectors with negative z components in the rectified coordinate system: they face away from the portal and thus do not receive any illumination. Each of alpha and beta then spans the range left-bracket negative pi slash 2 comma pi slash 2 right-bracket and the pair of them can be easily mapped to left-bracket 0 comma 1 right-bracket squared left-parenthesis u comma v right-parenthesis image coordinates. The environment map resampled into this parameterization is shown in Figure 12.21(b), with the visible regions for the same two points in the scene indicated.

Figure 12.22: Vectors in the portal’s coordinate system can be represented by a pair of angles left-parenthesis alpha comma beta right-parenthesis that measure the angle made by the x or y component, respectively, with the z axis.

We will start the implementation of the PortalImageInfiniteLight with its ImageFromRender() method, which applies this mapping to a vector in the rendering coordinate system wRender. (We will come to the initialization of the portalFrame member variable in the PortalImageInfiniteLight constructor later in this section.) It uses pstd::optional for the return value in order to be able to return an invalid result in the case that the vector is coplanar with the portal or facing away from it.

<<PortalImageInfiniteLight Private Methods>>= 
pstd::optional<Point2f> ImageFromRender(Vector3f wRender, Float *duv_dw = nullptr) const { Vector3f w = portalFrame.ToLocal(wRender); if (w.z <= 0) return {}; <<Compute Jacobian determinant of mapping normal d left-parenthesis u comma v right-parenthesis slash normal d omega if needed>> 
if (duv_dw) *duv_dw = Sqr(Pi) * (1 - Sqr(w.x)) * (1 - Sqr(w.y)) / w.z;
Float alpha = std::atan2(w.x, w.z), beta = std::atan2(w.y, w.z); return Point2f(Clamp((alpha + Pi / 2) / Pi, 0, 1), Clamp((beta + Pi / 2) / Pi, 0, 1)); }

We will find it useful to be able to convert sampling densities from the left-parenthesis u comma v right-parenthesis parameterization of the image to be with respect to solid angle on the unit sphere. The appropriate factor can be found following the usual approach of computing the determinant of the Jacobian of the mapping function, which is based on Equation (12.1), and then rescaling the coordinates to image coordinates in left-bracket 0 comma 1 right-bracket squared . The result is a simple expression when expressed in terms of  omega Subscript :

StartFraction normal d left-parenthesis u comma v right-parenthesis Over normal d omega Subscript Baseline EndFraction equals pi squared StartFraction left-parenthesis 1 minus omega Subscript Baseline Subscript x Superscript 2 Baseline right-parenthesis left-parenthesis 1 minus omega Subscript Baseline Subscript y Superscript 2 Baseline right-parenthesis Over omega Subscript Baseline Subscript z Baseline EndFraction period

If a non-nullptr duv_dw parameter is passed to this method, this factor is returned.

<<Compute Jacobian determinant of mapping normal d left-parenthesis u comma v right-parenthesis slash normal d omega if needed>>= 
if (duv_dw) *duv_dw = Sqr(Pi) * (1 - Sqr(w.x)) * (1 - Sqr(w.y)) / w.z;

The inverse transformation can be found by working in reverse. It is implemented in RenderFromImage(), which also optionally returns the same change of variables factor.

<<PortalImageInfiniteLight Private Methods>>+=  
Vector3f RenderFromImage(Point2f uv, Float *duv_dw = nullptr) const { Float alpha = -Pi / 2 + uv[0] * Pi, beta = -Pi / 2 + uv[1] * Pi; Float x = std::tan(alpha), y = std::tan(beta); Vector3f w = Normalize(Vector3f(x, y, 1)); <<Compute Jacobian determinant of mapping normal d left-parenthesis u comma v right-parenthesis slash normal d omega if needed>> 
if (duv_dw) *duv_dw = Sqr(Pi) * (1 - Sqr(w.x)) * (1 - Sqr(w.y)) / w.z;
return portalFrame.FromLocal(w); }

Because the mapping is rectified, we can find the image-space bounding box of the visible region of the environment map from a given point using the coordinates of two opposite portal corners. This method also returns an optional value, for the same reasons as for ImageFromRender().

<<PortalImageInfiniteLight Private Methods>>+= 
pstd::optional<Bounds2f> ImageBounds(Point3f p) const { pstd::optional<Point2f> p0 = ImageFromRender(Normalize(portal[0] - p)); pstd::optional<Point2f> p1 = ImageFromRender(Normalize(portal[2] - p)); if (!p0 || !p1) return {}; return Bounds2f(*p0, *p1); }

Most of the PortalImageInfiniteLight constructor consists of straightforward initialization of member variables from provided parameter values, checking that the provided image has RGB channels, and so forth. All of that has not been included in this text. We will, however, discuss the following three fragments, which run at the end of the constructor.

<<PortalImageInfiniteLight constructor conclusion>>= 
<<Compute frame for portal coordinate system>> 
Vector3f p01 = Normalize(portal[1] - portal[0]); Vector3f p03 = Normalize(portal[3] - portal[0]); portalFrame = Frame::FromXY(p03, p01);
<<Resample environment map into rectified image>> 
image = Image(PixelFormat::Float, equalAreaImage.Resolution(), {"R", "G", "B"}, equalAreaImage.Encoding(), alloc); ParallelFor(0, image.Resolution().y, [&](int y) { for (int x = 0; x < image.Resolution().x; ++x) { <<Resample equalAreaImage to compute rectified image pixel left-parenthesis x comma y right-parenthesis >> 
<<Find left-parenthesis u comma v right-parenthesis coordinates in equal-area image for pixel>> 
Point2f uv((x + 0.5f) / image.Resolution().x, (y + 0.5f) / image.Resolution().y); Vector3f w = RenderFromImage(uv); w = Normalize(renderFromLight.ApplyInverse(w)); Point2f uvEqui = EqualAreaSphereToSquare(w);
for (int c = 0; c < 3; ++c) { Float v = equalAreaImage.BilerpChannel(uvEqui, c, WrapMode::OctahedralSphere); image.SetChannel({x, y}, c, v); }
} });
<<Initialize sampling distribution for portal image infinite light>> 
auto duv_dw = [&](Point2f p) { Float duv_dw; (void)RenderFromImage(p, &duv_dw); return duv_dw; }; Array2D<Float> d = image.GetSamplingDistribution(duv_dw); distribution = WindowedPiecewiseConstant2D(d, alloc);

The portal itself is specified by four vertices, given in the rendering coordinate system. Additional code, not shown here, checks to ensure that they describe a planar quadrilateral. A Frame for the portal’s coordinate system can be found from two normalized adjacent edge vectors of the portal using the Frame::FromXY() method.

<<Compute frame for portal coordinate system>>= 
Vector3f p01 = Normalize(portal[1] - portal[0]); Vector3f p03 = Normalize(portal[3] - portal[0]); portalFrame = Frame::FromXY(p03, p01);

<<PortalImageInfiniteLight Private Members>>= 
pstd::array<Point3f, 4> portal; Frame portalFrame;

The constructor also resamples a provided equal-area image into the rectified representation at the same resolution. Because the rectified image depends on the geometry of the portal, it is better to take an equal-area image and resample it in the constructor than to require the user to provide an already-rectified image. In this way, it is easy for the user to change the portal specification just by changing the portal’s coordinates in the scene description file.

<<Resample environment map into rectified image>>= 
image = Image(PixelFormat::Float, equalAreaImage.Resolution(), {"R", "G", "B"}, equalAreaImage.Encoding(), alloc); ParallelFor(0, image.Resolution().y, [&](int y) { for (int x = 0; x < image.Resolution().x; ++x) { <<Resample equalAreaImage to compute rectified image pixel left-parenthesis x comma y right-parenthesis >> 
<<Find left-parenthesis u comma v right-parenthesis coordinates in equal-area image for pixel>> 
Point2f uv((x + 0.5f) / image.Resolution().x, (y + 0.5f) / image.Resolution().y); Vector3f w = RenderFromImage(uv); w = Normalize(renderFromLight.ApplyInverse(w)); Point2f uvEqui = EqualAreaSphereToSquare(w);
for (int c = 0; c < 3; ++c) { Float v = equalAreaImage.BilerpChannel(uvEqui, c, WrapMode::OctahedralSphere); image.SetChannel({x, y}, c, v); }
} });

<<PortalImageInfiniteLight Private Members>>+=  
Image image;

At each rectified image pixel, the implementation first computes the corresponding light-space direction and looks up a bilinearly interpolated value from the equal-area image. No further filtering is performed. A better implementation would use a spatially varying filter here in order to ensure that there was no risk of introducing aliasing due to undersampling the source image.

<<Resample equalAreaImage to compute rectified image pixel left-parenthesis x comma y right-parenthesis >>= 
<<Find left-parenthesis u comma v right-parenthesis coordinates in equal-area image for pixel>> 
Point2f uv((x + 0.5f) / image.Resolution().x, (y + 0.5f) / image.Resolution().y); Vector3f w = RenderFromImage(uv); w = Normalize(renderFromLight.ApplyInverse(w)); Point2f uvEqui = EqualAreaSphereToSquare(w);
for (int c = 0; c < 3; ++c) { Float v = equalAreaImage.BilerpChannel(uvEqui, c, WrapMode::OctahedralSphere); image.SetChannel({x, y}, c, v); }

The image coordinates in the equal-area image can be found by determining the direction vector corresponding to the current pixel in the rectified image and then finding the equal-area image coordinates that this direction maps to.

<<Find left-parenthesis u comma v right-parenthesis coordinates in equal-area image for pixel>>= 
Point2f uv((x + 0.5f) / image.Resolution().x, (y + 0.5f) / image.Resolution().y); Vector3f w = RenderFromImage(uv); w = Normalize(renderFromLight.ApplyInverse(w)); Point2f uvEqui = EqualAreaSphereToSquare(w);

Given the rectified image, the next step is to initialize an instance of the WindowedPiecewiseConstant2D data structure, which performs the sampling operation. (It is defined in Section A.5.6.) As its name suggests, it generalizes the functionality of the PiecewiseConstant2D class to allow a caller-specified window that limits the sampling region.

It is worthwhile to include the change of variables factor normal d left-parenthesis u comma v right-parenthesis slash normal d omega Subscript at each pixel in the image sampling distribution. Doing so causes the weights associated with image samples to be more uniform, as this factor will nearly cancel the same factor when a sample’s PDF is computed. (The cancellation is not exact, as the factor here is computed at the center of each pixel while in the PDF it is computed at the exact sample location.)

<<Initialize sampling distribution for portal image infinite light>>= 
auto duv_dw = [&](Point2f p) { Float duv_dw; (void)RenderFromImage(p, &duv_dw); return duv_dw; }; Array2D<Float> d = image.GetSamplingDistribution(duv_dw); distribution = WindowedPiecewiseConstant2D(d, alloc);

<<PortalImageInfiniteLight Private Members>>+=  
WindowedPiecewiseConstant2D distribution;

The light’s total power can be found by integrating radiance over the hemisphere of directions that can be seen through the portal and then multiplying by the portal’s area, since all light that reaches the scene passes through it. The corresponding PortalImageInfiniteLight::Phi() method is not included here, as it boils down to being a matter of looping over the pixels, applying the change of variables factor to account for integration over the unit sphere, and then multiplying the integrated radiance by the portal’s area.

In order to compute the radiance for a ray that has left the scene, the left-parenthesis u comma v right-parenthesis coordinates in the image corresponding to the ray’s direction are computed first. The radiance corresponding to those coordinates is returned if they are inside the portal bounds for the ray origin, and a zero-valued spectrum is returned otherwise. (In principle, the Le() method should only be called for rays that have left the scene, so that the portal check should always pass, but it is worth including for the occasional ray that escapes the scene due to a geometric error in the scene model. This way, those end up carrying no radiance rather than causing a light leak.)

<<PortalImageInfiniteLight Method Definitions>>= 
SampledSpectrum PortalImageInfiniteLight::Le(const Ray &ray, const SampledWavelengths &lambda) const { pstd::optional<Point2f> uv = ImageFromRender(Normalize(ray.d)); pstd::optional<Bounds2f> b = ImageBounds(ray.o); if (!uv || !b || !Inside(*uv, *b)) return SampledSpectrum(0.f); return ImageLookup(*uv, lambda); }

The ImageLookup() method returns the radiance at the given image left-parenthesis u comma v right-parenthesis and wavelengths. We encapsulate this functionality in its own method, as it will be useful repeatedly in the remainder of the light’s implementation.

<<PortalImageInfiniteLight Method Definitions>>+=  
SampledSpectrum PortalImageInfiniteLight::ImageLookup( Point2f uv, const SampledWavelengths &lambda) const { RGB rgb; for (int c = 0; c < 3; ++c) rgb[c] = image.LookupNearestChannel(uv, c); RGBIlluminantSpectrum spec(*imageColorSpace, ClampZero(rgb)); return scale * spec.Sample(lambda); }

As before, the image’s color space must be known in order to convert its RGB values to spectra.

<<PortalImageInfiniteLight Private Members>>+=  
const RGBColorSpace *imageColorSpace; Float scale;

SampleLi() is able to take advantage of the combination of the rectified image representation and the ability of WindowedPiecewiseConstant2D to sample a direction from the specified point that passes through the portal, according to the directional distribution of radiance over the portal.

<<PortalImageInfiniteLight Method Definitions>>+=  
pstd::optional<LightLiSample> PortalImageInfiniteLight::SampleLi(LightSampleContext ctx, Point2f u, SampledWavelengths lambda, bool allowIncompletePDF) const { <<Sample left-parenthesis u comma v right-parenthesis in potentially visible region of light image>> 
pstd::optional<Bounds2f> b = ImageBounds(ctx.p()); if (!b) return {}; Float mapPDF; pstd::optional<Point2f> uv = distribution.Sample(u, *b, &mapPDF); if (!uv) return {};
<<Convert portal image sample point to direction and compute PDF>> 
Float duv_dw; Vector3f wi = RenderFromImage(*uv, &duv_dw); if (duv_dw == 0) return {}; Float pdf = mapPDF / duv_dw;
<<Compute radiance for portal light sample and return LightLiSample>> 
SampledSpectrum L = ImageLookup(*uv, lambda); Point3f pl = ctx.p() + 2 * sceneRadius * wi; return LightLiSample(L, wi, pdf, Interaction(pl, &mediumInterface));
}

WindowedPiecewiseConstant2D’s Sample() method takes a Bounds2f to specify the sampling region. This is easily provided using the ImageBounds() method. It may not be able to generate a valid sample—for example, if the point is on the outside of the portal or lies on its plane. In this case, an unset sample is returned.

<<Sample left-parenthesis u comma v right-parenthesis in potentially visible region of light image>>= 
pstd::optional<Bounds2f> b = ImageBounds(ctx.p()); if (!b) return {}; Float mapPDF; pstd::optional<Point2f> uv = distribution.Sample(u, *b, &mapPDF); if (!uv) return {};

After image left-parenthesis u comma v right-parenthesis coordinates are converted to a direction, the method computes the sampling PDF with respect to solid angle at the reference point represented by ctx. Doing so just requires the application of the change of variables factor returned by RenderFromImage().

<<Convert portal image sample point to direction and compute PDF>>= 
Float duv_dw; Vector3f wi = RenderFromImage(*uv, &duv_dw); if (duv_dw == 0) return {}; Float pdf = mapPDF / duv_dw;

The remaining pieces are easy at this point: ImageLookup() provides the radiance for the sampled direction and the endpoint of the shadow ray is found in the same way that is done for the other infinite lights.

<<Compute radiance for portal light sample and return LightLiSample>>= 
SampledSpectrum L = ImageLookup(*uv, lambda); Point3f pl = ctx.p() + 2 * sceneRadius * wi; return LightLiSample(L, wi, pdf, Interaction(pl, &mediumInterface));

Also as with the other infinite lights, the radius of the scene’s bounding sphere is stored when the Preprocess() method, not included here, is called.

<<PortalImageInfiniteLight Private Members>>+= 
Float sceneRadius;

Finding the PDF for a specified direction follows the way in which the PDF was calculated in the sampling method.

<<PortalImageInfiniteLight Method Definitions>>+= 
Float PortalImageInfiniteLight::PDF_Li(LightSampleContext ctx, Vector3f w, bool allowIncompletePDF) const { <<Find image left-parenthesis u comma v right-parenthesis coordinates corresponding to direction w>> 
Float duv_dw; pstd::optional<Point2f> uv = ImageFromRender(w, &duv_dw); if (!uv || duv_dw == 0) return 0;
<<Return PDF for sampling left-parenthesis u comma v right-parenthesis from reference point>> 
pstd::optional<Bounds2f> b = ImageBounds(ctx.p()); if (!b) return 0; Float pdf = distribution.PDF(*uv, *b); return pdf / duv_dw;
}

First, ImageFromRender() gives the left-parenthesis u comma v right-parenthesis coordinates in the portal image for the specified direction.

<<Find image left-parenthesis u comma v right-parenthesis coordinates corresponding to direction w>>= 
Float duv_dw; pstd::optional<Point2f> uv = ImageFromRender(w, &duv_dw); if (!uv || duv_dw == 0) return 0;

Following its Sample() method, the WindowedPiecewiseConstant2D::PDF() method also takes a 2D bounding box to window the function. The PDF value it returns is normalized with respect to those bounds and a value of zero is returned if the given point is outside of them. Application of the change of variables factor gives the final PDF with respect to solid angle.

<<Return PDF for sampling left-parenthesis u comma v right-parenthesis from reference point>>= 
pstd::optional<Bounds2f> b = ImageBounds(ctx.p()); if (!b) return 0; Float pdf = distribution.PDF(*uv, *b); return pdf / duv_dw;