2.7 Transformations

In general, a transformation bold upper T is a mapping from points to points and from vectors to vectors:

normal p prime equals bold upper T left-parenthesis normal p right-parenthesis bold v Superscript prime Baseline equals bold upper T left-parenthesis bold v right-parenthesis period

The transformation bold upper T may be an arbitrary procedure. However, we will consider a subset of all possible transformations in this chapter. In particular, they will be

  • Linear: If bold upper T is an arbitrary linear transformation and s is an arbitrary scalar, then bold upper T left-parenthesis s bold v right-parenthesis equals s bold upper T left-parenthesis bold v right-parenthesis and bold upper T left-parenthesis bold v 1 plus bold v 2 right-parenthesis equals bold upper T left-parenthesis bold v 1 right-parenthesis plus bold upper T left-parenthesis bold v 2 right-parenthesis . These two properties can greatly simplify reasoning about transformations.
  • Continuous: Roughly speaking, bold upper T maps the neighborhoods around normal p and bold v to neighborhoods around normal p prime and bold v prime .
  • One-to-one and invertible: For each normal p , bold upper T maps normal p to a single unique normal p prime . Furthermore, there exists an inverse transform bold upper T Superscript negative 1 that maps normal p prime back to normal p .

We will often want to take a point, vector, or normal defined with respect to one coordinate frame and find its coordinate values with respect to another frame. Using basic properties of linear algebra, a 4 times 4 matrix can be shown to express the linear transformation of a point or vector from one frame to another. Furthermore, such a 4 times 4 matrix suffices to express all linear transformations of points and vectors within a fixed frame, such as translation in space or rotation around a point. Therefore, there are two different (and incompatible!) ways that a matrix can be interpreted:

  • Transformation of the frame: Given a point, the matrix could express how to compute a new point in the same frame that represents the transformation of the original point (e.g., by translating it in some direction).
  • Transformation from one frame to another: A matrix can express the coordinates of a point or vector in a new frame in terms of the coordinates in the original frame.

Most uses of transformations in pbrt are for transforming points from one frame to another.

In general, transformations make it possible to work in the most convenient coordinate space. For example, we can write routines that define a virtual camera, assuming that the camera is located at the origin, looks down the z axis, and has the y axis pointing up and the x axis pointing right. These assumptions greatly simplify the camera implementation. Then, to place the camera at any point in the scene looking in any direction, we just construct a transformation that maps points in the scene’s coordinate system to the camera’s coordinate system. (See Section 6.1.1 for more information about camera coordinate spaces in pbrt.)

2.7.1 Homogeneous Coordinates

Given a frame defined by left-parenthesis normal p Subscript normal o Baseline comma bold v bold 1 comma bold v bold 2 comma bold v bold 3 right-parenthesis , there is ambiguity between the representation of a point left-parenthesis normal p Subscript x Baseline comma normal p Subscript y Baseline comma normal p Subscript z Baseline right-parenthesis and a vector left-parenthesis bold v Subscript x Baseline comma bold v Subscript y Baseline comma bold v Subscript z Baseline right-parenthesis with the same left-parenthesis x comma y comma z right-parenthesis coordinates. Using the representations of points and vectors introduced at the start of the chapter, we can write the point as the inner product left-bracket s 1 s 2 s 3 1 right-bracket left-bracket bold v bold 1 bold v bold 2 bold v bold 3 normal p Subscript normal o Baseline right-bracket Superscript upper T and the vector as the inner product left-bracket s prime 1 s prime 2 s prime 3 0 right-bracket left-bracket bold v bold 1 bold v bold 2 bold v bold 3 normal p Subscript normal o Baseline right-bracket Superscript upper T Baseline period These four-vectors of three s Subscript i values and a zero or one are called the homogeneous representations of the point and the vector. The fourth coordinate of the homogeneous representation is sometimes called the weight. For a point, its value can be any scalar other than zero: the homogeneous points left-bracket 1 comma 3 comma negative 2 comma 1 right-bracket and left-bracket negative 2 comma negative 6 comma 4 comma negative 2 right-bracket describe the same Cartesian point left-parenthesis 1 comma 3 comma negative 2 right-parenthesis . Converting homogeneous points into ordinary points entails dividing the first three components by the weight:

left-parenthesis x comma y comma z comma w right-parenthesis right-arrow left-parenthesis StartFraction x Over w EndFraction comma StartFraction y Over w EndFraction comma StartFraction z Over w EndFraction right-parenthesis period

We will use these facts to see how a transformation matrix can describe how points and vectors in one frame can be mapped to another frame. Consider a matrix bold upper M that describes the transformation from one coordinate system to another:

