9.3 Bump Mapping

All of the Materials defined in the previous section take an optional floating-point texture that defines a displacement at each point on the surface: each point normal p Subscript has a displaced point normal p prime associated with it, defined by normal p prime equals normal p Subscript Baseline plus d left-parenthesis normal p Subscript Baseline right-parenthesis bold n left-parenthesis normal p Subscript Baseline right-parenthesis , where d left-parenthesis normal p Subscript Baseline right-parenthesis is the offset returned by the displacement texture at normal p Subscript and bold n left-parenthesis normal p Subscript Baseline right-parenthesis is the surface normal at normal p Subscript (Figure 9.6). We would like to use this texture to compute shading normals so that the surface appears as if it actually had been offset by the displacement function, without modifying its geometry. This process is called bump mapping. For relatively small displacement functions, the visual effect of bump mapping can be quite convincing. This idea and the specific technique to compute these shading normals in a way that gives a plausible appearance of the actual displaced surface were developed by Blinn (1978).

Figure 9.6: A displacement function associated with a material defines a new surface based on the old one, offset by the displacement amount along the normal at each point. pbrt doesn’t compute a geometric representation of this displaced surface, but instead uses it to compute shading normals for bump mapping.

Figure 9.7 shows the effect of applying bump mapping defined by an image map of a grid of lines to a sphere.

Figure 9.7: Using bump mapping to compute the shading normals for a sphere gives it the appearance of having much more geometric detail than is actually present.

A more complex example is shown in Figure 9.8, which shows a scene rendered with and without bump mapping. There, the bump map gives the appearance of a substantial amount of detail in the walls and floors that isn’t actually present in the geometric model.

Figure 9.8: The Sponza atrium model, rendered (1) without bump mapping and (2) with bump mapping. Bump mapping substantially increases the apparent geometric complexity of the model, without the increased rendering time and memory use that would result from a geometric representation with the equivalent amount of small-scale detail.

Figure 9.9 shows one of the image maps used to define the bump function in Figure 9.8.

Figure 9.9: One of the image maps used as a bump map for the Sponza atrium rendering in Figure 9.8.

The Material::Bump() method is a utility routine for use by Material implementations. It is responsible for computing the effect of bump mapping at the point being shaded given a particular displacement Texture. So that future Material implementations aren’t required to support bump mapping with this particular mechanism (or at all), we’ve placed this method outside of the hard-coded material evaluation pipeline and left it as a function that particular material implementations can call on their own.

The implementation of Material::Bump() is based on finding an approximation to the partial derivatives partial-differential normal p slash partial-differential u and partial-differential normal p slash partial-differential v of the displaced surface and using them in place of the surface’s actual partial derivatives to compute the shading normal. (Recall that the surface normal is given by the cross product of these vectors, bold n Subscript Baseline equals partial-differential normal p slash partial-differential u times partial-differential normal p slash partial-differential v .) Assume that the original surface is defined by a parametric function p left-parenthesis u comma v right-parenthesis , and the bump offset function is a scalar function d left-parenthesis u comma v right-parenthesis . Then the displaced surface is given by

p prime left-parenthesis u comma v right-parenthesis equals p left-parenthesis u comma v right-parenthesis plus d left-parenthesis u comma v right-parenthesis bold n Subscript Baseline left-parenthesis u comma v right-parenthesis comma

where bold n Subscript Baseline left-parenthesis u comma v right-parenthesis is the surface normal at left-parenthesis u comma v right-parenthesis .

The partial derivatives of this function can be found using the chain rule. For example, the partial derivative in u is

StartFraction partial-differential p Superscript prime Baseline Over partial-differential u EndFraction equals StartFraction partial-differential p left-parenthesis u comma v right-parenthesis Over partial-differential u EndFraction plus StartFraction partial-differential d left-parenthesis u comma v right-parenthesis Over partial-differential u EndFraction bold n Subscript Baseline left-parenthesis u comma v right-parenthesis plus d left-parenthesis u comma v right-parenthesis StartFraction partial-differential bold n Subscript Baseline left-parenthesis u comma v right-parenthesis Over partial-differential u EndFraction period
(9.1)

