12.2 Point Lights

A number of interesting lights can be described in terms of emission from a single point in space with some possibly angularly varying distribution of outgoing light. This section describes the implementation of a number of them, starting with PointLight, which represents an isotropic point light source that emits the same amount of light in all directions. (Figure 12.3 shows a scene rendered with a point light source.) Building on this base, a number of more complex lights based on point sources will then be introduced, including spotlights and a light that projects an image into the scene.

Figure 12.3: Scene Rendered with a Point Light Source. Notice the hard shadow boundaries from this type of light. (Dragon model courtesy of the Stanford Computer Graphics Laboratory.)

<<PointLight Definition>>= 
class PointLight : public LightBase { public: <<PointLight Public Methods>> 
PointLight(Transform renderFromLight, MediumInterface mediumInterface, Spectrum I, Float scale) : LightBase(LightType::DeltaPosition, renderFromLight, mediumInterface), I(LookupSpectrum(I)), scale(scale) {} static PointLight *Create(const Transform &renderFromLight, Medium medium, const ParameterDictionary &parameters, const RGBColorSpace *colorSpace, const FileLoc *loc, Allocator alloc); SampledSpectrum Phi(SampledWavelengths lambda) const; void Preprocess(const Bounds3f &sceneBounds) {} 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; std::string ToString() const; pstd::optional<LightLiSample> SampleLi(LightSampleContext ctx, Point2f u, SampledWavelengths lambda, bool allowIncompletePDF) const { Point3f p = renderFromLight(Point3f(0, 0, 0)); Vector3f wi = Normalize(p - ctx.p()); SampledSpectrum Li = scale * I->Sample(lambda) / DistanceSquared(p, ctx.p()); return LightLiSample(Li, wi, 1, Interaction(p, &mediumInterface)); } Float PDF_Li(LightSampleContext, Vector3f, bool allowIncompletePDF) const { return 0; }
private: <<PointLight Private Members>> 
const DenselySampledSpectrum *I; Float scale;
};

PointLights are positioned at the origin in the light coordinate system. To place them elsewhere, the rendering-from-light transformation should be set accordingly. In addition to passing the common light parameters to LightBase, the constructor supplies LightType::DeltaPosition for its light type, since point lights represent singularities that only emit light from a single position. The constructor also stores the light’s intensity (Section 4.1.1).

<<PointLight Public Methods>>= 
PointLight(Transform renderFromLight, MediumInterface mediumInterface, Spectrum I, Float scale) : LightBase(LightType::DeltaPosition, renderFromLight, mediumInterface), I(LookupSpectrum(I)), scale(scale) {}

As HomogeneousMedium and GridMedium did with spectral scattering coefficients, PointLight uses a DenselySampledSpectrum rather than a Spectrum for the spectral intensity, trading off storage for more efficient spectral sampling operations.

<<PointLight Private Members>>= 
const DenselySampledSpectrum *I; Float scale;

Strictly speaking, it is incorrect to describe the light arriving at a point due to a point light source using units of radiance. Radiant intensity is instead the proper unit for describing emission from a point light source, as explained in Section 4.1. In the light source interfaces here, however, we will abuse terminology and use SampleLi() methods to report the illumination arriving at a point for all types of light sources, dividing radiant intensity by the squared distance to the point  normal p Subscript to convert units. In the end, the correctness of the computation does not suffer from this fudge, and it makes the implementation of light transport algorithms more straightforward by not requiring them to use different interfaces for different types of lights.

Point lights are described by a delta distribution such that they only illuminate a receiving point from a single direction. Thus, the sampling problem is deterministic and makes no use of the random sample u. We find the light’s position p in the rendering coordinate system and sample its spectral emission at the provided wavelengths. Note that a PDF value of 1 is returned in the LightLiSample: there is implicitly a Dirac delta distribution in both the radiance and the PDF that cancels when the Monte Carlo estimator is evaluated.

<<PointLight Public Methods>>+=  
pstd::optional<LightLiSample> SampleLi(LightSampleContext ctx, Point2f u, SampledWavelengths lambda, bool allowIncompletePDF) const { Point3f p = renderFromLight(Point3f(0, 0, 0)); Vector3f wi = Normalize(p - ctx.p()); SampledSpectrum Li = scale * I->Sample(lambda) / DistanceSquared(p, ctx.p()); return LightLiSample(Li, wi, 1, Interaction(p, &mediumInterface)); }

