3.10 Applying Transformations

We can now define routines that perform the appropriate matrix multiplications to transform points and vectors. We will overload the function application operator to describe these transformations; this lets us write code like:

Point3f p = ...; Transform T = ...; Point3f pNew = T(p);

3.10.1 Points

The point transformation routine takes a point left-parenthesis x comma y comma z right-parenthesis and implicitly represents it as the homogeneous column vector left-bracket x y z Baseline 1 right-bracket Superscript upper T Baseline period It then transforms the point by premultiplying this vector with the transformation matrix. Finally, it divides by w to convert back to a nonhomogeneous point representation. For efficiency, this method skips the division by the homogeneous weight, w , when w equals 1 , which is common for most of the transformations that will be used in pbrt—only the projective transformations defined in Chapter 5 will require this division.

<<Transform Inline Methods>>= 
template <typename T> Point3<T> Transform::operator()(Point3<T> p) const { T xp = m[0][0] * p.x + m[0][1] * p.y + m[0][2] * p.z + m[0][3]; T yp = m[1][0] * p.x + m[1][1] * p.y + m[1][2] * p.z + m[1][3]; T zp = m[2][0] * p.x + m[2][1] * p.y + m[2][2] * p.z + m[2][3]; T wp = m[3][0] * p.x + m[3][1] * p.y + m[3][2] * p.z + m[3][3]; if (wp == 1) return Point3<T>(xp, yp, zp); else return Point3<T>(xp, yp, zp) / wp; }

The Transform class also provides a corresponding ApplyInverse() method for each type it transforms. The one for Point3 applies its inverse transformation to the given point. Calling this method is more succinct and generally more efficient than calling Transform::Inverse() and then calling its operator().

<<Transform Public Methods>>+=  
template <typename T> Point3<T> ApplyInverse(Point3<T> p) const;

All subsequent types that can be transformed also have an ApplyInverse() method, though we will not include them in the book text.

3.10.2 Vectors

The transformations of vectors can be computed in a similar fashion. However, the multiplication of the matrix and the column vector is simplified since the implicit homogeneous w coordinate is zero.

<<Transform Inline Methods>>+=  
template <typename T> Vector3<T> Transform::operator()(Vector3<T> v) const { return Vector3<T>(m[0][0] * v.x + m[0][1] * v.y + m[0][2] * v.z, m[1][0] * v.x + m[1][1] * v.y + m[1][2] * v.z, m[2][0] * v.x + m[2][1] * v.y + m[2][2] * v.z); }

3.10.3 Normals

Figure 3.29: Transforming Surface Normals. (a) Original circle, with the normal at a point indicated by an arrow. (b) When scaling the circle to be half as tall in the  y direction, simply treating the normal as a direction and scaling it in the same manner gives a normal that is no longer perpendicular to the surface. (c) A properly transformed normal.

Normals do not transform in the same way that vectors do, as shown in Figure 3.29. Although tangent vectors at a surface transform in the straightforward way, normals require special treatment. Because the normal vector bold n and any tangent vector bold t on the surface are orthogonal by construction, we know that

bold n dot bold t equals bold n Superscript upper T Baseline bold t equals 0 period

When we transform a point on the surface by some matrix bold upper M , the new tangent vector bold t prime at the transformed point is bold upper M bold t . The transformed normal bold n prime should be equal to bold upper S bold n for some 4 times 4 matrix bold upper S . To maintain the orthogonality requirement, we must have

StartLayout 1st Row 1st Column 0 2nd Column equals left-parenthesis bold n prime right-parenthesis Superscript upper T Baseline bold t Superscript prime Baseline 2nd Row 1st Column Blank 2nd Column equals left-parenthesis bold upper S bold n right-parenthesis Superscript upper T Baseline bold upper M bold t 3rd Row 1st Column Blank 2nd Column equals left-parenthesis bold n right-parenthesis Superscript upper T Baseline bold upper S Superscript upper T Baseline bold upper M bold t period EndLayout

This condition holds if bold upper S Superscript upper T Baseline bold upper M equals bold upper I , the identity matrix. Therefore, bold upper S Superscript upper T Baseline equals bold upper M Superscript negative 1 , and so bold upper S equals left-parenthesis bold upper M Superscript negative 1 Baseline right-parenthesis Superscript upper T Baseline comma and we see that normals must be transformed by the inverse transpose of the transformation matrix. This detail is one of the reasons why Transforms maintain their inverses.

Note that this method does not explicitly compute the transpose of the inverse when transforming normals. It just indexes into the inverse matrix in a different order (compare to the code for transforming Vector3fs).

<<Transform Inline Methods>>+=  
template <typename T> Normal3<T> Transform::operator()(Normal3<T> n) const { T x = n.x, y = n.y, z = n.z; return Normal3<T>(mInv[0][0] * x + mInv[1][0] * y + mInv[2][0] * z, mInv[0][1] * x + mInv[1][1] * y + mInv[2][1] * z, mInv[0][2] * x + mInv[1][2] * y + mInv[2][2] * z); }

