3.2 n-Tuple Base Classes

pbrt’s classes that represent two- and three-dimensional points, vectors, and surface normals are all based on general n -tuple classes, whose definitions we will start with. The definitions of these classes as well as the types that inherit from them are defined in the files util/vecmath.h and util/vecmath.cpp under the main pbrt source directory.

Although this and the following few sections define classes that have simple logic in most of their method implementations, they make more use of advanced C++ programming techniques than we generally use in pbrt. Doing so reduces the amount of redundant code needed to implement the point, vector, and normal classes and makes them extensible in ways that will be useful later. If you are not a C++ expert, it is fine to gloss over these details and to focus on understanding the functionality that these classes provide. Alternatively, you could use this as an opportunity to learn more corners of the language.

Both Tuple2 and Tuple3 are template classes. They are templated not just on a type used for storing each coordinate’s value but also on the type of the class that inherits from it to define a specific two- or three-dimensional type. If one has not seen it before, this is a strange construction: normally, inheritance is sufficient, and the base class has no need to know the type of the subclass. In this case, having the base class know the child class’s type makes it possible to write generic methods that operate on and return values of the child type, as we will see shortly.

<<Tuple2 Definition>>= 
template <template <typename> class Child, typename T> class Tuple2 { public: <<Tuple2 Public Methods>> 
static const int nDimensions = 2; Tuple2() = default; PBRT_CPU_GPU Tuple2(T x, T y) : x(x), y(y) { DCHECK(!HasNaN()); } PBRT_CPU_GPU bool HasNaN() const { return IsNaN(x) || IsNaN(y); } #ifdef PBRT_DEBUG_BUILD // The default versions of these are fine for release builds; for debug // we define them so that we can add the Assert checks. PBRT_CPU_GPU Tuple2(Child<T> c) { DCHECK(!c.HasNaN()); x = c.x; y = c.y; } PBRT_CPU_GPU Child<T> &operator=(Child<T> c) { DCHECK(!c.HasNaN()); x = c.x; y = c.y; return static_cast<Child<T> &>(*this); } #endif template <typename U> PBRT_CPU_GPU auto operator+(Child<U> c) const -> Child<decltype(T{} + U{})> { DCHECK(!c.HasNaN()); return {x + c.x, y + c.y}; } template <typename U> PBRT_CPU_GPU Child<T> &operator+=(Child<U> c) { DCHECK(!c.HasNaN()); x += c.x; y += c.y; return static_cast<Child<T> &>(*this); } template <typename U> PBRT_CPU_GPU auto operator-(Child<U> c) const -> Child<decltype(T{} - U{})> { DCHECK(!c.HasNaN()); return {x - c.x, y - c.y}; } template <typename U> PBRT_CPU_GPU Child<T> &operator-=(Child<U> c) { DCHECK(!c.HasNaN()); x -= c.x; y -= c.y; return static_cast<Child<T> &>(*this); } PBRT_CPU_GPU bool operator==(Child<T> c) const { return x == c.x && y == c.y; } PBRT_CPU_GPU bool operator!=(Child<T> c) const { return x != c.x || y != c.y; } template <typename U> PBRT_CPU_GPU auto operator*(U s) const -> Child<decltype(T{} * U{})> { return {s * x, s * y}; } template <typename U> PBRT_CPU_GPU Child<T> &operator*=(U s) { DCHECK(!IsNaN(s)); x *= s; y *= s; return static_cast<Child<T> &>(*this); } template <typename U> PBRT_CPU_GPU auto operator/(U d) const -> Child<decltype(T{} / U{})> { DCHECK(d != 0 && !IsNaN(d)); return {x / d, y / d}; } template <typename U> PBRT_CPU_GPU Child<T> &operator/=(U d) { DCHECK_NE(d, 0); DCHECK(!IsNaN(d)); x /= d; y /= d; return static_cast<Child<T> &>(*this); } PBRT_CPU_GPU Child<T> operator-() const { return {-x, -y}; } PBRT_CPU_GPU T operator[](int i) const { DCHECK(i >= 0 && i <= 1); return (i == 0) ? x : y; } PBRT_CPU_GPU T &operator[](int i) { DCHECK(i >= 0 && i <= 1); return (i == 0) ? x : y; } std::string ToString() const { return internal::ToString2(x, y); }
<<Tuple2 Public Members>> 
T x{}, y{};
};