Due to the delta distribution, the PointLight::PDF_Li() method returns 0. This value reflects the fact that there is no chance for some other sampling process to randomly generate a direction that would intersect an infinitesimal light source.

<<PointLight Public Methods>>+= 
Float PDF_Li(LightSampleContext, Vector3f, bool allowIncompletePDF) const { return 0; }

The total power emitted by the light source can be found by integrating the intensity over the entire sphere of directions:

normal upper Phi equals integral Underscript script upper S squared Endscripts upper I normal d omega Subscript Baseline equals upper I integral Underscript script upper S squared Endscripts normal d omega Subscript Baseline equals 4 pi upper I period

Radiant power is returned by the Phi() method and not the luminous power that may have been used to specify the light source.

<<PointLight Method Definitions>>= 
SampledSpectrum PointLight::Phi(SampledWavelengths lambda) const { return 4 * Pi * scale * I->Sample(lambda); }

12.2.1 Spotlights

Spotlights are a handy variation on point lights; rather than shining illumination in all directions, they emit light in a cone of directions from their position. For simplicity, we will define the spotlight in the light coordinate system to always be at position left-parenthesis 0 comma 0 comma 0 right-parenthesis and pointing down the plus z axis. To place or orient it elsewhere in the scene, the rendering-from-light transformation should be set accordingly. Figure 12.4 shows a rendering of the same scene as Figure 12.3, illuminated with a spotlight instead of a point light.

Figure 12.4: Scene Rendered with a Spotlight. The spotlight cone smoothly cuts off illumination past a user-specified angle from the light’s central axis. (Dragon model courtesy of the Stanford Computer Graphics Laboratory.)

<<SpotLight Definition>>= 
class SpotLight : public LightBase { public: <<SpotLight Public Methods>> 
SpotLight(const Transform &renderFromLight, const MediumInterface &m, Spectrum I, Float scale, Float totalWidth, Float falloffStart); static SpotLight *Create(const Transform &renderFromLight, Medium medium, const ParameterDictionary &parameters, const RGBColorSpace *colorSpace, const FileLoc *loc, Allocator alloc); void Preprocess(const Bounds3f &sceneBounds) {} PBRT_CPU_GPU SampledSpectrum I(Vector3f w, SampledWavelengths) const; 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"); } pstd::optional<LightBounds> Bounds() const; std::string ToString() const; pstd::optional<LightLiSample> SampleLi(LightSampleContext ctx, Point2f u, SampledWavelengths lambda, bool allowIncompletePDF) const { Point3f p = renderFromLight(Point3f(0, 0, 0)); Vector3f wi = Normalize(p - ctx.p()); <<Compute incident radiance Li for SpotLight>> 
Vector3f wLight = Normalize(renderFromLight.ApplyInverse(-wi)); SampledSpectrum Li = I(wLight, lambda) / DistanceSquared(p, ctx.p());
if (!Li) return {}; return LightLiSample(Li, wi, 1, Interaction(p, &mediumInterface)); }
private: <<SpotLight Private Members>> 
const DenselySampledSpectrum *Iemit; Float scale, cosFalloffStart, cosFalloffEnd;
};

Figure 12.5: Spotlights are defined by two angles, falloffStart and totalWidth, that are measured with respect to the z axis in light space. Objects inside the inner cone of angles, up to falloffStart, are fully illuminated by the light. The directions between falloffStart and totalWidth are a transition zone that ramps down from full illumination to no illumination, such that points outside the totalWidth cone are not illuminated at all. The cosine of the angle theta between the vector to a point  normal p and the spotlight axis can easily be computed with a dot product.

There is not anything interesting in the SpotLight constructor, so it is not included here. It is given angles that set the extent of the SpotLight’s cone—the overall angular width of the cone and the angle at which falloff starts (Figure 12.5)—but it stores the cosines of these angles, which are more useful to have at hand in the SpotLight’s methods.

