3.10 应用变换(Applying Transformations)
我们现在可以定义执行适当矩阵乘法来变换点和向量的例程。我们将重载函数应用运算符来描述这些变换;这使我们能够像下面这样编写代码:
Point3f p = ...;
Transform T = ...;
Point3f pNew = T(p);
3.10.1 点(Points)
点变换例程接受一个点 \( (x,y,z) \) 并隐式表示为齐次列向量 \( [x\ y\ z\ 1]^T \) 。然后,通过用变换矩阵左乘(premultiply)这个向量来变换该点。最后,将结果除以 \( w \) 来转换回非齐次点表示。为了提高效率,当权重 \( w=1 \) 时,该方法跳过了对齐次权重 \( w \) 的除法,这对于将在 pbrt 中使用的大多数变换是常见的——只有在第 5 章中定义的投影变换才需要此除法。
/** Transform 内联方法 */
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;
}
Transform 类还为其变换的每种类型提供了相应的 ApplyInverse() 方法。针对 Point3 的方法对给定点应用其逆变换。调用此方法比先调用 Transform::Inverse() 然后再调用其 operator() 更简洁且通常更高效。
/** Transform 公有方法 */
template <typename T>
Point3<T> ApplyInverse(Point3<T> p) const;
所有后续可以变换的类型也都有 ApplyInverse() 方法,尽管我们不会在书中包含它们。
3.10.2 向量(Vectors)
向量的变换可以以类似的方式计算。然而,矩阵与列向量的乘法被简化,因为隐式齐次 \( w \) 坐标为零。
/** Transform 内联方法 */
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)
![]()
图 3.29:变换表面法线。(a) 原始的圆,法线在某一点由箭头表示。(b) 当将圆在 \( y \) 方向上缩放为一半高度时,简单地将法线视为一个方向并以相同方式缩放,会导致法线不再垂直于表面。(c) 正确变换的法线。
法线的变换方式与向量不同,如图 3.29 所示。尽管表面的切向量以简单的方式变换,法线却需要特别处理。由于法向量 \( \mathbf{n} \) 和表面上的任何切向量 \( \mathbf{t} \) 在构造上是正交的,我们知道
\[
\mathbf{n}\cdot\mathbf{t} = \mathbf{n}^T\mathbf{t} = 0
\]
当我们通过某个矩阵 \( \mathbf{M} \) 变换表面上的一个点时,变换后点的切向量 \( \mathbf{t}' \) 为 \( \mathbf{M}\mathbf{t} \) 。变换后的法向量 \( \mathbf{n}' \) 应该等于通过某个 \( 4\times4 \) 矩阵 \( \mathbf{S} \) 变换后的 \( \mathbf{S}\mathbf{n} \) 。为了保持正交性要求,我们必须有
\[
\begin{align}
0 &= (\mathbf{n}')^T\mathbf{t}' \\
&= (\mathbf{S}\mathbf{n})^T\mathbf{M}\mathbf{t} \\
&= (\mathbf{n})^T\mathbf{S}^T\mathbf{M}\mathbf{t} \\
\end{align}
\]
该条件在 \( \mathbf{S}^T\mathbf{M} = \mathbf{I} \),即单位矩阵的情况下成立。因此,\( \mathbf{S}^T = \mathbf{M}^{-1} \),所以 \( \mathbf{S} = (\mathbf{M}^{-1})^T \),我们看到法线必须通过变换矩阵的逆的转置进行变换。这个细节是 Transform 包含其逆的原因之一。
请注意,这种方法在变换法线时并不显式计算逆矩阵的转置。它只是以不同的顺序索引逆矩阵(相比于变换 Vector3f 的代码)。
/** Transform 内联方法 */
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)
变换光线在概念上是非常简单的:只需变换组成它的起点和方向,并复制其他数据成员。( pbrt 也提供了一个类似的方法来变换 RayDifferential 。)
在 pbrt 中用于管理浮点数舍入误差的方法引入了一些细节,需要对变换后的光线起点进行小幅调整。<<将光线起点偏移到误差界限的边缘并计算 tMax>>片段处理了这些细节;该片段在第 6.8.6 节中定义,其中讨论了舍入误差及 pbrt 处理该误差的机制。
/** Transform 内联方法 */
Ray Transform::operator()(const Ray &r, Float *tMax) const {
Point3fi o = (*this)(Point3fi(r.o));
Vector3f d = (*this)(r.d);
/** 将光线起点偏移到误差界限的边缘并计算 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)
变换轴对齐包围盒(AABB)的最简单方法是变换其所有八个角顶点,然后计算一个包含这些点的新的边界框。下面展示了这种方法的实现;本章有一个课后练习要求你实现一种更高效地进行此计算的技术。
/** Transform 方法定义 */
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)
在定义了如何构建表示单个变换类型的矩阵之后,我们现在可以考虑由一系列单个变换所产生的总变换。我们终于将看到用矩阵表示变换的真正价值。
考虑一系列变换 \( \mathbf{A}\mathbf{B}\mathbf{C} \) 。我们希望计算一个新的变换 \( \mathbf{T} \) ,使得应用 \( \mathbf{T} \) 的结果与反向应用 \( \mathbf{A} \)、 \( \mathbf{B} \) 和 \( \mathbf{C} \) 的结果相同;即 \( \mathbf{A}(\mathbf{B}(\mathbf{C}(\text{p}))) = \mathbf{T}(\text{p}) \) 。这样的变换 \( \mathbf{T} \) 可以通过将变换 \( \mathbf{A} \)、 \( \mathbf{B} \) 和 \( \mathbf{C} \) 的矩阵相乘来计算。在 pbrt 中,我们可以写:
Transform T = A * B * C;
然后我们可以像往常一样将 T 应用于 Point3f 的 p , Point3f pp = T(p) ,而不是依次应用每个变换: Point3f pp = A(B(C(p))) 。
我们在 Transform 类中重载 C++ * 运算符,以计算用另一个变换 t2 后乘(postmultiply) 该变换所得到的新变换。在矩阵乘法中,结果矩阵的地 \( (i,j) \) 个元素是第一个矩阵的第 \( i \) 行与第二个矩阵的第 \( j \) 列的内积。
结果变换的逆等于 t2.mInv * mInv 的乘积。这是矩阵恒等式的结果
\[
(\mathbf{AB})^{-1} = \mathbf{B}^{-1}\mathbf{A}^{-1}
\]
/** Transform 方法定义 */
<<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)
某些类型的变换会将左手坐标系变成右手坐标系,反之亦然。一些例程需要知道源坐标系的手性是否与目标坐标系不同。特别是,想要确保表面法线始终指向表面“外部”的例程,在手性发生变化时,可能需要在变换后翻转法线的方向。
幸运的是,判断变换是否改变了手性是很简单的:只有当变换的左上角 \( 3\times3 \) 子矩阵的行列式为负时,才会发生这种情况。
/** Transform 方法定义 */
<<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)
有时定义一个旋转使得坐标系统中的三个规范正交向量(Orthonormal vectors)与 \( x \)、 \( y \) 和 \( z \) 轴对齐是有用的。将这种变换应用于该坐标系中的方向向量可以简化后续计算。例如,在 pbrt 中,BSDF 评估是在一个表面法线与 \( z \) 轴对齐的坐标系中进行的。除此之外,这使得可以高效地使用诸如第 3.8.3 节中介绍的 CosTheta() 函数来计算三角函数。
Frame 类高效地表示并执行此类变换,避免了 Transform 类的完全通用性(因此也避免了复杂性)。它只需要存储一个 \( 3\times 3 \) 矩阵,存储逆矩阵是没有必要的,因为在规范正交基向量(orthonormal basis vectors)的情况下,逆矩阵仅仅是该矩阵的转置。
/** Frame 定义 */
class Frame {
public:
/** Frame 公有方法 */
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 公有成员 */
Vector3f x, y, z;
};
给定三个规范正交向量 \( \mathbf{x} \)、 \( \mathbf{y} \) 和 \( \mathbf{z} \) ,将向量变换到其空间的矩阵 \( \mathbf{F} \) 是
\[ \mathbf{F} = \left( \begin{matrix} {\mathbf{x}_x} & {\mathbf{x}_y} & {\mathbf{x}_z} \\ {\mathbf{y}_x} & {\mathbf{y}_y} & {\mathbf{y}_z} \\ {\mathbf{z}_x} & {\mathbf{z}_y} & {\mathbf{z}_z} \\ \end{matrix} \right) = \left( \begin{matrix} {x} \\ {y} \\ {z} \end{matrix} \right) \]
Frame 使用三个 Vector3f 来存储此矩阵。
/** Frame 公有成员 */
Vector3f x, y, z;
三个基向量可以被明确指定;在调试构建中,构造函数中的 DCHECK() 确保提供的向量是规范正交的(orthonormal)。
/** Frame 公有方法 */
Frame() : x(1, 0, 0), y(0, 1, 0), z(0, 0, 1) {}
Frame(Vector3f x, Vector3f y, Vector3f z);
Frame 还提供了便利的方法,可以仅使用两个基向量来构造一个框架,通过叉积计算第三个向量。
/** Frame 公有方法 */
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));
}
也可以仅提供 \( z \) 轴向量,在这种情况下,其他向量被任意设置。
/** Frame 公有方法 */
static Frame FromZ(Vector3f z) {
Vector3f x, y;
CoordinateSystem(z, &x, &y);
return Frame(x, y, z);
}
其他多种未在此列出的函数允许使用法向量指定一个框架,和仅通过 \( x \) 或 \( y \) 基向量来指定它。
将向量变换至框架的坐标空间是通过 \( \mathbf{F} \) 矩阵完成的。由于 Vector3f 被用来存储其行,因此矩阵-向量的乘积可以表示为三个点积。
/** Frame 公有方法 */
Vector3f ToLocal(Vector3f v) const {
return Vector3f(Dot(v, x), Dot(v, y), Dot(v, z));
}
还提供了一种用于法向量的 ToLocal() 方法。在这种情况下,我们不需要计算 \( \mathbf{F} \) 的逆转置来变换法线(回顾第 3.10.3 节关于变换法线的讨论)。因为 \( \mathbf{F} \) 是一个规范正交矩阵(其行和列相互正交且都是单位长度),它的逆等于它的转置,所以它已经是它自己的逆转置。
/** Frame 公有方法 */
Normal3f ToLocal(Normal3f n) const {
return Normal3f(Dot(n, x), Dot(n, y), Dot(n, z));
}
将向量从框架的局部空间变换出来的方法是转置 \( \mathbf{F} \) 以找到其逆,然后再与向量相乘。在这种情况下,计算的结果可以表示为矩阵列的三个缩放版本的总和。与之前一样,表面法线作为常规向量进行变换。(该方法不在此处包含。)
/** Frame 公有方法 */
Vector3f FromLocal(Vector3f v) const {
return v.x * x + v.y * y + v.z * z;
}
为了方便,Transform 还有一个接受 Frame 的构造函数。其简单的实现不在此处包含。
/** Transform 公有方法 */
explicit Transform(const Frame &frame);