10.3 Texture Interface and Basic Textures

Texture is a template class parameterized by the return type of its evaluation function. This design makes it possible to reuse almost all of the code among textures that return different types. pbrt currently uses only Float and Spectrum textures.

<<Texture Declarations>>+=  
template <typename T> class Texture { public: <<Texture Interface>> 
virtual T Evaluate(const SurfaceInteraction &) const = 0; virtual ~Texture() { }
};

The key to Texture’s interface is its evaluation function; it returns a value of the template type T. The only information it has access to in order to evaluate its value is the SurfaceInteraction at the point being shaded. Different textures in this chapter will use different parts of this structure to drive their evaluation.

<<Texture Interface>>= 
virtual T Evaluate(const SurfaceInteraction &) const = 0;

10.3.1 Constant Texture

ConstantTexture returns the same value no matter where it is evaluated. Because it represents a constant function, it can be accurately reconstructed with any sampling rate and therefore needs no antialiasing. Although this texture is trivial, it is actually quite useful. By providing this class, all parameters to all Materials can be represented as Textures, whether they are spatially varying or not. For example, a red diffuse object will have a ConstantTexture that always returns red as the diffuse color of the material. This way, the shading system always evaluates a texture to get the surface properties at a point, avoiding the need for separate textured and nontextured versions of materials. This texture’s implementation is in the files textures/constant.h and textures/constant.cpp.

<<ConstantTexture Declarations>>= 
template <typename T> class ConstantTexture : public Texture<T> { public: <<ConstantTexture Public Methods>> 
ConstantTexture(const T &value) : value(value) { } T Evaluate(const SurfaceInteraction &) const { return value; }
private: T value; };

<<ConstantTexture Public Methods>>= 
ConstantTexture(const T &value) : value(value) { } T Evaluate(const SurfaceInteraction &) const { return value; }

10.3.2 Scale Texture

We have defined the texture interface in a way that makes it easy to use the output of one texture function when computing another. This is useful since it lets us define generic texture operations using any of the other texture types. The ScaleTexture takes two textures and returns the product of their values when evaluated. It is defined in textures/scale.h and textures/scale.cpp.

<<ScaleTexture Declarations>>= 
template <typename T1, typename T2> class ScaleTexture : public Texture<T2> { public: <<ScaleTexture Public Methods>> 
ScaleTexture(const std::shared_ptr<Texture<T1>> &tex1, const std::shared_ptr<Texture<T2>> &tex2) : tex1(tex1), tex2(tex2) { } T2 Evaluate(const SurfaceInteraction &si) const { return tex1->Evaluate(si) * tex2->Evaluate(si); }
private: <<ScaleTexture Private Data>> 
std::shared_ptr<Texture<T1>> tex1; std::shared_ptr<Texture<T2>> tex2;
};

The attentive reader may notice that the shared_ptr parameters to the constructor are stored in member variables and wonder if there is a performance issue from this approach along the lines of the one described in Section 9.3 with regard to the Bump() method. In this case we’re fine: Textures are only created at scene creation time, rather than at rendering time for each ray. Therefore, there are no issues with contention at the memory location that stores the reference count for each shared_ptr.

<<ScaleTexture Public Methods>>= 
ScaleTexture(const std::shared_ptr<Texture<T1>> &tex1, const std::shared_ptr<Texture<T2>> &tex2) : tex1(tex1), tex2(tex2) { }

Note that the return types of the two textures can be different; the implementation here just requires that it be possible to multiply their values together. Thus, a Float texture can be used to scale a Spectrum texture.

<<ScaleTexture Private Data>>= 
std::shared_ptr<Texture<T1>> tex1; std::shared_ptr<Texture<T2>> tex2;

ScaleTexture ignores antialiasing, leaving it to its two subtextures to antialias themselves but not making an effort to antialias their product. While it is easy to show that the product of two band-limited functions is also band limited, the maximum frequency present in the product may be greater than that of either of the two terms individually. Thus, even if the scale and value textures are perfectly antialiased, the result might not be. Fortunately, the most common use of this texture is to scale another texture by a constant, in which case the other texture’s antialiasing is sufficient.

<<ScaleTexture Public Methods>>+= 
T2 Evaluate(const SurfaceInteraction &si) const { return tex1->Evaluate(si) * tex2->Evaluate(si); }

10.3.3 Mix Textures

The MixTexture class is a more general variation of ScaleTexture. It takes three textures as input: two may be of any single type, and the third must return a floating-point value. The floating-point texture is then used to linearly interpolate between the two other textures. Note that a ConstantTexture could be used for the floating-point value to achieve a uniform blend, or a more complex Texture could be used to blend in a spatially nonuniform way. This texture is defined in textures/mix.h and textures/mix.cpp.

<<MixTexture Declarations>>= 
template <typename T> class MixTexture : public Texture<T> { public: <<MixTexture Public Methods>> 
MixTexture(const std::shared_ptr<Texture<T>> &tex1, const std::shared_ptr<Texture<T>> &tex2, const std::shared_ptr<Texture<Float>> &amount) : tex1(tex1), tex2(tex2), amount(amount) { } T Evaluate(const SurfaceInteraction &si) const { T t1 = tex1->Evaluate(si), t2 = tex2->Evaluate(si); Float amt = amount->Evaluate(si); return (1 - amt) * t1 + amt * t2; }
private: std::shared_ptr<Texture<T>> tex1, tex2; std::shared_ptr<Texture<Float>> amount; };