<<SpotLight Private Members>>= 
const DenselySampledSpectrum *Iemit; Float scale, cosFalloffStart, cosFalloffEnd;

The SpotLight::SampleLi() method is of similar form to that of PointLight::SampleLi(), though an unset sample is returned if the receiving point is outside of the spotlight’s outer cone and thus receives zero radiance.

<<SpotLight Public Methods>>= 
pstd::optional<LightLiSample> SampleLi(LightSampleContext ctx, Point2f u, SampledWavelengths lambda, bool allowIncompletePDF) const { Point3f p = renderFromLight(Point3f(0, 0, 0)); Vector3f wi = Normalize(p - ctx.p()); <<Compute incident radiance Li for SpotLight>> 
Vector3f wLight = Normalize(renderFromLight.ApplyInverse(-wi)); SampledSpectrum Li = I(wLight, lambda) / DistanceSquared(p, ctx.p());
if (!Li) return {}; return LightLiSample(Li, wi, 1, Interaction(p, &mediumInterface)); }

The I() method computes the distribution of light accounting for the spotlight cone. This computation is encapsulated in a separate method since other SpotLight methods will need to perform it as well.

<<Compute incident radiance Li for SpotLight>>= 
Vector3f wLight = Normalize(renderFromLight.ApplyInverse(-wi)); SampledSpectrum Li = I(wLight, lambda) / DistanceSquared(p, ctx.p());

As with point lights, the SpotLight’s PDF_Li() method always returns zero. It is not included here.

To compute the spotlight’s strength for a direction leaving the light, the first step is to compute the cosine of the angle between that direction and the vector along the center of the spotlight’s cone. Because the spotlight is oriented to point down the plus z axis, the CosTheta() function can be used to do so.

The SmoothStep() function is then used to modulate the emission according to the cosine of the angle: it returns 0 if the provided value is below cosFalloffEnd, 1 if it is above cosFalloffStart, and it interpolates between 0 and 1 for intermediate values using a cubic curve. (To understand its usage, keep in mind that for theta element-of left-bracket 0 comma pi right-bracket , as is the case here, if theta greater-than theta prime , then cosine theta less-than cosine theta prime .)

<<SpotLight Method Definitions>>= 
SampledSpectrum SpotLight::I(Vector3f w, SampledWavelengths lambda) const { return SmoothStep(CosTheta(w), cosFalloffEnd, cosFalloffStart) * scale * Iemit->Sample(lambda); }

To compute the power emitted by a spotlight, it is necessary to integrate the falloff function over the sphere. In spherical coordinates, theta and phi are separable, so we just need to integrate over theta and scale the result by 2 pi . For the part that lies inside the inner cone of full power, we have

integral Subscript 0 Superscript theta Subscript normal s normal t normal a normal r normal t Baseline Baseline sine theta normal d theta equals 1 minus cosine theta Subscript normal s normal t normal a normal r normal t Baseline period

The falloff region works out simply, thanks in part to SmoothStep() being a polynomial.

integral Subscript theta Subscript normal s normal t normal a normal r normal t Baseline Superscript theta Subscript normal e normal n normal d Baseline Baseline normal s normal m normal o normal o normal t normal h normal s normal t normal e normal p left-parenthesis cosine theta comma theta Subscript normal e normal n normal d Baseline comma theta Subscript normal s normal t normal a normal r normal t Baseline right-parenthesis sine theta normal d theta equals StartFraction cosine theta Subscript normal s normal t normal a normal r normal t Baseline minus cosine theta Subscript normal e normal n normal d Baseline Over 2 EndFraction period

<<SpotLight Method Definitions>>+=  
SampledSpectrum SpotLight::Phi(SampledWavelengths lambda) const { return scale * Iemit->Sample(lambda) * 2 * Pi * ((1 - cosFalloffStart) + (cosFalloffStart - cosFalloffEnd) / 2); }

12.2.2 Texture Projection Lights

Another useful light source acts like a slide projector; it takes an image map and projects its image out into the scene. The ProjectionLight class uses a projective transformation to project points in the scene onto the light’s projection plane based on the field of view angle given to the constructor (Figure 12.6).

