12.3 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. It is defined in lights/point.h and lights/point.cpp. Figure 12.6 shows a scene rendered with a point light source. Building on this base, a number of more complex lights based on point sources will be introduced, including spotlights and a light that projects an image into the scene.
PointLights are positioned at the origin in light space. To place them elsewhere, the light-to-world transformation should be set appropriately. Using this transformation, the world space position of the light is precomputed and cached in the constructor by transforming from light space to world space.
The constructor also stores the intensity for the light source, which is the amount of power per unit solid angle. Because the light source is isotropic, this is a constant. Finally, since point lights represent singularities that only emit light from a single position, the Light::flags field is initialized to LightFlags::DeltaPosition.
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 5.4. In the light source interfaces here, however, we will abuse terminology and use Sample_Li() 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 p to convert units. Section 14.2 revisits the details of this issue in its discussion of how delta distributions affect evaluation of the integral in the scattering equation. 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.
The total power emitted by the light source can be found by integrating the intensity over the entire sphere of directions:
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 and pointing down the axis. To place or orient it elsewhere in the scene, the Light::WorldToLight transformation should be set accordingly. Figure 12.7 shows a rendering of the same scene as Figure 12.6, only illuminated with a spotlight instead of a point light. The SpotLight class is defined in lights/spot.h and lights/spot.cpp.
Two angles are passed to the constructor to set the extent of the SpotLight’s cone: the overall angular width of the cone and the angle at which falloff starts (Figure 12.8). The constructor precomputes and stores the cosines of these angles for use in the SpotLight’s methods.
The SpotLight::Sample_Li() method is almost identical to PointLight::Sample_Li(), except that it also calls the Falloff() method, which 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.
To compute the spotlight’s strength for a receiving point , this first step is to compute the cosine of the angle between the vector from the spotlight origin to and the vector along the center of the spotlight’s cone. To compute the cosine of the offset angle to a point p, we have, as illustrated in Figure 12.8,
This value is then compared to the cosines of the falloff and overall width angles to see where the point lies with respect to the spotlight cone. We can trivially determine that points with a cosine greater than the cosine of the falloff angle are inside the cone receiving full illumination, and points with cosine less than the width angle’s cosine are completely outside the cone. (Note that the computation is slightly tricky since for , if , then .)
For points inside the transition range from fully illuminated to outside of the cone, the intensity is scaled to smoothly fall off from full illumination to darkness:
The solid angle subtended by a cone with spread angle is . Therefore, the integral over directions on the sphere that gives power from radiant intensity can be solved to compute the total power of a light that only emits illumination in a cone. For the spotlight, we can reasonably approximate the power of the light by computing the solid angle of directions that is covered by the cone with a spread angle cosine halfway between falloffWidth and falloffStart.
12.3.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.9). Its implementation is in lights/projection.h and lights/projection.cpp.
The use of this light in the lighting example scene is shown in Figure 12.10.
This light could use a Texture to represent the light projection distribution so that procedural projection patterns could be used. However, having a precise representation of the projection function, as is available by using an image in a MIPMap, is useful for being able to sample the projection distribution using Monte Carlo techniques, so we will use that representation in the implementation here.
Note that the projected image is explicitly stored as an RGBSpectrum, even if full spectral rendering is being performed. Unless the image map being used has full spectral data, storing full SampledSpectrum values in this case only wastes memory; whether an RGB color is converted to a SampledSpectrum before the MIPMap is created or after the MIPMap returns a value from its Lookup() routine gives the same result in either case.
Finally, the constructor finds the cosine of the angle between the axis and the vector to a corner of the screen window. This value is used elsewhere to define the minimal cone of directions that encompasses the set of directions in which light is projected. This cone is useful for algorithms like photon mapping that need to randomly sample rays leaving the light source (explained in Chapter 16). We won’t derive this computation here; it is based on straightforward trigonometry.
Similar to the spotlight’s version, ProjectionLight::Sample_Li() calls a utility method, ProjectionLight::Projection(), to determine how much light is projected in the given direction. Therefore, we won’t include the implementation of Sample_Li() here.
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 value. Therefore, the projection code immediately returns no illumination for projection points that are behind the near plane for the projection. If this check were not done, then it wouldn’t be possible to know if a projected point was originally behind the light (and therefore not illuminated) or in front of it.
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 for the lookup in the image map. Note it is explicitly specified that the RGBSpectrum value passed to the Spectrum constructor represents an illuminant’s SPD and not that of a reflectance. (Recall from Section 5.2.2 that different matching functions are used for converting from RGB to SPDs for illuminants versus reflectances.)
The total power of this light is approximated as a spotlight that subtends the same angle as the diagonal of the projected image, scaled by the average intensity in the image map. This approximation becomes increasingly inaccurate as the projected image’s aspect ratio becomes less square, for example, and it doesn’t account for the fact that texels toward the edges of the image map subtend a larger solid angle than texels in the middle when projected with a perspective projection. Nevertheless, it’s a reasonable first-order approximation.
12.3.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.11 shows an example of a goniophotometric diagram in two dimensions. In this section, we’ll implement a light source that uses goniophotometric diagrams encoded in 2D image maps to describe the emission distribution of the light. The implementation is very similar to the point light sources defined previously in this section; it scales the intensity based on the outgoing direction according to the goniophotometric diagram’s values.
Figure 12.12 shows a few goniophotometric diagrams encoded as image maps.
Figure 12.13 shows a scene rendered with a light source that uses one of these images to modulate its directional distribution of illumination.
The GonioPhotometricLight constructor takes a base intensity and an image map that scales the intensity based on the angular distribution of light.
The GonioPhotometricLight::Sample_Li() method is not shown here. It is essentially identical to the SpotLight::Sample_Li() and ProjectionLight::Sample_Li() methods that use a helper function to scale the amount of radiance. It assumes that the scale texture is encoded using spherical coordinates, so that the given direction needs to be converted to and values and scaled to before being used to index into the texture. Goniophotometric diagrams are usually defined in a coordinate space where the axis is up, whereas the spherical coordinate utility routines in pbrt assume that is up, so and are swapped before doing the conversion.
The Power() method uses the average intensity over the image to compute power. This computation is inaccurate because the spherical coordinate parameterization of directions has various distortions, particularly near the and directions. Again, this error is acceptable for the uses of this method in pbrt.