<<MixTexture Public Methods>>= 
MixTexture(const std::shared_ptr<Texture<T>> &tex1, const std::shared_ptr<Texture<T>> &tex2, const std::shared_ptr<Texture<Float>> &amount) : tex1(tex1), tex2(tex2), amount(amount) { }

To evaluate the mixture, the three textures are evaluated and the floating-point value is used to linearly interpolate between the two. When the blend amount amt is zero, the first texture’s value is returned, and when it is one the second one’s value is returned. We will generally assume that amt will be between zero and one, but this behavior is not enforced, so extrapolation is possible as well. As with the ScaleTexture, antialiasing is ignored, so the introduction of aliasing here is a possibility.

<<MixTexture Public Methods>>+= 
T Evaluate(const SurfaceInteraction &si) const { T t1 = tex1->Evaluate(si), t2 = tex2->Evaluate(si); Float amt = amount->Evaluate(si); return (1 - amt) * t1 + amt * t2; }

10.3.4 Bilinear Interpolation

<<BilerpTexture Declarations>>= 
template <typename T> class BilerpTexture : public Texture<T> { public: <<BilerpTexture Public Methods>> 
BilerpTexture(std::unique_ptr<TextureMapping2D> mapping, const T &v00, const T &v01, const T &v10, const T &v11) : mapping(std::move(mapping)), v00(v00), v01(v01), v10(v10), v11(v11) { } T Evaluate(const SurfaceInteraction &si) const { Vector2f dstdx, dstdy; Point2f st = mapping->Map(si, &dstdx, &dstdy); return (1-st[0]) * (1-st[1]) * v00 + (1-st[0]) * (st[1]) * v01 + ( st[0]) * (1-st[1]) * v10 + ( st[0]) * (st[1]) * v11; }
private: <<BilerpTexture Private Data>> 
std::unique_ptr<TextureMapping2D> mapping; const T v00, v01, v10, v11;
};

The BilerpTexture class provides bilinear interpolation among four constant values. Values are defined at left-parenthesis 0 comma 0 right-parenthesis , left-parenthesis 1 comma 0 right-parenthesis , left-parenthesis 0 comma 1 right-parenthesis , and left-parenthesis 1 comma 1 right-parenthesis in left-parenthesis s comma t right-parenthesis parameter space. The value at a particular left-parenthesis s comma t right-parenthesis position is found by interpolating between them. It is defined in the files textures/bilerp.h and textures/bilerp.cpp.

<<BilerpTexture Public Methods>>= 
BilerpTexture(std::unique_ptr<TextureMapping2D> mapping, const T &v00, const T &v01, const T &v10, const T &v11) : mapping(std::move(mapping)), v00(v00), v01(v01), v10(v10), v11(v11) { }

<<BilerpTexture Private Data>>= 
std::unique_ptr<TextureMapping2D> mapping; const T v00, v01, v10, v11;

The interpolated value of the four values at an left-parenthesis s comma t right-parenthesis position can be computed by three linear interpolations. For example, we can first use s to interpolate between the values at left-parenthesis 0 comma 0 right-parenthesis and left-parenthesis 1 comma 0 right-parenthesis and store that in a temporary tmp1. We can then do the same for the left-parenthesis 0 comma 1 right-parenthesis and left-parenthesis 1 comma 1 right-parenthesis values and store the result in tmp2. Finally, we use t to interpolate between tmp1 and tmp2 and obtain the final result. Mathematically, this is

StartLayout 1st Row 1st Column normal t normal m normal p Subscript 1 2nd Column equals left-parenthesis 1 minus s right-parenthesis normal v 00 plus s normal v 10 2nd Row 1st Column normal t normal m normal p Subscript 2 2nd Column equals left-parenthesis 1 minus s right-parenthesis normal v 01 plus s normal v 11 3rd Row 1st Column normal r normal e normal s normal u normal l normal t 2nd Column equals left-parenthesis 1 minus t right-parenthesis normal t normal m normal p Subscript 1 Baseline plus t normal t normal m normal p Subscript 2 Baseline period EndLayout

Rather than storing the intermediate values explicitly, we can perform some algebraic rearrangement to give us the same result from an appropriately weighted average of the four corner values:

normal r normal e normal s normal u normal l normal t equals left-parenthesis 1 minus s right-parenthesis left-parenthesis 1 minus t right-parenthesis normal v 00 plus left-parenthesis 1 minus s right-parenthesis t normal v 01 plus s left-parenthesis 1 minus t right-parenthesis normal v 10 plus s t normal v 11 period

<<BilerpTexture Public Methods>>+= 
T Evaluate(const SurfaceInteraction &si) const { Vector2f dstdx, dstdy; Point2f st = mapping->Map(si, &dstdx, &dstdy); return (1-st[0]) * (1-st[1]) * v00 + (1-st[0]) * (st[1]) * v01 + ( st[0]) * (1-st[1]) * v10 + ( st[0]) * (st[1]) * v11; }