bold upper M equals Start 4 By 4 Matrix 1st Row 1st Column m Subscript 0 comma 0 Baseline 2nd Column m Subscript 0 comma 1 Baseline 3rd Column m Subscript 0 comma 2 Baseline 4th Column m Subscript 0 comma 3 Baseline 2nd Row 1st Column m Subscript 1 comma 0 Baseline 2nd Column m Subscript 1 comma 1 Baseline 3rd Column m Subscript 1 comma 2 Baseline 4th Column m Subscript 1 comma 3 Baseline 3rd Row 1st Column m Subscript 2 comma 0 Baseline 2nd Column m Subscript 2 comma 1 Baseline 3rd Column m Subscript 2 comma 2 Baseline 4th Column m Subscript 2 comma 3 Baseline 4th Row 1st Column m Subscript 3 comma 0 Baseline 2nd Column m Subscript 3 comma 1 Baseline 3rd Column m Subscript 3 comma 2 Baseline 4th Column m Subscript 3 comma 3 Baseline EndMatrix period

(In this book, we define matrix element indices starting from zero, so that equations and source code correspond more directly.) Then if the transformation represented by bold upper M is applied to the x axis vector left-parenthesis 1 comma 0 comma 0 right-parenthesis , we have

bold upper M bold x equals bold upper M left-bracket 1 0 0 0 right-bracket Superscript upper T Baseline equals left-bracket m Subscript 0 comma 0 Baseline m Subscript 1 comma 0 Baseline m Subscript 2 comma 0 Baseline m Subscript 3 comma 0 Baseline right-bracket Superscript upper T Baseline period

Thus, directly reading the columns of the matrix shows how the basis vectors and the origin of the current coordinate system are transformed by the matrix:

StartLayout 1st Row 1st Column bold upper M bold y 2nd Column equals left-bracket m Subscript 0 comma 1 Baseline m Subscript 1 comma 1 Baseline m Subscript 2 comma 1 Baseline m Subscript 3 comma 1 Baseline right-bracket Superscript upper T Baseline 2nd Row 1st Column bold upper M bold z 2nd Column equals left-bracket m Subscript 0 comma 2 Baseline m Subscript 1 comma 2 Baseline m Subscript 2 comma 2 Baseline m Subscript 3 comma 2 Baseline right-bracket Superscript upper T Baseline 3rd Row 1st Column bold upper M normal p 2nd Column equals left-bracket m Subscript 0 comma 3 Baseline m Subscript 1 comma 3 Baseline m Subscript 2 comma 3 Baseline m Subscript 3 comma 3 Baseline right-bracket Superscript upper T Baseline period EndLayout

In general, by characterizing how the basis is transformed, we know how any point or vector specified in terms of that basis is transformed. Because points and vectors in the current coordinate system are expressed in terms of the current coordinate system’s frame, applying the transformation to them directly is equivalent to applying the transformation to the current coordinate system’s basis and finding their coordinates in terms of the transformed basis.

We will not use homogeneous coordinates explicitly in our code; there is no Homogeneous class in pbrt. However, the various transformation routines in the next section will implicitly convert points, vectors, and normals to homogeneous form, transform the homogeneous points, and then convert them back before returning the result. This isolates the details of homogeneous coordinates in one place (namely, the implementation of transformations).

