## 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 .

`RandomWalkIntegrator`and the

`SimplePathIntegrator`. (a) Scene rendered with 64 pixel samples using the

`RandomWalkIntegrator`. (b) Rendered with 64 pixel samples and the

`SimplePathIntegrator`. The

`SimplePathIntegrator`gives an image that is visibly much improved, thanks to using more effective BSDF and light sampling techniques. Here, mean squared error (MSE) is reduced by a factor of 101. Even though rendering time was longer, the overall improvement in Monte Carlo efficiency was still .

*(Scene courtesy of Angelo Ferretti.)*

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.

`SimplePathIntegrator`vertex and accumulate contribution>>

`ray`with scene>>

`sampleLights`is true>>

`sampledLight`to estimate direct illumination>>

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.

`SimplePathIntegrator`vertex and accumulate contribution>>

`ray`with scene>>

`sampleLights`is true>>

`sampledLight`to estimate direct illumination>>

Each iteration of the `while` loop accounts for an additional segment
of a path, corresponding to a term of ’s sum.

`SimplePathIntegrator`vertex and accumulate contribution>>=

`ray`with scene>>

`sampleLights`is true>>

`sampledLight`to estimate direct illumination>>

The first step is to find the intersection of the ray for the current segment with the scene geometry.

`ray`with scene>>=

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.)

`sampleLights`is true>>=

`sampledLight`to estimate direct illumination>>

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.

`sampledLight`to estimate direct illumination>>=

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.