Figure 12.6: The Basic Setting for Projection Light Sources. A point normal p Subscript in the light’s coordinate system is projected onto the plane of the image using the light’s projection matrix.

The use of this light in the lighting example scene is shown in Figure 12.7.

Figure 12.7: Scene Rendered with a Projection Light Using a Grid Image. The projection light acts like a slide projector, projecting an image onto objects in the scene. (Dragon model courtesy of the Stanford Computer Graphics Laboratory.)

<<ProjectionLight Definition>>= 
class ProjectionLight : public LightBase { public: <<ProjectionLight Public Methods>> 
ProjectionLight(Transform renderFromLight, MediumInterface medium, Image image, const RGBColorSpace *colorSpace, Float scale, Float fov, Allocator alloc); static ProjectionLight *Create(const Transform &renderFromLight, Medium medium, const ParameterDictionary &parameters, const FileLoc *loc, Allocator alloc); void Preprocess(const Bounds3f &sceneBounds) {} PBRT_CPU_GPU pstd::optional<LightLiSample> SampleLi(LightSampleContext ctx, Point2f u, SampledWavelengths lambda, bool allowIncompletePDF) const; PBRT_CPU_GPU SampledSpectrum I(Vector3f w, const SampledWavelengths &lambda) const; 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"); } pstd::optional<LightBounds> Bounds() const; std::string ToString() const;
private: <<ProjectionLight Private Members>> 
Image image; const RGBColorSpace *imageColorSpace; Float scale; Bounds2f screenBounds; Float hither = 1e-3f; Transform screenFromLight, lightFromScreen; Float A;
};

This light could use a Texture to represent the light projection distribution so that procedural projection patterns could be used. However, having a tabularized representation of the projection function makes it easier to sample with probability proportional to the projection function. Therefore, the Image class is used to specify the projection pattern.

<<ProjectionLight Method Definitions>>= 
ProjectionLight::ProjectionLight( Transform renderFromLight, MediumInterface mediumInterface, Image im, const RGBColorSpace *imageColorSpace, Float scale, Float fov, Allocator alloc) : LightBase(LightType::DeltaPosition, renderFromLight, mediumInterface), image(std::move(im)), imageColorSpace(imageColorSpace), scale(scale), distrib(alloc) { <<ProjectionLight constructor implementation>> 
<<Initialize ProjectionLight projection matrix>> 
Float aspect = Float(image.Resolution().x) / Float(image.Resolution().y); if (aspect > 1) screenBounds = Bounds2f(Point2f(-aspect, -1), Point2f(aspect, 1)); else screenBounds = Bounds2f(Point2f(-1, -1/aspect), Point2f(1, 1/aspect)); screenFromLight = Perspective(fov, hither, 1e30f /* yon */); lightFromScreen = Inverse(screenFromLight);
<<Compute projection image area A>> 
Float opposite = std::tan(Radians(fov) / 2); A = 4 * Sqr(opposite) * (aspect > 1 ? aspect : (1 / aspect));
<<Compute sampling distribution for ProjectionLight>> 
}

A color space for the image is stored so that it is possible to convert image RGB values to spectra.

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

The constructor has more work to do than the ones we have seen so far, including initializing a projection matrix and computing the area of the projected image on the projection plane.

<<ProjectionLight constructor implementation>>= 
<<Initialize ProjectionLight projection matrix>> 
Float aspect = Float(image.Resolution().x) / Float(image.Resolution().y); if (aspect > 1) screenBounds = Bounds2f(Point2f(-aspect, -1), Point2f(aspect, 1)); else screenBounds = Bounds2f(Point2f(-1, -1/aspect), Point2f(1, 1/aspect)); screenFromLight = Perspective(fov, hither, 1e30f /* yon */); lightFromScreen = Inverse(screenFromLight);
<<Compute projection image area A>> 
Float opposite = std::tan(Radians(fov) / 2); A = 4 * Sqr(opposite) * (aspect > 1 ? aspect : (1 / aspect));
<<Compute sampling distribution for ProjectionLight>> 

First, similar to the PerspectiveCamera, the ProjectionLight constructor computes a projection matrix and the screen space extent of the projection on the z equals 1 plane.