<<Transform Declarations>>= 
class Transform { public: <<Transform Public Methods>> 
Transform() { } Transform(const Float mat[4][4]) { m = Matrix4x4(mat[0][0], mat[0][1], mat[0][2], mat[0][3], mat[1][0], mat[1][1], mat[1][2], mat[1][3], mat[2][0], mat[2][1], mat[2][2], mat[2][3], mat[3][0], mat[3][1], mat[3][2], mat[3][3]); mInv = Inverse(m); } Transform(const Matrix4x4 &m) : m(m), mInv(Inverse(m)) { } Transform(const Matrix4x4 &m, const Matrix4x4 &mInv) : m(m), mInv(mInv) { } void Print(FILE *f) const; friend Transform Inverse(const Transform &t) { return Transform(t.mInv, t.m); } friend Transform Transpose(const Transform &t) { return Transform(Transpose(t.m), Transpose(t.mInv)); } bool operator==(const Transform &t) const { return t.m == m && t.mInv == mInv; } bool operator!=(const Transform &t) const { return t.m != m || t.mInv != mInv; } bool operator<(const Transform &t2) const { for (int i = 0; i < 4; ++i) for (int j = 0; j < 4; ++j) { if (m.m[i][j] < t2.m.m[i][j]) return true; if (m.m[i][j] > t2.m.m[i][j]) return false; } return false; } bool IsIdentity() const { return (m.m[0][0] == 1.f && m.m[0][1] == 0.f && m.m[0][2] == 0.f && m.m[0][3] == 0.f && m.m[1][0] == 0.f && m.m[1][1] == 1.f && m.m[1][2] == 0.f && m.m[1][3] == 0.f && m.m[2][0] == 0.f && m.m[2][1] == 0.f && m.m[2][2] == 1.f && m.m[2][3] == 0.f && m.m[3][0] == 0.f && m.m[3][1] == 0.f && m.m[3][2] == 0.f && m.m[3][3] == 1.f); } const Matrix4x4 &GetMatrix() const { return m; } const Matrix4x4 &GetInverseMatrix() const { return mInv; } bool HasScale() const { Float la2 = (*this)(Vector3f(1, 0, 0)).LengthSquared(); Float lb2 = (*this)(Vector3f(0, 1, 0)).LengthSquared(); Float lc2 = (*this)(Vector3f(0, 0, 1)).LengthSquared(); #define NOT_ONE(x) ((x) < .999f || (x) > 1.001f) return (NOT_ONE(la2) || NOT_ONE(lb2) || NOT_ONE(lc2)); #undef NOT_ONE } template <typename T> inline Point3<T> operator()(const Point3<T> &p) const; template <typename T> inline Vector3<T> operator()(const Vector3<T> &v) const; template <typename T> inline Normal3<T> operator()(const Normal3<T> &) const; template <typename T> inline void operator()(const Normal3<T> &, Normal3<T> *nt) const; inline Ray operator()(const Ray &r) const; inline RayDifferential operator()(const RayDifferential &r) const; Bounds3f operator()(const Bounds3f &b) const; Transform operator*(const Transform &t2) const; bool SwapsHandedness() const; SurfaceInteraction operator()(const SurfaceInteraction &si) const; template <typename T> inline Point3<T> operator()(const Point3<T> &pt, Vector3<T> *absError) const; template <typename T> inline Point3<T> operator()(const Point3<T> &p, const Vector3<T> &pError, Vector3<T> *pTransError) const; template <typename T> inline Vector3<T> operator()(const Vector3<T> &v, Vector3<T> *vTransError) const; template <typename T> inline Vector3<T> operator()(const Vector3<T> &v, const Vector3<T> &vError, Vector3<T> *vTransError) const; inline Ray operator()(const Ray &r, Vector3f *oError, Vector3f *dError) const; inline Ray operator()(const Ray &r, const Vector3f &oErrorIn, const Vector3f &dErrorIn, Vector3f *oErrorOut, Vector3f *dErrorOut) const;
private: <<Transform Private Data>> 
Matrix4x4 m, mInv; friend class AnimatedTransform; friend struct Quaternion;
};

A transformation is represented by the elements of the matrix m, a Matrix4x4 object. The low-level Matrix4x4 class is defined in Section A.5.3. The matrix m is stored in row-major form, so element m[i][j] corresponds to m Subscript i comma j , where  i is the row number and  j is the column number. For convenience, the Transform also stores the inverse of the matrix m in the Transform::mInv member; for pbrt’s needs, it is better to have the inverse easily available than to repeatedly compute it as needed.

This representation of transformations is relatively memory hungry: assuming 4 bytes of storage for a Float value, a Transform requires 128 bytes of storage. Used naïvely, this approach can be wasteful; if a scene has millions of shapes but only a few thousand unique transformations, there’s no reason to redundantly store the same transform many times in memory. Therefore, Shapes in pbrt store a pointer to a Transform, and the scene specification code defined in Section A.3.5 uses a TransformCache to ensure that all shapes that share the same transformation point to a single instance of that transformation in memory.

This decision to share transformations implies a loss of flexibility, however: the elements of a Transform shouldn’t be modified after it is created if the Transform is shared by multiple objects in the scene (and those objects don’t expect it to be changing.) This limitation isn’t a problem in practice, since the transformations in a scene are typically created when pbrt parses the scene description file and don’t need to change later at rendering time.

<<Transform Private Data>>= 
Matrix4x4 m, mInv;

2.7.2 Basic Operations

When a new Transform is created, it defaults to the identity transformation—the transformation that maps each point and each vector to itself. This transformation is represented by the identity matrix:

bold upper I equals Start 4 By 4 Matrix 1st Row 1st Column 1 2nd Column 0 3rd Column 0 4th Column 0 2nd Row 1st Column 0 2nd Column 1 3rd Column 0 4th Column 0 3rd Row 1st Column 0 2nd Column 0 3rd Column 1 4th Column 0 4th Row 1st Column 0 2nd Column 0 3rd Column 0 4th Column 1 EndMatrix period

The implementation here relies on the default Matrix4x4 constructor to fill in the identity matrix for m and mInv.

<<Transform Public Methods>>= 
Transform() { }

A Transform can also be created from a given matrix. In this case, the given matrix must be explicitly inverted.

<<Transform Public Methods>>+=  
Transform(const Float mat[4][4]) { m = Matrix4x4(mat[0][0], mat[0][1], mat[0][2], mat[0][3], mat[1][0], mat[1][1], mat[1][2], mat[1][3], mat[2][0], mat[2][1], mat[2][2], mat[2][3], mat[3][0], mat[3][1], mat[3][2], mat[3][3]); mInv = Inverse(m); }

<<Transform Public Methods>>+=  
Transform(const Matrix4x4 &m) : m(m), mInv(Inverse(m)) { }

The most commonly used constructor takes a reference to the transformation matrix along with an explicitly provided inverse. This is a superior approach to computing the inverse in the constructor because many geometric transformations have very simple inverses and we can avoid the expense and potential loss of numeric precision from computing a general 4 times 4 matrix inverse. Of course, this places the burden on the caller to make sure that the supplied inverse is correct.

<<Transform Public Methods>>+=  
Transform(const Matrix4x4 &m, const Matrix4x4 &mInv) : m(m), mInv(mInv) { }

The Transform representing the inverse of a Transform can be returned by just swapping the roles of mInv and m.

<<Transform Public Methods>>+=  
friend Transform Inverse(const Transform &t) { return Transform(t.mInv, t.m); }

Transposing the two matrices in the transform to compute a new transform can also be useful.

<<Transform Public Methods>>+=  
friend Transform Transpose(const Transform &t) { return Transform(Transpose(t.m), Transpose(t.mInv)); }

We provide Transform equality (and inequality) testing methods; their implementation is straightforward and not included here. Transform also provides an IsIdentity() method that checks to see if the transformation is the identity.

2.7.3 Translations

One of the simplest transformations is the translation transformation, bold upper T left-parenthesis normal upper Delta x comma normal upper Delta y comma normal upper Delta z right-parenthesis . When applied to a point normal p , it translates normal p ’s coordinates by normal upper Delta x , normal upper Delta y , and normal upper Delta z , as shown in Figure 2.10. As an example, bold upper T left-parenthesis 2 comma 2 comma 1 right-parenthesis left-parenthesis x comma y comma z right-parenthesis equals left-parenthesis x plus 2 comma y plus 2 comma z plus 1 right-parenthesis .

Figure 2.10: Translation in 2D. Adding offsets normal upper Delta x and normal upper Delta y to a point’s coordinates correspondingly changes its position in space.

Translation has some simple properties:

StartLayout 1st Row 1st Column bold upper T left-parenthesis 0 comma 0 comma 0 right-parenthesis 2nd Column equals bold upper I 2nd Row 1st Column bold upper T left-parenthesis x 1 comma y 1 comma z 1 right-parenthesis bold upper T left-parenthesis x 2 comma y 2 comma z 2 right-parenthesis 2nd Column equals bold upper T left-parenthesis x 1 plus x 2 comma y 1 plus y 2 comma z 1 plus z 2 right-parenthesis 3rd Row 1st Column bold upper T left-parenthesis x 1 comma y 1 comma z 1 right-parenthesis bold upper T left-parenthesis x 2 comma y 2 comma z 2 right-parenthesis 2nd Column equals bold upper T left-parenthesis x 2 comma y 2 comma z 2 right-parenthesis bold upper T left-parenthesis x 1 comma y 1 comma z 1 right-parenthesis 4th Row 1st Column bold upper T Superscript negative 1 Baseline left-parenthesis x comma y comma z right-parenthesis 2nd Column equals bold upper T left-parenthesis negative x comma negative y comma negative z right-parenthesis period EndLayout

Translation only affects points, leaving vectors unchanged. In matrix form, the translation transformation is

bold upper T left-parenthesis normal upper Delta x comma normal upper Delta y comma normal upper Delta z right-parenthesis equals Start 4 By 4 Matrix 1st Row 1st Column 1 2nd Column 0 3rd Column 0 4th Column normal upper Delta x 2nd Row 1st Column 0 2nd Column 1 3rd Column 0 4th Column normal upper Delta y 3rd Row 1st Column 0 2nd Column 0 3rd Column 1 4th Column normal upper Delta z 4th Row 1st Column 0 2nd Column 0 3rd Column 0 4th Column 1 EndMatrix period

When we consider the operation of a translation matrix on a point, we see the value of homogeneous coordinates. Consider the product of the matrix for bold upper T left-parenthesis normal upper Delta x comma normal upper Delta y comma normal upper Delta z right-parenthesis with a point normal p in homogeneous coordinates left-bracket x y z Baseline 1 right-bracket Superscript upper T :

Start 4 By 4 Matrix 1st Row 1st Column 1 2nd Column 0 3rd Column 0 4th Column normal upper Delta x 2nd Row 1st Column 0 2nd Column 1 3rd Column 0 4th Column normal upper Delta y 3rd Row 1st Column 0 2nd Column 0 3rd Column 1 4th Column normal upper Delta z 4th Row 1st Column 0 2nd Column 0 3rd Column 0 4th Column 1 EndMatrix Start 4 By 1 Matrix 1st Row x 2nd Row y 3rd Row z 4th Row 1 EndMatrix equals Start 4 By 1 Matrix 1st Row x plus normal upper Delta x 2nd Row y plus normal upper Delta y 3rd Row z plus normal upper Delta z 4th Row 1 EndMatrix period

As expected, we have computed a new point with its coordinates offset by left-parenthesis normal upper Delta x comma normal upper Delta y comma normal upper Delta z right-parenthesis . However, if we apply bold upper T to a vector bold v , we have

Start 4 By 4 Matrix 1st Row 1st Column 1 2nd Column 0 3rd Column 0 4th Column normal upper Delta x 2nd Row 1st Column 0 2nd Column 1 3rd Column 0 4th Column normal upper Delta y 3rd Row 1st Column 0 2nd Column 0 3rd Column 1 4th Column normal upper Delta z 4th Row 1st Column 0 2nd Column 0 3rd Column 0 4th Column 1 EndMatrix Start 4 By 1 Matrix 1st Row x 2nd Row y 3rd Row z 4th Row 0 EndMatrix equals Start 4 By 1 Matrix 1st Row x 2nd Row y 3rd Row z 4th Row 0 EndMatrix period

The result is the same vector bold v . This makes sense because vectors represent directions, so translation leaves them unchanged.

We will define a routine that creates a new Transform matrix to represent a given translation—it is a straightforward application of the translation matrix equation. This routine fully initializes the Transform that is returned, also initializing the matrix that represents the inverse of the translation.

<<Transform Method Definitions>>= 
Transform Translate(const Vector3f &delta) { Matrix4x4 m(1, 0, 0, delta.x, 0, 1, 0, delta.y, 0, 0, 1, delta.z, 0, 0, 0, 1); Matrix4x4 minv(1, 0, 0, -delta.x, 0, 1, 0, -delta.y, 0, 0, 1, -delta.z, 0, 0, 0, 1); return Transform(m, minv); }

2.7.4 Scaling

Another basic transformation is the scale transformation, bold upper S left-parenthesis s Subscript x Baseline comma s Subscript y Baseline comma s Subscript z Baseline right-parenthesis . It has the effect of taking a point or vector and multiplying its components by scale factors in x y , and  z : bold upper S left-parenthesis 2 comma 2 comma 1 right-parenthesis left-parenthesis x comma y comma z right-parenthesis equals left-parenthesis 2 x comma 2 y comma z right-parenthesis . It has the following basic properties:

StartLayout 1st Row 1st Column bold upper S left-parenthesis 1 comma 1 comma 1 right-parenthesis 2nd Column equals bold upper I 2nd Row 1st Column bold upper S left-parenthesis x 1 comma y 1 comma z 1 right-parenthesis bold upper S left-parenthesis x 2 comma y 2 comma z 2 right-parenthesis 2nd Column equals bold upper S left-parenthesis x 1 x 2 comma y 1 y 2 comma z 1 z 2 right-parenthesis 3rd Row 1st Column bold upper S Superscript negative 1 Baseline left-parenthesis x comma y comma z right-parenthesis 2nd Column equals bold upper S left-parenthesis StartFraction 1 Over x EndFraction comma StartFraction 1 Over y EndFraction comma StartFraction 1 Over z EndFraction right-parenthesis period EndLayout

We can differentiate between uniform scaling, where all three scale factors have the same value, and nonuniform scaling, where they may have different values. The general scale matrix is

bold upper S left-parenthesis x comma y comma z right-parenthesis equals Start 4 By 4 Matrix 1st Row 1st Column x 2nd Column 0 3rd Column 0 4th Column 0 2nd Row 1st Column 0 2nd Column y 3rd Column 0 4th Column 0 3rd Row 1st Column 0 2nd Column 0 3rd Column z 4th Column 0 4th Row 1st Column 0 2nd Column 0 3rd Column 0 4th Column 1 EndMatrix period

<<Transform Method Definitions>>+=  
Transform Scale(Float x, Float y, Float z) { Matrix4x4 m(x, 0, 0, 0, 0, y, 0, 0, 0, 0, z, 0, 0, 0, 0, 1); Matrix4x4 minv(1/x, 0, 0, 0, 0, 1/y, 0, 0, 0, 0, 1/z, 0, 0, 0, 0, 1); return Transform(m, minv); }

It’s useful to be able to test if a transformation has a scaling term in it; an easy way to do this is to transform the three coordinate axes and see if any of their lengths are appreciably different from one.

<<Transform Public Methods>>+=  
bool HasScale() const { Float la2 = (*this)(Vector3f(1, 0, 0)).LengthSquared(); Float lb2 = (*this)(Vector3f(0, 1, 0)).LengthSquared(); Float lc2 = (*this)(Vector3f(0, 0, 1)).LengthSquared(); #define NOT_ONE(x) ((x) < .999f || (x) > 1.001f) return (NOT_ONE(la2) || NOT_ONE(lb2) || NOT_ONE(lc2)); #undef NOT_ONE }

2.7.5 x , y , and z Axis Rotations

Another useful type of transformation is the rotation transformation, bold upper R . In general, we can define an arbitrary axis from the origin in any direction and then rotate around that axis by a given angle. The most common rotations of this type are around the x , y , and z coordinate axes. We will write these rotations as bold upper R Subscript x Baseline left-parenthesis theta right-parenthesis , bold upper R Subscript y Baseline left-parenthesis theta right-parenthesis , and so on. The rotation around an arbitrary axis left-parenthesis x comma y comma z right-parenthesis is denoted by bold upper R Subscript left-parenthesis x comma y comma z right-parenthesis Baseline left-parenthesis theta right-parenthesis .

Rotations also have some basic properties:

StartLayout 1st Row 1st Column bold upper R Subscript a Baseline left-parenthesis 0 right-parenthesis 2nd Column equals bold upper I 2nd Row 1st Column bold upper R Subscript a Baseline left-parenthesis theta 1 right-parenthesis bold upper R Subscript a Baseline left-parenthesis theta 2 right-parenthesis 2nd Column equals bold upper R Subscript a Baseline left-parenthesis theta 1 plus theta 2 right-parenthesis 3rd Row 1st Column bold upper R Subscript a Baseline left-parenthesis theta 1 right-parenthesis bold upper R Subscript a Baseline left-parenthesis theta 2 right-parenthesis 2nd Column equals bold upper R Subscript a Baseline left-parenthesis theta 2 right-parenthesis bold upper R Subscript a Baseline left-parenthesis theta 1 right-parenthesis 4th Row 1st Column bold upper R Subscript a Superscript negative 1 Baseline left-parenthesis theta right-parenthesis 2nd Column equals bold upper R Subscript a Baseline left-parenthesis negative theta right-parenthesis equals bold upper R Subscript a Superscript upper T Baseline left-parenthesis theta right-parenthesis comma EndLayout

where bold upper R Superscript upper T is the matrix transpose of bold upper R . This last property, that the inverse of bold upper R is equal to its transpose, stems from the fact that bold upper R is an orthogonal matrix; its first three columns (or rows) are all normalized and orthogonal to each other. Fortunately, the transpose is much easier to compute than a full matrix inverse.

For a left-handed coordinate system, the matrix for clockwise rotation around the x axis is

bold upper R Subscript x Baseline left-parenthesis theta right-parenthesis equals Start 4 By 4 Matrix 1st Row 1st Column 1 2nd Column 0 3rd Column 0 4th Column 0 2nd Row 1st Column 0 2nd Column cosine theta 3rd Column minus sine theta 4th Column 0 3rd Row 1st Column 0 2nd Column sine theta 3rd Column cosine theta 4th Column 0 4th Row 1st Column 0 2nd Column 0 3rd Column 0 4th Column 1 EndMatrix period

Figure 2.11 gives an intuition for how this matrix works.

Figure 2.11: Clockwise rotation by an angle theta about the  x axis leaves the  x coordinate unchanged. The  y and  z axes are mapped to the vectors given by the dashed lines; y and z coordinates move accordingly.

It’s easy to see that the matrix leaves the x axis unchanged:

bold upper R Subscript x Baseline left-parenthesis theta right-parenthesis left-bracket 1 0 0 0 right-bracket Superscript upper T Baseline equals left-bracket 1 0 0 0 right-bracket Superscript upper T Baseline period

It maps the y axis left-parenthesis 0 comma 1 comma 0 right-parenthesis to left-parenthesis 0 comma cosine theta comma sine theta right-parenthesis and the z axis to left-parenthesis 0 comma minus sine theta comma cosine theta right-parenthesis . The y and z axes remain in the same plane, perpendicular to the x axis, but are rotated by the given angle. An arbitrary point in space is similarly rotated about the x axis by this transformation while staying in the same y z plane as it was originally.

The implementation of the RotateX() function is straightforward.

<<Transform Method Definitions>>+=  
Transform RotateX(Float theta) { Float sinTheta = std::sin(Radians(theta)); Float cosTheta = std::cos(Radians(theta)); Matrix4x4 m(1, 0, 0, 0, 0, cosTheta, -sinTheta, 0, 0, sinTheta, cosTheta, 0, 0, 0, 0, 1); return Transform(m, Transpose(m)); }

Similarly, for clockwise rotation around y and z , we have

bold upper R Subscript y Baseline left-parenthesis theta right-parenthesis equals Start 4 By 4 Matrix 1st Row 1st Column cosine theta 2nd Column 0 3rd Column sine theta 4th Column 0 2nd Row 1st Column 0 2nd Column 1 3rd Column 0 4th Column 0 3rd Row 1st Column minus sine theta 2nd Column 0 3rd Column cosine theta 4th Column 0 4th Row 1st Column 0 2nd Column 0 3rd Column 0 4th Column 1 EndMatrix bold upper R Subscript z Baseline left-parenthesis theta right-parenthesis equals Start 4 By 4 Matrix 1st Row 1st Column cosine theta 2nd Column minus sine theta 3rd Column 0 4th Column 0 2nd Row 1st Column sine theta 2nd Column cosine theta 3rd Column 0 4th Column 0 3rd Row 1st Column 0 2nd Column 0 3rd Column 1 4th Column 0 4th Row 1st Column 0 2nd Column 0 3rd Column 0 4th Column 1 EndMatrix period

The implementations of RotateY() and RotateZ() follow directly and are not included here.

2.7.6 Rotation around an Arbitrary Axis

We also provide a routine to compute the transformation that represents rotation around an arbitrary axis. The usual derivation of this matrix is based on computing rotations that map the given axis to a fixed axis (e.g., z ), performing the rotation there, and then rotating the fixed axis back to the original axis. A more elegant derivation can be constructed with vector algebra.

Consider a normalized direction vector bold a that gives the axis to rotate around by angle theta , and a vector bold v to be rotated (Figure 2.12).

Figure 2.12: A vector bold v can be rotated around an arbitrary axis bold a by constructing a coordinate system left-parenthesis normal p Subscript Baseline comma bold v bold 1 comma bold v bold 2 right-parenthesis in the plane perpendicular to the axis that passes through bold v ’s end point and rotating the vectors bold v bold 1 and bold v bold 2 about normal p Subscript . Applying this rotation to the axes of the coordinate system left-parenthesis 1 comma 0 comma 0 right-parenthesis , left-parenthesis 0 comma 1 comma 0 right-parenthesis , and left-parenthesis 0 comma 0 comma 1 right-parenthesis gives the general rotation matrix for this rotation.

First, we can compute the vector bold v Subscript bold c along the axis bold a that is in the plane through the end point of bold v and is parallel to bold a . Assuming bold v and bold a form an angle alpha , we have

bold v Subscript bold c Baseline equals bold a double-vertical-bar bold v double-vertical-bar cosine alpha equals bold a left-parenthesis bold v dot bold a right-parenthesis period

We now compute a pair of basis vectors bold v bold 1 and bold v bold 2 in this plane. Trivially, one of them is

bold v bold 1 equals bold v minus bold v Subscript bold c Baseline comma

and the other can be computed with a cross product

bold v bold 2 equals left-parenthesis bold v bold 1 times bold a right-parenthesis period

Because bold a is normalized, bold v bold 1 and bold v bold 2 have the same length, equal to the length of the vector between bold v and bold v Subscript bold c . To now compute the rotation by an angle theta about bold v Subscript bold c in the plane of rotation, the rotation formulas earlier give us

bold v prime equals bold v Subscript bold c Baseline plus bold v bold 1 cosine theta plus bold v bold 2 sine theta period

To convert this to a rotation matrix, we apply this formula to the basis vectors left-parenthesis 1 comma 0 comma 0 right-parenthesis , left-parenthesis 0 comma 1 comma 0 right-parenthesis , and left-parenthesis 0 comma 0 comma 1 right-parenthesis to get the values of the rows of the matrix. The result of all this is encapsulated in the following function. As with the other rotation matrices, the inverse is equal to the transpose.

<<Transform Method Definitions>>+=  
Transform Rotate(Float theta, const Vector3f &axis) { Vector3f a = Normalize(axis); Float sinTheta = std::sin(Radians(theta)); Float cosTheta = std::cos(Radians(theta)); Matrix4x4 m; <<Compute rotation of first basis vector>> 
m.m[0][0] = a.x * a.x + (1 - a.x * a.x) * cosTheta; m.m[0][1] = a.x * a.y * (1 - cosTheta) - a.z * sinTheta; m.m[0][2] = a.x * a.z * (1 - cosTheta) + a.y * sinTheta; m.m[0][3] = 0;
<<Compute rotations of second and third basis vectors>> 
m.m[1][0] = a.x * a.y * (1 - cosTheta) + a.z * sinTheta; m.m[1][1] = a.y * a.y + (1 - a.y * a.y) * cosTheta; m.m[1][2] = a.y * a.z * (1 - cosTheta) - a.x * sinTheta; m.m[1][3] = 0; m.m[2][0] = a.x * a.z * (1 - cosTheta) - a.y * sinTheta; m.m[2][1] = a.y * a.z * (1 - cosTheta) + a.x * sinTheta; m.m[2][2] = a.z * a.z + (1 - a.z * a.z) * cosTheta; m.m[2][3] = 0;
return Transform(m, Transpose(m)); }

<<Compute rotation of first basis vector>>= 
m.m[0][0] = a.x * a.x + (1 - a.x * a.x) * cosTheta; m.m[0][1] = a.x * a.y * (1 - cosTheta) - a.z * sinTheta; m.m[0][2] = a.x * a.z * (1 - cosTheta) + a.y * sinTheta; m.m[0][3] = 0;

The code for the other two basis vectors follows similarly and isn’t included here.

2.7.7 The Look-At Transformation

The look-at transformation is particularly useful for placing a camera in the scene. The caller specifies the desired position of the camera, a point the camera is looking at, and an “up” vector that orients the camera along the viewing direction implied by the first two parameters. All of these values are given in world space coordinates. The look-at construction then gives a transformation between camera space and world space (Figure 2.13).

Figure 2.13: Given a camera position, the position being looked at from the camera, and an “up” direction, the look-at transformation describes a transformation from a left-handed viewing coordinate system where the camera is at the origin looking down the plus z axis, and the plus y axis is along the up direction.

In order to find the entries of the look-at transformation matrix, we use principles described earlier in this section: the columns of a transformation matrix give the effect of the transformation on the basis of a coordinate system.

<<Transform Method Definitions>>+=  
Transform LookAt(const Point3f &pos, const Point3f &look, const Vector3f &up) { Matrix4x4 cameraToWorld; <<Initialize fourth column of viewing matrix>> 
cameraToWorld.m[0][3] = pos.x; cameraToWorld.m[1][3] = pos.y; cameraToWorld.m[2][3] = pos.z; cameraToWorld.m[3][3] = 1;
<<Initialize first three columns of viewing matrix>> 
Vector3f dir = Normalize(look - pos); Vector3f right = Normalize(Cross(Normalize(up), dir)); Vector3f newUp = Cross(dir, right); cameraToWorld.m[0][0] = right.x; cameraToWorld.m[1][0] = right.y; cameraToWorld.m[2][0] = right.z; cameraToWorld.m[3][0] = 0.; cameraToWorld.m[0][1] = newUp.x; cameraToWorld.m[1][1] = newUp.y; cameraToWorld.m[2][1] = newUp.z; cameraToWorld.m[3][1] = 0.; cameraToWorld.m[0][2] = dir.x; cameraToWorld.m[1][2] = dir.y; cameraToWorld.m[2][2] = dir.z; cameraToWorld.m[3][2] = 0.;
return Transform(Inverse(cameraToWorld), cameraToWorld); }

The easiest column is the fourth one, which gives the point that the camera space origin, left-bracket 0 0 0 1 right-bracket Superscript upper T Baseline comma maps to in world space. This is clearly just the camera position, supplied by the user.

<<Initialize fourth column of viewing matrix>>= 
cameraToWorld.m[0][3] = pos.x; cameraToWorld.m[1][3] = pos.y; cameraToWorld.m[2][3] = pos.z; cameraToWorld.m[3][3] = 1;

The other three columns aren’t much more difficult. First, LookAt() computes the normalized direction vector from the camera location to the look-at point; this gives the vector coordinates that the z axis should map to and, thus, the third column of the matrix. (In a left-handed coordinate system, camera space is defined with the viewing direction down the plus z axis.) The first column, giving the world space direction that the plus x axis in camera space maps to, is found by taking the cross product of the user-supplied “up” vector with the recently computed viewing direction vector. Finally, the “up” vector is recomputed by taking the cross product of the viewing direction vector with the transformed  x axis vector, thus ensuring that the  y and  z axes are perpendicular and we have an orthonormal viewing coordinate system.

<<Initialize first three columns of viewing matrix>>= 
Vector3f dir = Normalize(look - pos); Vector3f right = Normalize(Cross(Normalize(up), dir)); Vector3f newUp = Cross(dir, right); cameraToWorld.m[0][0] = right.x; cameraToWorld.m[1][0] = right.y; cameraToWorld.m[2][0] = right.z; cameraToWorld.m[3][0] = 0.; cameraToWorld.m[0][1] = newUp.x; cameraToWorld.m[1][1] = newUp.y; cameraToWorld.m[2][1] = newUp.z; cameraToWorld.m[3][1] = 0.; cameraToWorld.m[0][2] = dir.x; cameraToWorld.m[1][2] = dir.y; cameraToWorld.m[2][2] = dir.z; cameraToWorld.m[3][2] = 0.;