We already have computed the value of partial-differential p left-parenthesis u comma v right-parenthesis slash partial-differential u ; it’s partial-differential normal p slash partial-differential u and is available in the SurfaceInteraction structure, which also stores the surface normal bold n Subscript Baseline left-parenthesis u comma v right-parenthesis and the partial derivative partial-differential bold n Subscript Baseline left-parenthesis u comma v right-parenthesis slash partial-differential u equals partial-differential bold n Subscript Baseline slash partial-differential u . The displacement function d left-parenthesis u comma v right-parenthesis can be evaluated as needed, which leaves partial-differential d left-parenthesis u comma v right-parenthesis slash partial-differential u as the only remaining term.

There are two possible approaches to finding the values of partial-differential d left-parenthesis u comma v right-parenthesis slash partial-differential u and partial-differential d left-parenthesis u comma v right-parenthesis slash partial-differential v . One option would be to augment the Texture interface with a method to compute partial derivatives of the underlying texture function. For example, for image map textures mapped to the surface directly using its left-parenthesis u comma v right-parenthesis parameterization, these partial derivatives can be computed by subtracting adjacent texels in the u and v directions. However, this approach is difficult to extend to complex procedural textures like some of the ones defined in Chapter 10. Therefore, pbrt directly computes these values with forward differencing in the Material::Bump() method, without modifying the Texture interface.

Recall the definition of the partial derivative:

StartFraction partial-differential d left-parenthesis u comma v right-parenthesis Over partial-differential u EndFraction equals limit Underscript normal upper Delta Subscript u Baseline right-arrow 0 Endscripts StartFraction d left-parenthesis u plus normal upper Delta Subscript u Baseline comma v right-parenthesis minus d left-parenthesis u comma v right-parenthesis Over normal upper Delta Subscript u Baseline EndFraction period

Forward differencing approximates the value using a finite value of normal upper Delta Subscript u and evaluating d left-parenthesis u comma v right-parenthesis at two positions. Thus, the final expression for partial-differential p prime slash partial-differential u is the following (for simplicity, we have dropped the explicit dependence on left-parenthesis u comma v right-parenthesis for some of the terms):

StartFraction partial-differential p Superscript prime Baseline Over partial-differential u EndFraction almost-equals StartFraction partial-differential normal p Over partial-differential u EndFraction plus StartFraction d left-parenthesis u plus normal upper Delta Subscript u Baseline comma v right-parenthesis minus d left-parenthesis u comma v right-parenthesis Over normal upper Delta Subscript u Baseline EndFraction bold n Subscript Baseline plus d left-parenthesis u comma v right-parenthesis StartFraction partial-differential bold n Over partial-differential u EndFraction period

Interestingly enough, most bump-mapping implementations ignore the final term under the assumption that d left-parenthesis u comma v right-parenthesis is expected to be relatively small. (Since bump mapping is mostly useful for approximating small perturbations, this is a reasonable assumption.) The fact that many renderers do not compute the values partial-differential bold n Subscript slash partial-differential u and partial-differential bold n Subscript slash partial-differential v may also have something to do with this simplification. An implication of ignoring the last term is that the magnitude of the displacement function then does not affect the bump-mapped partial derivatives; adding a constant value to it globally doesn’t affect the final result, since only differences of the bump function affect it. pbrt computes all three terms since it has partial-differential bold n Subscript slash partial-differential u and partial-differential bold n Subscript slash partial-differential v readily available, although in practice this final term rarely makes a visually noticeable difference.

One important detail in the definition of Bump() is that the d parameter is declared to be of type const shared_ptr<Texture<Float>> &, rather than, for example, shared_ptr<Texture<Float>>. This difference is very important for performance, but the reason is subtle. If a C++ reference was not used here, then the shared_ptr implementation would need to increment the reference count for the temporary value passed to the method, and the reference count would need to be decremented when the method returned. This is an efficient operation with serial code, but with multiple threads of execution, it leads to a situation where multiple processing cores end up modifying the same memory location whenever different rendering tasks run this method with the same displacement texture. This state of affairs in turn leads to the expensive “read for ownership” operation described in Section A.6.1.