<<Initialize ProjectionLight projection matrix>>= 
Float aspect = Float(image.Resolution().x) / Float(image.Resolution().y); if (aspect > 1) screenBounds = Bounds2f(Point2f(-aspect, -1), Point2f(aspect, 1)); else screenBounds = Bounds2f(Point2f(-1, -1/aspect), Point2f(1, 1/aspect)); screenFromLight = Perspective(fov, hither, 1e30f /* yon */); lightFromScreen = Inverse(screenFromLight);

Since there is no particular need to keep ProjectionLights compact, both of the screen–light transformations are stored explicitly, which makes code in the following that uses them more succinct.

<<ProjectionLight Private Members>>+=  
Bounds2f screenBounds; Float hither = 1e-3f; Transform screenFromLight, lightFromScreen;

For a number of the following methods, we will need the light-space area of the image on the z equals 1 plane. One way to find this is to compute half of one of the two rectangle edge lengths using the projection’s field of view and to use the fact that the plane is a distance of 1 from the camera’s position. Doubling that gives one edge length and the other can be found using a factor based on the aspect ratio; see Figure 12.8.

Figure 12.8: The first step of computing the light-space area of the image on the z equals 1 projection plane is to compute the length opposite illustrated here. It is easily found using basic trigonometry.

<<Compute projection image area A>>= 
Float opposite = std::tan(Radians(fov) / 2); A = 4 * Sqr(opposite) * (aspect > 1 ? aspect : (1 / aspect));

<<ProjectionLight Private Members>>+= 
Float A;

The ProjectionLight::SampleLi() follows the same form as SpotLight::SampleLi() except that it uses the following I() method to compute the spectral intensity of the projected image. We will therefore skip over its implementation here. We will also not include the PDF_Li() method’s implementation, as it, too, returns 0.

The direction passed to the I() method should be normalized and already transformed into the light’s coordinate system.

<<ProjectionLight Method Definitions>>+=  
SampledSpectrum ProjectionLight::I(Vector3f w, const SampledWavelengths &lambda) const { <<Discard directions behind projection light>> 
if (w.z < hither) return SampledSpectrum(0.f);
<<Project point onto projection plane and compute RGB>> 
Point3f ps = screenFromLight(Point3f(w)); if (!Inside(Point2f(ps.x, ps.y), screenBounds)) return SampledSpectrum(0.f); Point2f uv = Point2f(screenBounds.Offset(Point2f(ps.x, ps.y))); RGB rgb; for (int c = 0; c < 3; ++c) rgb[c] = image.LookupNearestChannel(uv, c);
<<Return scaled wavelength samples corresponding to RGB>> 
RGBIlluminantSpectrum s(*imageColorSpace, ClampZero(rgb)); return scale * s.Sample(lambda);
}

Because the projective transformation has the property that it projects points behind the center of projection to points in front of it, it is important to discard points with a negative z value. Therefore, the projection code immediately returns no illumination for projection points that are behind the hither plane for the projection. If this check were not done, then it would not be possible to know if a projected point was originally behind the light (and therefore not illuminated) or in front of it.

<<Discard directions behind projection light>>= 
if (w.z < hither) return SampledSpectrum(0.f);

After being projected to the projection plane, points with coordinate values outside the screen window are discarded. Points that pass this test are transformed to get texture coordinates inside left-bracket 0 comma 1 right-bracket squared for the lookup in the image.

One thing to note is that a “nearest” lookup is used rather than, say, bilinear interpolation of the image samples. Although bilinear interpolation would lead to smoother results, especially for low-resolution image maps, in this way the projection function will exactly match the piecewise-constant distribution that is used for importance sampling in the light emission sampling methods. Further, the code here assumes that the image stores red, green, and blue in its first three channels; the code that creates ProjectionLights ensures that this is so.

<<Project point onto projection plane and compute RGB>>= 
Point3f ps = screenFromLight(Point3f(w)); if (!Inside(Point2f(ps.x, ps.y), screenBounds)) return SampledSpectrum(0.f); Point2f uv = Point2f(screenBounds.Offset(Point2f(ps.x, ps.y))); RGB rgb; for (int c = 0; c < 3; ++c) rgb[c] = image.LookupNearestChannel(uv, c);

