13.3 A Simple Path Tracer
The path tracing estimator in Equation (13.7) makes it possible to apply the BSDF and light sampling techniques that were respectively defined in Chapters 9 and 12 to rendering. As shown in Figure 13.6, more effective importance sampling approaches than the uniform sampling in the RandomWalkIntegrator significantly reduce error. Although the SimplePathIntegrator takes longer to render an image at equal sample counts, most of that increase is because paths often terminate early with the RandomWalkIntegrator; because it samples outgoing directions at intersections uniformly over the sphere, half of the sampled directions lead to path termination at non-transmissive surfaces. The overall improvement in Monte Carlo efficiency from the SimplePathIntegrator is .
The “simple” in the name of this integrator is meaningful: PathIntegrator, which will be introduced shortly, adds a number of additional sampling improvements and should be used in preference to SimplePathIntegrator if rendering efficiency is important. This integrator is still useful beyond pedagogy, however; it is also useful for debugging and for validating the implementation of sampling algorithms. For example, it can be configured to use BSDFs’ sampling methods or to use uniform directional sampling; given a sufficient number of samples, both approaches should converge to the same result (assuming that the BSDF is not perfect specular). If they do not, the error is presumably in the BSDF sampling code. Light sampling techniques can be tested in a similar fashion.
The constructor sets the following member variables from provided parameters, so it is not included here. Similar to the RandomWalkIntegrator, maxDepth caps the maximum path length.
The sampleLights member variable determines whether lights’ SampleLi() methods should be used to sample direct illumination or whether illumination should only be found by rays randomly intersecting emissive surfaces, as was done in the RandomWalkIntegrator. In a similar fashion, sampleBSDF determines whether BSDFs’ Sample_f() methods should be used to sample directions or whether uniform directional sampling should be used. Both are true by default. A UniformLightSampler is always used for sampling a light; this, too, is an instance where this integrator opts for simplicity and a lower likelihood of bugs in exchange for lower efficiency.
As a RayIntegrator, this integrator provides a Li() method that returns an estimate of the radiance along the provided ray. It does not provide the capability of initializing a VisibleSurface at the first intersection point, so the corresponding parameter is ignored.
A number of variables record the current state of the path. L is the current estimated scattered radiance from the running total of and ray is updated after each surface intersection to be the next ray to be traced. specularBounce records if the last outgoing path direction sampled was due to specular reflection; the need to track this will be explained shortly.
The beta variable holds the path throughput weight, which is defined as the factors of the throughput function —that is, the product of the BSDF values and cosine terms for the vertices generated so far, divided by their respective sampling PDFs:
Thus, the product of beta with scattered light from direct lighting from the final vertex of the path gives the contribution for a path. (This quantity will reoccur many times in the following few chapters, and we will consistently refer to it as beta.) Because the effect of earlier path vertices is aggregated in this way, there is no need to store the positions and BSDFs of all the vertices of the path—only the last one.
Each iteration of the while loop accounts for an additional segment of a path, corresponding to a term of ’s sum.
The first step is to find the intersection of the ray for the current segment with the scene geometry.
If there is no intersection, then the ray path comes to an end. Before the accumulated path radiance estimate can be returned, however, in some cases radiance from infinite light sources is added to the path’s radiance estimate, with contribution scaled by the accumulated beta factor.
If sampleLights is false, then emission is only found when rays happen to intersect emitters, in which case the contribution of infinite area lights must be added to rays that do not intersect any geometry. If it is true, then the integrator calls the Light SampleLi() method to estimate direct illumination at each path vertex. In that case, infinite lights have already been accounted for, except in the case of a specular BSDF at the previous vertex. Then, SampleLi() is not useful since only the specular direction scatters light. Therefore, specularBounce records whether the last BSDF was perfect specular, in which case infinite area lights must be included here after all.
If the ray hits an emissive surface, similar logic governs whether its emission is added to the path’s radiance estimate.
The next step is to find the BSDF at the intersection point. A special case arises when an unset BSDF is returned by the SurfaceInteraction’s GetBSDF() method. In that case, the current surface should have no effect on light. pbrt uses such surfaces to represent transitions between participating media, whose boundaries are themselves optically inactive (i.e., they have the same index of refraction on both sides). Since the SimplePathIntegrator ignores media, it simply skips over such surfaces without counting them as scattering events in the depth counter.
Otherwise we have a valid surface intersection and can go ahead and increment depth. The path is then terminated if it has reached the maximum depth.
If explicit light sampling is being performed, then the first step is to use the UniformLightSampler to choose a single light source. (Recall from Section 12.6 that sampling only one of the scene’s light sources can still give a valid estimate of the effect of all of them, given suitable weighting.)
Given a light source, a call to SampleLi() yields a sample on the light. If the light sample is valid, a direct lighting calculation is performed.
Returning to the path tracing estimator in Equation (13.7), we have the path throughput weight in beta, which corresponds to the term in parentheses there. A call to SampleLi() yields a sample on the light. Because the light sampling methods return samples that are with respect to solid angle and not area, yet another Jacobian correction term is necessary, and the estimator becomes
where is the solid angle density that the chosen light would use to sample the direction and is the discrete probability of sampling the light (recall Equation (12.2)). Their product gives the full probability of the light sample.
Before tracing the shadow ray to evaluate the visibility factor , it is worth checking if the BSDF is zero for the sampled direction, in which case that computational expense is unnecessary.
Unoccluded() is a convenience method provided in the Integrator base class.
To sample the next path vertex, the direction of the ray leaving the surface is found either by calling the BSDF’s sampling method or by sampling uniformly, depending on the sampleBSDF parameter.
If BSDF sampling is being used to sample the new direction, the Sample_f() method gives a direction and the associated BSDF and PDF values. beta can then be updated according to Equation (13.8).
Otherwise, the fragment <<Uniformly sample sphere or hemisphere to get new path direction>> uniformly samples a new direction for the ray leaving the surface. It goes through more care than the RandomWalkIntegrator did: for example, if the surface is reflective but not transmissive, it makes sure that the sampled direction is in the hemisphere where light is scattered. We will not include that fragment here, as it has to handle a number of such cases, but there is not much that is interesting about how it does so.