3.10.4 Rays

Transforming rays is conceptually straightforward: it is a matter of transforming the constituent origin and direction and copying the other data members. (pbrt also provides a similar method for transforming RayDifferentials.)

The approach used in pbrt to manage floating-point round-off error introduces some subtleties that require a small adjustment to the transformed ray origin. The <<Offset ray origin to edge of error bounds and compute tMax>> fragment handles these details; it is defined in Section 6.8.6, where round-off error and pbrt’s mechanisms for dealing with it are discussed.

<<Transform Inline Methods>>+= 
Ray Transform::operator()(const Ray &r, Float *tMax) const { Point3fi o = (*this)(Point3fi(r.o)); Vector3f d = (*this)(r.d); <<Offset ray origin to edge of error bounds and compute tMax>> 
if (Float lengthSquared = LengthSquared(d); lengthSquared > 0) { Float dt = Dot(Abs(d), o.Error()) / lengthSquared; o += d * dt; if (tMax) *tMax -= dt; }
return Ray(Point3f(o), d, r.time, r.medium); }

3.10.5 Bounding Boxes

The easiest way to transform an axis-aligned bounding box is to transform all eight of its corner vertices and then compute a new bounding box that encompasses those points. The implementation of this approach is shown below; one of the exercises for this chapter is to implement a technique to do this computation more efficiently.

<<Transform Method Definitions>>= 
Bounds3f Transform::operator()(const Bounds3f &b) const { Bounds3f bt; for (int i = 0; i < 8; ++i) bt = Union(bt, (*this)(b.Corner(i))); return bt; }

3.10.6 Composition of Transformations

Having defined how the matrices representing individual types of transformations are constructed, we can now consider an aggregate transformation resulting from a series of individual transformations. We will finally see the real value of representing transformations with matrices.

Consider a series of transformations bold upper A bold upper B bold upper C . We would like to compute a new transformation bold upper T such that applying bold upper T gives the same result as applying each of bold upper A , bold upper B , and bold upper C in reverse order; that is, bold upper A left-parenthesis bold upper B left-parenthesis bold upper C left-parenthesis normal p right-parenthesis right-parenthesis right-parenthesis equals bold upper T left-parenthesis normal p right-parenthesis . Such a transformation bold upper T can be computed by multiplying the matrices of the transformations bold upper A , bold upper B , and bold upper C together. In pbrt, we can write:

Transform T = A * B * C;

Then we can apply T to Point3fs p as usual, Point3f pp = T(p), instead of applying each transformation in turn: Point3f pp = A(B(C(p))).

We overload the C++ * operator in the Transform class to compute the new transformation that results from postmultiplying a transformation with another transformation t2. In matrix multiplication, the left-parenthesis i comma j right-parenthesis th element of the resulting matrix is the inner product of the i th row of the first matrix with the j th column of the second.

The inverse of the resulting transformation is equal to the product of t2.mInv * mInv. This is a result of the matrix identity

left-parenthesis bold upper A bold upper B right-parenthesis Superscript negative 1 Baseline equals bold upper B Superscript negative 1 Baseline bold upper A Superscript negative 1 Baseline period

<<Transform Method Definitions>>+=  
Transform Transform::operator*(const Transform &t2) const { return Transform(m * t2.m, t2.mInv * mInv); }

3.10.7 Transformations and Coordinate System Handedness

Certain types of transformations change a left-handed coordinate system into a right-handed one, or vice versa. Some routines will need to know if the handedness of the source coordinate system is different from that of the destination. In particular, routines that want to ensure that a surface normal always points “outside” of a surface might need to flip the normal’s direction after transformation if the handedness changes.

Fortunately, it is easy to tell if handedness is changed by a transformation: it happens only when the determinant of the transformation’s upper-left 3 times 3 submatrix is negative.

<<Transform Method Definitions>>+= 
bool Transform::SwapsHandedness() const { SquareMatrix<3> s(m[0][0], m[0][1], m[0][2], m[1][0], m[1][1], m[1][2], m[2][0], m[2][1], m[2][2]); return Determinant(s) < 0; }

3.10.8 Vector Frames

It is sometimes useful to define a rotation that aligns three orthonormal vectors in a coordinate system with the x , y , and z axes. Applying such a transformation to direction vectors in that coordinate system can simplify subsequent computations. For example, in pbrt, BSDF evaluation is performed in a coordinate system where the surface normal is aligned with the z axis. Among other things, this makes it possible to efficiently evaluate trigonometric functions using functions like the CosTheta() function that was introduced in Section 3.8.3.

The Frame class efficiently represents and performs such transformations, avoiding the full generality (and hence, complexity) of the Transform class. It only needs to store a 3 times 3 matrix, and storing the inverse is unnecessary since it is just the matrix’s transpose, given orthonormal basis vectors.