It is important to use an RGBIlluminantSpectrum to convert the RGB value to spectral samples rather than, say, an RGBUnboundedSpectrum. This ensures that, for example, a left-parenthesis 1 comma 1 comma 1 right-parenthesis RGB value corresponds to the color space’s illuminant and not a constant spectral distribution.

<<Return scaled wavelength samples corresponding to RGB>>= 
RGBIlluminantSpectrum s(*imageColorSpace, ClampZero(rgb)); return scale * s.Sample(lambda);

The total emitted power is given by integrating radiant intensity over the sphere of directions (Equation (4.2)), though here the projection function is tabularized over a planar 2D area. Power can thus be computed by integrating over the area of the image and applying a change of variables factor normal d omega Subscript slash normal d upper A :

normal upper Phi equals integral Underscript script upper S squared Endscripts upper I left-parenthesis omega Subscript Baseline right-parenthesis normal d omega Subscript Baseline equals integral Underscript upper A Endscripts upper I left-parenthesis normal p Subscript Baseline right-parenthesis StartFraction normal d omega Subscript Baseline Over normal d upper A EndFraction normal d upper A period

<<ProjectionLight Method Definitions>>+= 
SampledSpectrum ProjectionLight::Phi(SampledWavelengths lambda) const { SampledSpectrum sum(0.f); for (int y = 0; y < image.Resolution().y; ++y) for (int x = 0; x < image.Resolution().x; ++x) { <<Compute change of variables factor dwdA for projection light pixel>> 
Point2f ps = screenBounds.Lerp(Point2f((x + 0.5f) / image.Resolution().x, (y + 0.5f) / image.Resolution().y)); Vector3f w = Vector3f(lightFromScreen(Point3f(ps.x, ps.y, 0))); w = Normalize(w); Float dwdA = Pow<3>(CosTheta(w));
<<Update sum for projection light pixel>> 
RGB rgb; for (int c = 0; c < 3; ++c) rgb[c] = image.GetChannel({x, y}, c); RGBIlluminantSpectrum s(*imageColorSpace, ClampZero(rgb)); sum += s.Sample(lambda) * dwdA;
} <<Return final power for projection light>> 
return scale * A * sum / (image.Resolution().x * image.Resolution().y);
}

Recall from Section 4.2.3 that differential area normal d upper A Subscript is converted to differential solid angle normal d omega Subscript by multiplying by a cosine theta factor and dividing by the squared distance. Because the plane we are integrating over is at z equals 1 , the distance from the origin to a point on the plane is equal to 1 slash cosine theta and thus the aggregate factor is cosine cubed theta ; see Figure 12.9.

Figure 12.9: To find the power of a point light source, we generally integrate radiant intensity over directions around the light. For the ProjectionLight, we instead integrate over the z equals 1 plane, in which case we need to account for the change of variables, applying both a cosine theta and a 1 slash r squared factor.

<<Compute change of variables factor dwdA for projection light pixel>>= 
Point2f ps = screenBounds.Lerp(Point2f((x + 0.5f) / image.Resolution().x, (y + 0.5f) / image.Resolution().y)); Vector3f w = Vector3f(lightFromScreen(Point3f(ps.x, ps.y, 0))); w = Normalize(w); Float dwdA = Pow<3>(CosTheta(w));

For the same reasons as in the Project() method, an RGBIlluminantSpectrum is used to convert each RGB value to spectral samples.

<<Update sum for projection light pixel>>= 
RGB rgb; for (int c = 0; c < 3; ++c) rgb[c] = image.GetChannel({x, y}, c); RGBIlluminantSpectrum s(*imageColorSpace, ClampZero(rgb)); sum += s.Sample(lambda) * dwdA;

The final integrated value includes a factor of the area that was integrated over, A, and is divided by the total number of pixels.

<<Return final power for projection light>>= 
return scale * A * sum / (image.Resolution().x * image.Resolution().y);

12.2.3 Goniophotometric Diagram Lights

A goniophotometric diagram describes the angular distribution of luminance from a point light source; it is widely used in illumination engineering to characterize lights. Figure 12.10 shows an example of a goniophotometric diagram in two dimensions. In this section, we will implement a light source that uses goniophotometric diagrams encoded in 2D image maps to describe the emission distribution lights.