The two-dimensional tuple stores its values as x and y and makes them available as public member variables. The pair of curly braces after each one ensures that the member variables are default initialized; for numeric types, this initializes them to 0.

<<Tuple2 Public Members>>= 
T x{}, y{};

We will focus on the Tuple3 implementation for the remainder of this section. Tuple2 is almost entirely the same but with one fewer coordinate.

<<Tuple3 Definition>>= 
template <template <typename> class Child, typename T> class Tuple3 { public: <<Tuple3 Public Methods>> 
Tuple3(T x, T y, T z) : x(x), y(y), z(z) { DCHECK(!HasNaN()); } bool HasNaN() const { return IsNaN(x) || IsNaN(y) || IsNaN(z); } T operator[](int i) const { if (i == 0) return x; if (i == 1) return y; return z; } T &operator[](int i) { if (i == 0) return x; if (i == 1) return y; return z; } template <typename U> auto operator+(Child<U> c) const -> Child<decltype(T{} + U{})> { return {x + c.x, y + c.y, z + c.z}; } static const int nDimensions = 3; #ifdef PBRT_DEBUG_BUILD // The default versions of these are fine for release builds; for debug // we define them so that we can add the Assert checks. PBRT_CPU_GPU Tuple3(Child<T> c) { DCHECK(!c.HasNaN()); x = c.x; y = c.y; z = c.z; } PBRT_CPU_GPU Child<T> &operator=(Child<T> c) { DCHECK(!c.HasNaN()); x = c.x; y = c.y; z = c.z; return static_cast<Child<T> &>(*this); } #endif template <typename U> PBRT_CPU_GPU Child<T> &operator+=(Child<U> c) { DCHECK(!c.HasNaN()); x += c.x; y += c.y; z += c.z; return static_cast<Child<T> &>(*this); } template <typename U> PBRT_CPU_GPU auto operator-(Child<U> c) const -> Child<decltype(T{} - U{})> { DCHECK(!c.HasNaN()); return {x - c.x, y - c.y, z - c.z}; } template <typename U> PBRT_CPU_GPU Child<T> &operator-=(Child<U> c) { DCHECK(!c.HasNaN()); x -= c.x; y -= c.y; z -= c.z; return static_cast<Child<T> &>(*this); } PBRT_CPU_GPU bool operator==(Child<T> c) const { return x == c.x && y == c.y && z == c.z; } PBRT_CPU_GPU bool operator!=(Child<T> c) const { return x != c.x || y != c.y || z != c.z; } template <typename U> PBRT_CPU_GPU auto operator*(U s) const -> Child<decltype(T{} * U{})> { return {s * x, s * y, s * z}; } template <typename U> PBRT_CPU_GPU Child<T> &operator*=(U s) { DCHECK(!IsNaN(s)); x *= s; y *= s; z *= s; return static_cast<Child<T> &>(*this); } template <typename U> PBRT_CPU_GPU auto operator/(U d) const -> Child<decltype(T{} / U{})> { DCHECK_NE(d, 0); return {x / d, y / d, z / d}; } template <typename U> PBRT_CPU_GPU Child<T> &operator/=(U d) { DCHECK_NE(d, 0); x /= d; y /= d; z /= d; return static_cast<Child<T> &>(*this); } PBRT_CPU_GPU Child<T> operator-() const { return {-x, -y, -z}; } std::string ToString() const { return internal::ToString3(x, y, z); }
<<Tuple3 Public Members>> 
T x{}, y{}, z{};
};

By default, the left-parenthesis x comma y comma z right-parenthesis values are set to zero, although the user of the class can optionally supply values for each of the components. If the user does supply values, the constructor checks that none of them has the floating-point “not a number” (NaN) value using the DCHECK() macro. When compiled in optimized mode, this macro disappears from the compiled code, saving the expense of verifying this case. NaNs almost certainly indicate a bug in the system; if a NaN is generated by some computation, we would like to catch it as soon as possible in order to make isolating its source easier. (See Section 6.8.1 for more discussion of NaN values.)

<<Tuple3 Public Methods>>= 
Tuple3(T x, T y, T z) : x(x), y(y), z(z) { DCHECK(!HasNaN()); }