<<Frame Definition>>= 
class Frame { public: <<Frame Public Methods>> 
Frame() : x(1, 0, 0), y(0, 1, 0), z(0, 0, 1) {} Frame(Vector3f x, Vector3f y, Vector3f z); static Frame FromXZ(Vector3f x, Vector3f z) { return Frame(x, Cross(z, x), z); } static Frame FromXY(Vector3f x, Vector3f y) { return Frame(x, y, Cross(x, y)); } static Frame FromZ(Vector3f z) { Vector3f x, y; CoordinateSystem(z, &x, &y); return Frame(x, y, z); } static Frame FromX(Vector3f x) { Vector3f y, z; CoordinateSystem(x, &y, &z); return Frame(x, y, z); } static Frame FromY(Vector3f y) { Vector3f x, z; CoordinateSystem(y, &z, &x); return Frame(x, y, z); } static Frame FromX(Normal3f x) { Vector3f y, z; CoordinateSystem(x, &y, &z); return Frame(Vector3f(x), y, z); } static Frame FromY(Normal3f y) { Vector3f x, z; CoordinateSystem(y, &z, &x); return Frame(x, Vector3f(y), z); } PBRT_CPU_GPU static Frame FromZ(Normal3f z) { return FromZ(Vector3f(z)); } Vector3f ToLocal(Vector3f v) const { return Vector3f(Dot(v, x), Dot(v, y), Dot(v, z)); } Normal3f ToLocal(Normal3f n) const { return Normal3f(Dot(n, x), Dot(n, y), Dot(n, z)); } Vector3f FromLocal(Vector3f v) const { return v.x * x + v.y * y + v.z * z; } Normal3f FromLocal(Normal3f n) const { return Normal3f(n.x * x + n.y * y + n.z * z); } std::string ToString() const { return StringPrintf("[ Frame x: %s y: %s z: %s ]", x, y, z); }
<<Frame Public Members>> 
Vector3f x, y, z;
};

Given three orthonormal vectors bold x , bold y , and bold z , the matrix bold upper F that transforms vectors into their space is

bold upper F equals Start 3 By 3 Matrix 1st Row 1st Column bold x Subscript x Baseline 2nd Column bold x Subscript y Baseline 3rd Column bold x Subscript z Baseline 2nd Row 1st Column bold y Subscript x Baseline 2nd Column bold y Subscript y Baseline 3rd Column bold y Subscript z Baseline 3rd Row 1st Column bold z Subscript x Baseline 2nd Column bold z Subscript y Baseline 3rd Column bold z Subscript z Baseline EndMatrix equals Start 3 By 1 Matrix 1st Row bold x 2nd Row bold y 3rd Row bold z EndMatrix period

The Frame stores this matrix using three Vector3fs.

<<Frame Public Members>>= 
Vector3f x, y, z;

The three basis vectors can be specified explicitly; in debug builds, DCHECK()s in the constructor ensure that the provided vectors are orthonormal.

<<Frame Public Methods>>= 
Frame() : x(1, 0, 0), y(0, 1, 0), z(0, 0, 1) {} Frame(Vector3f x, Vector3f y, Vector3f z);

Frame also provides convenience methods that construct a frame from just two of the basis vectors, using the cross product to compute the third.

<<Frame Public Methods>>+=  
static Frame FromXZ(Vector3f x, Vector3f z) { return Frame(x, Cross(z, x), z); } static Frame FromXY(Vector3f x, Vector3f y) { return Frame(x, y, Cross(x, y)); }

Only the z axis vector can be provided as well, in which case the others are set arbitrarily.

<<Frame Public Methods>>+=  
static Frame FromZ(Vector3f z) { Vector3f x, y; CoordinateSystem(z, &x, &y); return Frame(x, y, z); }

A variety of other functions, not included here, allow specifying a frame using a normal vector and specifying it via just the x or y basis vector.

Transforming a vector into the frame’s coordinate space is done using the bold upper F matrix. Because Vector3fs were used to store its rows, the matrix-vector product can be expressed as three dot products.

<<Frame Public Methods>>+=  
Vector3f ToLocal(Vector3f v) const { return Vector3f(Dot(v, x), Dot(v, y), Dot(v, z)); }

A ToLocal() method is also provided for normal vectors. In this case, we do not need to compute the inverse transpose of bold upper F for the transformation normals (recall the discussion of transforming normals in Section 3.10.3). Because bold upper F is an orthonormal matrix (its rows and columns are mutually orthogonal and unit length), its inverse is equal to its transpose, so it is its own inverse transpose already.

<<Frame Public Methods>>+=  
Normal3f ToLocal(Normal3f n) const { return Normal3f(Dot(n, x), Dot(n, y), Dot(n, z)); }

The method that transforms vectors out of the frame’s local space transposes bold upper F to find its inverse before multiplying by the vector. In this case, the resulting computation can be expressed as the sum of three scaled versions of the matrix columns. As before, surface normals transform as regular vectors. (That method is not included here.)

<<Frame Public Methods>>+= 
Vector3f FromLocal(Vector3f v) const { return v.x * x + v.y * y + v.z * z; }

For convenience, there is a Transform constructor that takes a Frame. Its simple implementation is not included here.

<<Transform Public Methods>>+=  
explicit Transform(const Frame &frame);