Figure 12.10: An Example of a Goniophotometric Diagram Specifying an Outgoing Light Distribution from a Point Light Source in 2D. The emitted intensity is defined in a fixed set of directions on the unit sphere, and the intensity for a given outgoing direction omega Subscript is found by interpolating the intensities of the adjacent samples.

Figure 12.11: Goniophotometric Diagrams for Real-World Light Sources. These images are encoded using an equal-area parameterization (Section 3.8.3). (a) A light that mostly illuminates in its up direction, with only a small amount of illumination in the down direction. (b) A light that mostly illuminates in the down direction. (c) A light that casts illumination both above and below.

Figure 12.11 shows a few goniophotometric diagrams encoded as image maps and Figure 12.12 shows a scene rendered with a light source that uses one of these images to modulate its directional distribution of illumination. The GoniometricLight uses the equal-area parameterization of the sphere that was introduced in Section 3.8.3, so the center of the image corresponds to the “up” direction.

Figure 12.12: Scene Rendered Using the Goniophotometric Diagram from Figure 12.11(b). Even though a point light source is the basis of this light, including the directional variation of a realistic light improves the visual realism of the rendered image. (Dragon model courtesy of the Stanford Computer Graphics Laboratory.)

<<GoniometricLight Definition>>= 
class GoniometricLight : public LightBase { public: <<GoniometricLight Public Methods>> 
GoniometricLight(const Transform &renderFromLight, const MediumInterface &mediumInterface, Spectrum I, Float scale, Image image, Allocator alloc); static GoniometricLight *Create(const Transform &renderFromLight, Medium medium, const ParameterDictionary &parameters, const RGBColorSpace *colorSpace, const FileLoc *loc, Allocator alloc); void Preprocess(const Bounds3f &sceneBounds) {} PBRT_CPU_GPU pstd::optional<LightLiSample> SampleLi(LightSampleContext ctx, Point2f u, SampledWavelengths lambda, bool allowIncompletePDF) const; 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"); } pstd::optional<LightBounds> Bounds() const; std::string ToString() const; SampledSpectrum I(Vector3f w, const SampledWavelengths &lambda) const { Point2f uv = EqualAreaSphereToSquare(w); return scale * Iemit->Sample(lambda) * image.LookupNearestChannel(uv, 0); }
private: <<GoniometricLight Private Members>> 
const DenselySampledSpectrum *Iemit; Float scale; Image image;
};

The GoniometricLight constructor takes a base intensity, an image map that scales the intensity based on the angular distribution of light, and the usual transformation and medium interface; these are stored in the following member variables. In the following methods, only the first channel of the image map will be used to scale the light’s intensity: the GoniometricLight does not support specifying color via the image. It is the responsibility of calling code to convert RGB images to luminance or some other appropriate scalar value before passing the image to the constructor here.

<<GoniometricLight Private Members>>= 
const DenselySampledSpectrum *Iemit; Float scale; Image image;

The SampleLi() method follows the same form as that of SpotLight and ProjectionLight, so it is not included here. It uses the following method to compute the radiant intensity for a given direction.

<<GoniometricLight Public Methods>>= 
SampledSpectrum I(Vector3f w, const SampledWavelengths &lambda) const { Point2f uv = EqualAreaSphereToSquare(w); return scale * Iemit->Sample(lambda) * image.LookupNearestChannel(uv, 0); }

Because it uses an equal-area mapping from the image to the sphere, each pixel in the image subtends an equal solid angle and the change of variables factor for integrating over the sphere of directions is the same for all pixels. Its value is 4 pi , the ratio of the area of the unit sphere to the unit square.

<<GoniometricLight Method Definitions>>= 
SampledSpectrum GoniometricLight::Phi(SampledWavelengths lambda) const { Float sumY = 0; for (int y = 0; y < image.Resolution().y; ++y) for (int x = 0; x < image.Resolution().x; ++x) sumY += image.GetChannel({x, y}, 0); return scale * Iemit->Sample(lambda) * 4 * Pi * sumY / (image.Resolution().x * image.Resolution().y); }