Readers who have been exposed to object-oriented design may question our decision to make the tuple component values publicly accessible. Typically, member variables are only accessible inside their class, and external code that wishes to access or modify the contents of a class must do so through a well-defined API that may include selector and mutator functions. Although we are sympathetic to the principle of encapsulation, it is not appropriate here. The purpose of selector and mutator functions is to hide the class’s internal implementation details. In the case of three-dimensional tuples, hiding this basic part of their design gains nothing and adds bulk to code that uses them.

<<Tuple3 Public Members>>= 
T x{}, y{}, z{};

The HasNaN() test checks each component individually.

<<Tuple3 Public Methods>>+=  
bool HasNaN() const { return IsNaN(x) || IsNaN(y) || IsNaN(z); }

An alternate implementation of these two tuple classes would be to have a single template class that is also parameterized with an integer number of dimensions and to represent the coordinates with an array of that many T values. While this approach would reduce the total amount of code by eliminating the need for separate two- and three-dimensional tuple types, individual components of the vector could not be accessed as v.x and so forth. We believe that, in this case, a bit more code in the vector implementations is worthwhile in return for more transparent access to components. However, some routines do find it useful to be able to easily loop over the components of vectors; the tuple classes also provide a C++ operator to index into the components so that, given an instance v, v[0] == v.x and so forth.

<<Tuple3 Public Methods>>+=  
T operator[](int i) const { if (i == 0) return x; if (i == 1) return y; return z; }

If the tuple type is non-const, then indexing returns a reference, allowing components of the tuple to be set.

<<Tuple3 Public Methods>>+=  
T &operator[](int i) { if (i == 0) return x; if (i == 1) return y; return z; }

We can now turn to the implementation of arithmetic operations that operate on the values stored in a tuple. Their code is fairly dense. For example, here is the method that adds together two three-tuples of some type (for example, Child might be Vector3, the forthcoming three-dimensional vector type).

<<Tuple3 Public Methods>>+= 
template <typename U> auto operator+(Child<U> c) const -> Child<decltype(T{} + U{})> { return {x + c.x, y + c.y, z + c.z}; }

There are a few things to note in the implementation of operator+. By virtue of being a template method based on another type U, it supports adding two elements of the same Child template type, though they may use different types for storing their components (T and U in the code here). However, because the base type of the method’s parameter is Child, it is only possible to add two values of the same child type using this method. If this method instead took a Tuple3 for the parameter, then it would silently allow addition with any type that inherited from Tuple3, which might not be intended.

There are two interesting things in the declaration of the return type, to the right of the -> operator after the method’s parameter list. First, the base return type is Child; thus, if one adds two Vector3 values, the returned value will be of Vector3 type. This, too, eliminates a class of potential errors: if a Tuple3 was returned, then it would for example be possible to add two Vector3s and assign the result to a Point3, which is nonsensical. Finally, the component type of the returned type is determined based on the type of an expression adding values of types T and U. Thus, this method follows C++’s standard type promotion rules: if a Vector3 that stored integer values is added to one that stores Floats, the result is a Vector3 storing Floats.

In the interests of space, we will not include the other Tuple3 arithmetic operators here, nor will we include the various other utility functions that perform component-wise operations on them. The full list of capabilities provided by Tuple2 and Tuple3 is:

  • The basic arithmetic operators of per-component addition, subtraction, and negation, including the “in place” (e.g., operator+=) forms of them.
  • Component-wise multiplication and division by a scalar value, including “in place” variants.
  • Abs(a), which returns a value where the absolute value of each component of the tuple type has been taken.
  • Ceil(a) and Floor(a), which return a value where the components have been rounded up or down to the nearest integer value, respectively.
  • Lerp(t, a, b), which returns the result of the linear interpolation (1-t)*a + t*b.
  • FMA(a, b, c), which takes three tuples and returns the result of a component-wise fused multiply-add a*b + c.
  • Min(a, b) and Max(a, b), which respectively return the component-wise minimum and maximum of the two given tuples.
  • MinComponentValue(a) and MaxComponentValue(a), which respectively return the minimum and maximum value of the tuple’s components.
  • MinComponentIndex(a) and MaxComponentIndex(a), which respectively return the zero-based index of the tuple element with minimum or maximum value.
  • Permute(a, perm), which returns the permutation of the tuple according to an array of indices.
  • HProd(a), which returns the horizontal product—the component values multiplied together.