<<Material Method Definitions>>= 
void Material::Bump(const std::shared_ptr<Texture<Float>> &d, SurfaceInteraction *si) { <<Compute offset positions and evaluate displacement texture>> 
SurfaceInteraction siEval = *si; <<Shift siEval du in the u direction>> 
Float du = .5f * (std::abs(si->dudx) + std::abs(si->dudy)); if (du == 0) du = .01f; siEval.p = si->p + du * si->shading.dpdu; siEval.uv = si->uv + Vector2f(du, 0.f); siEval.n = Normalize((Normal3f)Cross(si->shading.dpdu, si->shading.dpdv) + du * si->dndu);
Float uDisplace = d->Evaluate(siEval); <<Shift siEval dv in the v direction>> 
Float dv = .5f * (std::abs(si->dvdx) + std::abs(si->dvdy)); if (dv == 0) dv = .01f; siEval.p = si->p + dv * si->shading.dpdv; siEval.uv = si->uv + Vector2f(0.f, dv); siEval.n = Normalize((Normal3f)Cross(si->shading.dpdu, si->shading.dpdv) + dv * si->dndv);
Float vDisplace = d->Evaluate(siEval); Float displace = d->Evaluate(*si);
<<Compute bump-mapped differential geometry>> 
Vector3f dpdu = si->shading.dpdu + (uDisplace - displace) / du * Vector3f(si->shading.n) + displace * Vector3f(si->shading.dndu); Vector3f dpdv = si->shading.dpdv + (vDisplace - displace) / dv * Vector3f(si->shading.n) + displace * Vector3f(si->shading.dndv); si->SetShadingGeometry(dpdu, dpdv, si->shading.dndu, si->shading.dndv, false);
}

<<Compute offset positions and evaluate displacement texture>>= 
SurfaceInteraction siEval = *si; <<Shift siEval du in the u direction>> 
Float du = .5f * (std::abs(si->dudx) + std::abs(si->dudy)); if (du == 0) du = .01f; siEval.p = si->p + du * si->shading.dpdu; siEval.uv = si->uv + Vector2f(du, 0.f); siEval.n = Normalize((Normal3f)Cross(si->shading.dpdu, si->shading.dpdv) + du * si->dndu);
Float uDisplace = d->Evaluate(siEval); <<Shift siEval dv in the v direction>> 
Float dv = .5f * (std::abs(si->dvdx) + std::abs(si->dvdy)); if (dv == 0) dv = .01f; siEval.p = si->p + dv * si->shading.dpdv; siEval.uv = si->uv + Vector2f(0.f, dv); siEval.n = Normalize((Normal3f)Cross(si->shading.dpdu, si->shading.dpdv) + dv * si->dndv);
Float vDisplace = d->Evaluate(siEval); Float displace = d->Evaluate(*si);

One remaining issue is how to choose the offsets normal upper Delta Subscript u and normal upper Delta Subscript v for the finite differencing computations. They should be small enough that fine changes in d left-parenthesis u comma v right-parenthesis are captured but large enough so that available floating-point precision is sufficient to give a good result. Here, we will choose normal upper Delta Subscript u and normal upper Delta Subscript v values that lead to an offset that is about half the image space pixel sample spacing and use them to update the appropriate member variables in the SurfaceInteraction to reflect a shift to the offset position. (See Section 10.1.1 for an explanation of how the image space distances are computed.)

Another detail to note in the following code: we recompute the surface normal bold n Subscript as the cross product of partial-differential normal p slash partial-differential u and partial-differential normal p slash partial-differential v rather than using si->shading.n directly. The reason for this is that the orientation of bold n Subscript may have been flipped (recall the fragment <<Adjust normal based on orientation and handedness>> in Section 2.10.1). However, we need the original normal here. Later, when the results of the computation are passed to SurfaceInteraction::SetShadingGeometry(), the normal we compute will itself be flipped if necessary.

<<Shift siEval du in the u direction>>= 
Float du = .5f * (std::abs(si->dudx) + std::abs(si->dudy)); if (du == 0) du = .01f; siEval.p = si->p + du * si->shading.dpdu; siEval.uv = si->uv + Vector2f(du, 0.f); siEval.n = Normalize((Normal3f)Cross(si->shading.dpdu, si->shading.dpdv) + du * si->dndu);

The <<Shift siEval dv in the v direction>> fragment is nearly the same as the fragment that shifts du, so it isn’t included here.

Given the new positions and the displacement texture’s values at them, the partial derivatives can be computed directly using Equation (9.1):

<<Compute bump-mapped differential geometry>>= 
Vector3f dpdu = si->shading.dpdu + (uDisplace - displace) / du * Vector3f(si->shading.n) + displace * Vector3f(si->shading.dndu); Vector3f dpdv = si->shading.dpdv + (vDisplace - displace) / dv * Vector3f(si->shading.n) + displace * Vector3f(si->shading.dndv); si->SetShadingGeometry(dpdu, dpdv, si->shading.dndu, si->shading.dndv, false);