8707 字
44 分钟
计算机图形学笔记(一):数学基础与理论根基
NOTE

说实话,刚开始学图形学的时候,看到那些密密麻麻的数学公式真的头大。这一部分主要整理了线性代数、几何变换、投影几何和微积分在图形学中的应用,是后续所有章节的地基。

目录#

  1. 线性代数基础 — 向量、矩阵、齐次坐标系统
  2. 几何变换数学原理 — 平移、旋转、缩放的数学本质
  3. 投影几何学 — 透视投影与视图变换的完整推导
  4. 微积分在图形学中的应用 — 曲线曲率与渲染积分

一、线性代数基础#

1.1 向量的数学定义与几何意义#

1.1.1 向量的基本概念#

数学定义:向量是具有大小和方向的量,在 nn 维欧几里得空间 Rn\mathbb{R}^n 中可以表示为:

v⃗=(v1v2⋮vn)∈Rn\vec{v} = \begin{pmatrix} v_1 \\ v_2 \\ \vdots \\ v_n \end{pmatrix} \in \mathbb{R}^n

向量的模长(欧几里得范数):

∥v⃗∥=v12+v22+⋯+vn2=∑i=1nvi2\|\vec{v}\| = \sqrt{v_1^2 + v_2^2 + \cdots + v_n^2} = \sqrt{\sum_{i=1}^{n} v_i^2}

单位向量:

v^=v⃗∥v⃗∥,∥v^∥=1\hat{v} = \frac{\vec{v}}{\|\vec{v}\|}, \quad \|\hat{v}\| = 1

几何意义与性质:

  • 位置表示:向量可以看作从原点指向某点的有向线段
  • 方向性:向量的方向由其各分量的比值决定,与起点无关
  • 平移不变性:向量表示的是相对位移,不依赖于坐标系原点
  • 线性性:向量空间满足加法和数乘的线性性质

在计算机图形学中的核心应用:

  • 位置向量:表示 3D 空间中顶点的坐标 p=(x,y,z)Tp = (x, y, z)^T
  • 方向向量:表示光线方向、法向量、视线方向等
  • 位移向量:表示物体的平移变换
  • 颜色向量:RGB / RGBA 颜色空间的表示
  • 纹理坐标:UV 坐标的二维向量表示

1.1.2 向量运算的数学原理#

向量加法(Vector Addition)#

数学定义:对于 nn 维向量 u⃗\vec{u} 和 v⃗\vec{v},其和定义为:

u⃗+v⃗=(u1+v1u2+v2⋮un+vn)\vec{u} + \vec{v} = \begin{pmatrix} u_1 + v_1 \\ u_2 + v_2 \\ \vdots \\ u_n + v_n \end{pmatrix}

几何解释:

  • 平行四边形法则:u⃗+v⃗\vec{u} + \vec{v} 是以 u⃗\vec{u} 和 v⃗\vec{v} 为邻边的平行四边形的对角线
  • 三角形法则:将 v⃗\vec{v} 的起点放在 u⃗\vec{u} 的终点,连接 u⃗\vec{u} 的起点和 v⃗\vec{v} 的终点

代数性质:

  • 交换律:u⃗+v⃗=v⃗+u⃗\vec{u} + \vec{v} = \vec{v} + \vec{u}
  • 结合律:(u⃗+v⃗)+w⃗=u⃗+(v⃗+w⃗)(\vec{u} + \vec{v}) + \vec{w} = \vec{u} + (\vec{v} + \vec{w})
  • 零元素:u⃗+0⃗=u⃗\vec{u} + \vec{0} = \vec{u}
  • 逆元素:u⃗+(−u⃗)=0⃗\vec{u} + (-\vec{u}) = \vec{0}

GAMES101 项目实现:

// Eigen 库中的向量加法
Eigen::Vector3f u(1.0f, 2.0f, 3.0f);
Eigen::Vector3f v(4.0f, 5.0f, 6.0f);
Eigen::Vector3f result = u + v; // (5, 7, 9)
// 在光栅化中的应用:顶点位置变换
Eigen::Vector3f vertex_position = base_position + displacement;

向量数乘(Scalar Multiplication)#

数学定义:对于标量 k∈Rk \in \mathbb{R} 和向量 v⃗∈Rn\vec{v} \in \mathbb{R}^n:

kv⃗=(kv1kv2⋮kvn)k\vec{v} = \begin{pmatrix} kv_1 \\ kv_2 \\ \vdots \\ kv_n \end{pmatrix}

几何效果分析:

  • k>1k > 1:向量伸长 kk 倍,方向不变
  • 0<k<10 < k < 1:向量缩短为原长度的 kk 倍,方向不变
  • k=0k = 0:得到零向量
  • k<0k < 0:向量反向并缩放 ∣k∣|k| 倍

重要应用:

  • 单位向量计算:v^=1∥v⃗∥v⃗\hat{v} = \frac{1}{\|\vec{v}\|}\vec{v}
  • 缩放变换:在几何变换中实现物体的放大缩小
  • 插值计算:线性插值 p⃗(t)=(1−t)p0⃗+tp1⃗\vec{p}(t) = (1-t)\vec{p_0} + t\vec{p_1}

点积(内积 / 数量积)#

数学定义:对于向量 u⃗,v⃗∈Rn\vec{u}, \vec{v} \in \mathbb{R}^n,点积定义为:

u⃗⋅v⃗=∑i=1nuivi=u1v1+u2v2+⋯+unvn\vec{u} \cdot \vec{v} = \sum_{i=1}^{n} u_i v_i = u_1v_1 + u_2v_2 + \cdots + u_nv_n

几何形式:

u⃗⋅v⃗=∥u⃗∥∥v⃗∥cos⁡θ\vec{u} \cdot \vec{v} = \|\vec{u}\| \|\vec{v}\| \cos \theta

其中 θ\theta 是两向量间的夹角 (0≤θ≤π)(0 \leq \theta \leq \pi)。

几何意义深度解析:

  • 投影解释:u⃗⋅v⃗=∥u⃗∥⋅projv⃗u⃗\vec{u} \cdot \vec{v} = \|\vec{u}\| \cdot \text{proj}_{\vec{v}}\vec{u}
  • 标量结果:点积的结果是标量,不是向量
  • 角度计算:cos⁡θ=u⃗⋅v⃗∥u⃗∥∥v⃗∥\cos\theta = \frac{\vec{u} \cdot \vec{v}}{\|\vec{u}\| \|\vec{v}\|}

重要代数性质:

  • 交换律:u⃗⋅v⃗=v⃗⋅u⃗\vec{u} \cdot \vec{v} = \vec{v} \cdot \vec{u}
  • 分配律:u⃗⋅(v⃗+w⃗)=u⃗⋅v⃗+u⃗⋅w⃗\vec{u} \cdot (\vec{v} + \vec{w}) = \vec{u} \cdot \vec{v} + \vec{u} \cdot \vec{w}
  • 结合律:(ku⃗)⋅v⃗=k(u⃗⋅v⃗)(k\vec{u}) \cdot \vec{v} = k(\vec{u} \cdot \vec{v})
  • 正定性:u⃗⋅u⃗=∥u⃗∥2≥0\vec{u} \cdot \vec{u} = \|\vec{u}\|^2 \geq 0,等号成立当且仅当 u⃗=0⃗\vec{u} = \vec{0}

几何判断准则:

  • u⃗⋅v⃗=0  ⟺  u⃗⊥v⃗\vec{u} \cdot \vec{v} = 0 \iff \vec{u} \perp \vec{v}(垂直判断)
  • u⃗⋅v⃗>0  ⟺  θ<90°\vec{u} \cdot \vec{v} > 0 \iff \theta < 90°(锐角)
  • u⃗⋅v⃗<0  ⟺  θ>90°\vec{u} \cdot \vec{v} < 0 \iff \theta > 90°(钝角)

GAMES101 项目中的关键应用:

// 1. 计算两向量夹角
float dot_product = u.dot(v);
float angle = std::acos(std::clamp(dot_product / (u.norm() * v.norm()), -1.0f, 1.0f));
// 2. 光照计算中的 Lambert 余弦定律
float cos_theta = std::max(0.0f, normal.dot(light_direction));
Vector3f diffuse_color = albedo * light_color * cos_theta;
// 3. 视锥体裁剪中的平面测试
float distance_to_plane = point.dot(plane_normal) + plane_d;
bool is_inside = distance_to_plane >= 0;
// 4. 背面剔除
Vector3f view_direction = camera_position - vertex_position;
bool is_front_facing = face_normal.dot(view_direction) > 0;

叉积(外积 / 向量积)#

数学定义:对于三维向量 u⃗,v⃗∈R3\vec{u}, \vec{v} \in \mathbb{R}^3,叉积定义为:

u⃗×v⃗=(u2v3−u3v2u3v1−u1v3u1v2−u2v1)\vec{u} \times \vec{v} = \begin{pmatrix} u_2v_3 - u_3v_2 \\ u_3v_1 - u_1v_3 \\ u_1v_2 - u_2v_1 \end{pmatrix}

行列式表示:叉积可以表示为形式行列式:

u⃗×v⃗=∣i⃗j⃗k⃗u1u2u3v1v2v3∣\vec{u} \times \vec{v} = \begin{vmatrix} \vec{i} & \vec{j} & \vec{k} \\ u_1 & u_2 & u_3 \\ v_1 & v_2 & v_3 \end{vmatrix}

几何意义深度解析:

  • 方向:遵循右手定则,垂直于 u⃗\vec{u} 和 v⃗\vec{v} 构成的平面
  • 大小:∥u⃗×v⃗∥=∥u⃗∥∥v⃗∥sin⁡θ\|\vec{u} \times \vec{v}\| = \|\vec{u}\| \|\vec{v}\| \sin\theta
  • 面积解释:∥u⃗×v⃗∥\|\vec{u} \times \vec{v}\| 等于以 u⃗\vec{u} 和 v⃗\vec{v} 为邻边的平行四边形面积

重要代数性质:

  • 反交换律:u⃗×v⃗=−v⃗×u⃗\vec{u} \times \vec{v} = -\vec{v} \times \vec{u}
  • 分配律:u⃗×(v⃗+w⃗)=u⃗×v⃗+u⃗×w⃗\vec{u} \times (\vec{v} + \vec{w}) = \vec{u} \times \vec{v} + \vec{u} \times \vec{w}
  • 标量结合律:(ku⃗)×v⃗=k(u⃗×v⃗)=u⃗×(kv⃗)(k\vec{u}) \times \vec{v} = k(\vec{u} \times \vec{v}) = \vec{u} \times (k\vec{v})
  • 平行判断:u⃗×v⃗=0⃗  ⟺  u⃗∥v⃗\vec{u} \times \vec{v} = \vec{0} \iff \vec{u} \parallel \vec{v}
  • 垂直性:(u⃗×v⃗)⋅u⃗=0(\vec{u} \times \vec{v}) \cdot \vec{u} = 0 且 (u⃗×v⃗)⋅v⃗=0(\vec{u} \times \vec{v}) \cdot \vec{v} = 0

标量三重积:

u⃗⋅(v⃗×w⃗)=∣u1u2u3v1v2v3w1w2w3∣\vec{u} \cdot (\vec{v} \times \vec{w}) = \begin{vmatrix} u_1 & u_2 & u_3 \\ v_1 & v_2 & v_3 \\ w_1 & w_2 & w_3 \end{vmatrix}

表示以三个向量为邻边的平行六面体的有向体积。

GAMES101 项目中的核心应用:

// 1. 计算三角形法向量(Assignment 2/3 中的关键操作)
Eigen::Vector3f edge1 = vertex2 - vertex1;
Eigen::Vector3f edge2 = vertex3 - vertex1;
Eigen::Vector3f normal = edge1.cross(edge2).normalized();
// 2. 判断点在三角形内部(重心坐标计算的基础)
Vector3f v0 = C - A, v1 = B - A, v2 = P - A;
Vector3f cross1 = v0.cross(v1); // 三角形面积向量
Vector3f cross2 = v0.cross(v2); // 子三角形面积向量
float area_ratio = cross2.dot(cross1) / cross1.squaredNorm();
// 3. 构建坐标系(相机变换中的应用)
Vector3f forward = (target - eye).normalized();
Vector3f right = forward.cross(up).normalized();
Vector3f camera_up = right.cross(forward);
// 4. 计算三角形面积
float triangle_area = 0.5f * edge1.cross(edge2).norm();

1.2 矩阵理论基础#

1.2.1 矩阵的定义与基本运算#

矩阵定义:m×nm \times n 矩阵是由 mm 行 nn 列实数排列成的矩形阵列:

A=(aij)m×nA = (a_{ij})_{m \times n}

其中 aija_{ij} 表示位于第 ii 行第 jj 列的元素。

矩阵乘法的严格定义:对于矩阵 A∈Rm×pA \in \mathbb{R}^{m \times p} 和 B∈Rp×nB \in \mathbb{R}^{p \times n},乘积 C=AB∈Rm×nC = AB \in \mathbb{R}^{m \times n} 的元素为:

cij=∑k=1paikbkj,i=1,2,…,m; j=1,2,…,nc_{ij} = \sum_{k=1}^{p} a_{ik} b_{kj}, \quad i = 1,2,\ldots,m;\ j = 1,2,\ldots,n

矩阵乘法的几何意义:

  • 线性变换复合:(AB)x⃗=A(Bx⃗)(AB)\vec{x} = A(B\vec{x}) 表示先应用变换 BB,再应用变换 AA
  • 基向量变换:矩阵 AA 的第 jj 列是标准基向量 ej⃗\vec{e_j} 经过变换 AA 后的结果
  • 坐标系变换:从一个坐标系到另一个坐标系的映射

矩阵乘法的重要性质:

  • 结合律:(AB)C=A(BC)(AB)C = A(BC)
  • 分配律:A(B+C)=AB+ACA(B + C) = AB + AC,(A+B)C=AC+BC(A + B)C = AC + BC
  • 非交换性:一般情况下 AB≠BAAB \neq BA
  • 与标量乘法的关系:(kA)B=k(AB)=A(kB)(kA)B = k(AB) = A(kB)

1.2.2 特殊矩阵类型#

单位矩阵(Identity Matrix)#

定义:n×nn \times n 单位矩阵 InI_n 定义为对角线元素为 1、其他元素为 0 的矩阵:

(In)ij=δij={1,i=j0,i≠j(I_n)_{ij} = \delta_{ij} = \begin{cases} 1, & i = j \\ 0, & i \neq j \end{cases}

基本性质:

  • 乘法单位元:AI=IA=AAI = IA = A(当维度匹配时)
  • 几何意义:恒等变换,不改变任何向量
  • 特征值:所有特征值都是 1

转置矩阵(Transpose Matrix)#

定义:矩阵 AA 的转置 ATA^T 定义为:(AT)ij=Aji(A^T)_{ij} = A_{ji}

重要性质:

  • 对合性:(AT)T=A(A^T)^T = A
  • 乘积转置:(AB)T=BTAT(AB)^T = B^T A^T
  • 和的转置:(A+B)T=AT+BT(A + B)^T = A^T + B^T
  • 标量乘法:(kA)T=kAT(kA)^T = kA^T

几何意义:

  • 对于旋转矩阵:RT=R−1R^T = R^{-1}(正交矩阵性质)
  • 反映了线性变换的「逆向」操作

逆矩阵(Inverse Matrix)#

定义:对于方阵 A∈Rn×nA \in \mathbb{R}^{n \times n},如果存在矩阵 A−1A^{-1} 使得:

AA−1=A−1A=InAA^{-1} = A^{-1}A = I_n

则称 A−1A^{-1} 为 AA 的逆矩阵。

存在性条件:

  • 行列式非零:det⁡(A)≠0\det(A) \neq 0
  • 满秩条件:rank(A)=n\text{rank}(A) = n
  • AA 的列向量线性无关

计算方法:

  1. 伴随矩阵法:A−1=1det⁡(A)adj(A)A^{-1} = \frac{1}{\det(A)} \text{adj}(A)
  2. 高斯-约旦消元法:[A∣I]→[I∣A−1][A|I] \to [I|A^{-1}]
  3. LU 分解法:适用于大型矩阵

重要性质:

  • (A−1)−1=A(A^{-1})^{-1} = A
  • (AB)−1=B−1A−1(AB)^{-1} = B^{-1}A^{-1}
  • (AT)−1=(A−1)T(A^T)^{-1} = (A^{-1})^T
  • det⁡(A−1)=1det⁡(A)\det(A^{-1}) = \frac{1}{\det(A)}

1.2.3 齐次坐标系统#

齐次坐标的数学基础#

引入动机:

  1. 统一变换表示:将平移、旋转、缩放等变换统一为矩阵乘法
  2. 透视投影简化:用线性代数处理非线性的透视除法
  3. 变换复合:多个变换的复合简化为矩阵乘法
  4. 无穷远点表示:优雅地处理平行线相交于无穷远点

齐次坐标的数学定义:对于 nn 维欧几里得空间中的点,其齐次坐标是 (n+1)(n+1) 维向量。齐次坐标 (x,y,z,w)(x, y, z, w) 对应笛卡尔坐标 (x/w,y/w,z/w)(x/w, y/w, z/w),其中 w≠0w \neq 0。

齐次坐标的分类:

  • 点的表示:(x,y,z,1)(x, y, z, 1) — 第四个分量为 1
  • 向量的表示:(x,y,z,0)(x, y, z, 0) — 第四个分量为 0
  • 无穷远点:(x,y,z,0)(x, y, z, 0) 且 (x,y,z)≠(0,0,0)(x, y, z) \neq (0, 0, 0)

齐次坐标的等价性:所有非零标量倍数表示同一个点:

k(x,y,z,w)∼(x,y,z,w)(k≠0)k(x, y, z, w) \sim (x, y, z, w) \quad (k \neq 0)

齐次坐标的运算规则#

点与向量的区别:

运算公式结果类型
点 + 向量(x1,y1,z1,1)+(x2,y2,z2,0)(x_1, y_1, z_1, 1) + (x_2, y_2, z_2, 0)点
点 − 点(x1,y1,z1,1)−(x2,y2,z2,1)(x_1, y_1, z_1, 1) - (x_2, y_2, z_2, 1)向量
向量 + 向量(x1,y1,z1,0)+(x2,y2,z2,0)(x_1, y_1, z_1, 0) + (x_2, y_2, z_2, 0)向量

仿射组合:对于点 Pi=(xi,yi,zi,1)P_i = (x_i, y_i, z_i, 1),其仿射组合为:

∑iαiPi=(∑iαixi,∑iαiyi,∑iαizi,∑iαi)\sum_i \alpha_i P_i = \left(\sum_i \alpha_i x_i, \sum_i \alpha_i y_i, \sum_i \alpha_i z_i, \sum_i \alpha_i\right)

当 ∑iαi=1\sum_i \alpha_i = 1 时,结果是有效的点。

GAMES101 项目中的实际应用#

变换矩阵的统一表示:

// 不使用齐次坐标的变换(需要分别处理)
Vector3f transformed_point = rotation_matrix * point + translation_vector;
Vector3f transformed_vector = rotation_matrix * vector; // 向量不受平移影响
// 使用齐次坐标的统一变换
Matrix4f transformation = translation * rotation * scaling;
Vector4f homogeneous_point(point.x, point.y, point.z, 1.0f);
Vector4f homogeneous_vector(vector.x, vector.y, vector.z, 0.0f);
Vector4f transformed_point = transformation * homogeneous_point;
Vector4f transformed_vector = transformation * homogeneous_vector;
// 转换回笛卡尔坐标
Vector3f result_point = transformed_point.head<3>() / transformed_point.w();
Vector3f result_vector = transformed_vector.head<3>(); // w 分量为 0,不需要除法

MVP 变换链的实现:

// Assignment 1 中的核心变换
Matrix4f mvp = projection * view * model;
for (auto& vertex : vertices) {
Vector4f clip_coord = mvp * Vector4f(vertex.x, vertex.y, vertex.z, 1.0f);
// 透视除法(齐次坐标到 NDC)
Vector3f ndc_coord = clip_coord.head<3>() / clip_coord.w();
// 视口变换
Vector3f screen_coord = viewport_transform * ndc_coord;
}

二、几何变换数学原理#

2.1 线性变换理论#

2.1.1 线性变换的数学定义#

严格数学定义:设 VV 和 WW 是向量空间,映射 T:V→WT: V \to W 称为线性变换,当且仅当对于任意 u⃗,v⃗∈V\vec{u}, \vec{v} \in V 和标量 kk,满足:

  1. 加法保持性(可加性):T(u⃗+v⃗)=T(u⃗)+T(v⃗)T(\vec{u} + \vec{v}) = T(\vec{u}) + T(\vec{v})
  2. 数乘保持性(齐次性):T(ku⃗)=kT(u⃗)T(k\vec{u}) = kT(\vec{u})

等价条件:

T(αu⃗+βv⃗)=αT(u⃗)+βT(v⃗)T(\alpha\vec{u} + \beta\vec{v}) = \alpha T(\vec{u}) + \beta T(\vec{v})

矩阵表示定理:对于有限维向量空间,任何线性变换 T:Rn→RmT: \mathbb{R}^n \to \mathbb{R}^m 都可以唯一地表示为矩阵乘法 T(v⃗)=Av⃗T(\vec{v}) = A\vec{v},其中 A∈Rm×nA \in \mathbb{R}^{m \times n}。

变换矩阵的构造:矩阵 AA 的第 jj 列是标准基向量 ej⃗\vec{e_j} 的像:

A=[T(e1⃗)T(e2⃗)⋯T(en⃗)]A = [T(\vec{e_1}) \quad T(\vec{e_2}) \quad \cdots \quad T(\vec{e_n})]

线性变换的重要性质:

  • 零向量保持:T(0⃗)=0⃗T(\vec{0}) = \vec{0}
  • 线性组合保持:T(∑iαivi⃗)=∑iαiT(vi⃗)T\left(\sum_i \alpha_i \vec{v_i}\right) = \sum_i \alpha_i T(\vec{v_i})
  • 子空间保持:线性子空间的像仍是线性子空间
  • 平行线保持:平行线变换后仍平行(或重合)
  • 原点固定:线性变换总是将原点映射到原点

2.1.2 基本线性变换#

缩放变换(Scaling Transform)#

数学定义:

S(sx,sy,sz):(xyz)↦(sxxsyyszz)S(s_x, s_y, s_z): \begin{pmatrix} x \\ y \\ z \end{pmatrix} \mapsto \begin{pmatrix} s_x x \\ s_y y \\ s_z z \end{pmatrix}

齐次坐标矩阵表示:

S(sx,sy,sz)=(sx0000sy0000sz00001)S(s_x, s_y, s_z) = \begin{pmatrix} s_x & 0 & 0 & 0 \\ 0 & s_y & 0 & 0 \\ 0 & 0 & s_z & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}

几何效果分析:

  • 均匀缩放:sx=sy=sz=ss_x = s_y = s_z = s
    • s>1s > 1 放大、0<s<10 < s < 1 缩小、s=1s = 1 恒等
  • 非均匀缩放:改变物体的长宽高比例,可能导致圆变椭圆等形变
  • 反射缩放:某 si<0s_i < 0 时沿该轴反射

重要性质:

  • 可逆性:S−1(sx,sy,sz)=S(1/sx,1/sy,1/sz)S^{-1}(s_x, s_y, s_z) = S(1/s_x, 1/s_y, 1/s_z)
  • 行列式:det⁡(S)=sxsysz\det(S) = s_x s_y s_z(体积缩放因子)
  • 对角元素 sx,sy,szs_x, s_y, s_z 就是特征值
  • 缩放矩阵之间满足交换律

GAMES101 项目实现:

Eigen::Matrix4f create_scaling_matrix(float sx, float sy, float sz) {
Eigen::Matrix4f scaling = Eigen::Matrix4f::Identity();
scaling(0, 0) = sx;
scaling(1, 1) = sy;
scaling(2, 2) = sz;
return scaling;
}
// 应用示例:在 Assignment 1 中缩放三角形
Matrix4f model = create_scaling_matrix(1.5f, 1.5f, 1.5f); // 放大 1.5 倍
for (auto& vertex : triangle_vertices) {
Vector4f scaled_vertex = model * Vector4f(vertex.x, vertex.y, vertex.z, 1.0f);
vertex = scaled_vertex.head<3>();
}

旋转变换(Rotation Transform)#

绕坐标轴旋转的数学推导#

绕 Z 轴旋转的详细推导:设点 P(x,y)P(x, y) 绕原点逆时针旋转角度 θ\theta 到点 P′(x′,y′)P'(x', y')。

极坐标方法:将点 PP 表示为极坐标形式 P:(rcos⁡ϕ,rsin⁡ϕ)P: (r\cos\phi, r\sin\phi),P′:(rcos⁡(ϕ+θ),rsin⁡(ϕ+θ))P': (r\cos(\phi + \theta), r\sin(\phi + \theta))。

利用三角恒等式:

x′=rcos⁡(ϕ+θ)=xcos⁡θ−ysin⁡θx' = r\cos(\phi + \theta) = x\cos\theta - y\sin\thetay′=rsin⁡(ϕ+θ)=xsin⁡θ+ycos⁡θy' = r\sin(\phi + \theta) = x\sin\theta + y\cos\theta

矩阵形式:

Rz(θ)=(cos⁡θ−sin⁡θ00sin⁡θcos⁡θ0000100001)R_z(\theta) = \begin{pmatrix} \cos\theta & -\sin\theta & 0 & 0 \\ \sin\theta & \cos\theta & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}

其他坐标轴旋转:

Rx(θ)=(10000cos⁡θ−sin⁡θ00sin⁡θcos⁡θ00001)R_x(\theta) = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & \cos\theta & -\sin\theta & 0 \\ 0 & \sin\theta & \cos\theta & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}Ry(θ)=(cos⁡θ0sin⁡θ00100−sin⁡θ0cos⁡θ00001)R_y(\theta) = \begin{pmatrix} \cos\theta & 0 & \sin\theta & 0 \\ 0 & 1 & 0 & 0 \\ -\sin\theta & 0 & \cos\theta & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}
WARNING

注意 RyR_y 中 sin⁡θ\sin\theta 的符号与 Rx,RzR_x, R_z 相反,这是因为右手坐标系下 y 轴与其他两轴的位置关系决定的。

旋转矩阵的重要性质:

  • 正交性:RTR=IR^T R = I,即 R−1=RTR^{-1} = R^T
  • 行列式:det⁡(R)=1\det(R) = 1(保持定向)
  • 长度保持:∥Rv⃗∥=∥v⃗∥\|R\vec{v}\| = \|\vec{v}\|
  • 角度保持:u⃗⋅v⃗=(Ru⃗)⋅(Rv⃗)\vec{u} \cdot \vec{v} = (R\vec{u}) \cdot (R\vec{v})

GAMES101 Assignment 1 实现:

Eigen::Matrix4f get_model_matrix(float rotation_angle) {
Eigen::Matrix4f model = Eigen::Matrix4f::Identity();
// 角度转弧度
float angle_rad = rotation_angle * MY_PI / 180.0f;
float cos_a = std::cos(angle_rad);
float sin_a = std::sin(angle_rad);
// 绕 Z 轴旋转矩阵
model(0, 0) = cos_a; model(0, 1) = -sin_a;
model(1, 0) = sin_a; model(1, 1) = cos_a;
return model;
}

任意轴旋转(Rodrigues 旋转公式)#

问题描述:给定单位向量 n⃗\vec{n} 和角度 θ\theta,求绕轴 n⃗\vec{n} 旋转 θ\theta 角度的旋转矩阵。

Rodrigues 公式(向量形式):

v⃗rot=v⃗cos⁡θ+(n⃗×v⃗)sin⁡θ+n⃗(n⃗⋅v⃗)(1−cos⁡θ)\vec{v}_{\text{rot}} = \vec{v}\cos\theta + (\vec{n} \times \vec{v})\sin\theta + \vec{n}(\vec{n} \cdot \vec{v})(1-\cos\theta)

矩阵形式:

R(n⃗,θ)=Icos⁡θ+[n⃗]×sin⁡θ+n⃗n⃗T(1−cos⁡θ)R(\vec{n}, \theta) = I\cos\theta + [\vec{n}]_{\times}\sin\theta + \vec{n}\vec{n}^T(1-\cos\theta)

其中反对称矩阵(叉积矩阵):

[n⃗]×=(0−nznynz0−nx−nynx0)[\vec{n}]_{\times} = \begin{pmatrix} 0 & -n_z & n_y \\ n_z & 0 & -n_x \\ -n_y & n_x & 0 \end{pmatrix}

公式的几何意义:

  • n⃗n⃗T(1−cos⁡θ)\vec{n}\vec{n}^T(1-\cos\theta):向量在旋转轴上的投影分量(不变部分)
  • Icos⁡θI\cos\theta:垂直于旋转轴的分量的余弦部分
  • [n⃗]×sin⁡θ[\vec{n}]_{\times}\sin\theta:垂直于旋转轴的分量的正弦部分

推导过程:

  1. 将向量 v⃗\vec{v} 分解为平行和垂直于 n⃗\vec{n} 的分量:v⃗=v⃗∥+v⃗⊥\vec{v} = \vec{v}_{\parallel} + \vec{v}_{\perp},其中 v⃗∥=(v⃗⋅n⃗)n⃗\vec{v}_{\parallel} = (\vec{v} \cdot \vec{n})\vec{n}
  2. 平行分量不受旋转影响:v⃗∥′=v⃗∥\vec{v}_{\parallel}' = \vec{v}_{\parallel}
  3. 垂直分量在垂直于 n⃗\vec{n} 的平面内旋转:v⃗⊥′=v⃗⊥cos⁡θ+(n⃗×v⃗⊥)sin⁡θ\vec{v}_{\perp}' = \vec{v}_{\perp}\cos\theta + (\vec{n} \times \vec{v}_{\perp})\sin\theta

GAMES101 项目实现:

Eigen::Matrix4f get_rotation(Vector3f axis, float angle) {
// 确保轴向量为单位向量
axis.normalize();
float theta = angle * MY_PI / 180.0f;
// 构建 3x3 旋转矩阵
Eigen::Matrix3f I = Eigen::Matrix3f::Identity();
// 反对称矩阵 [n]×
Eigen::Matrix3f N;
N << 0, -axis[2], axis[1],
axis[2], 0, -axis[0],
-axis[1], axis[0], 0;
// 外积矩阵 nnᵀ
Eigen::Matrix3f nnT = axis * axis.transpose();
// Rodrigues 公式
Eigen::Matrix3f R = I * std::cos(theta)
+ N * std::sin(theta)
+ nnT * (1 - std::cos(theta));
// 扩展为 4x4 齐次矩阵
Eigen::Matrix4f rotation_matrix = Eigen::Matrix4f::Identity();
rotation_matrix.block<3, 3>(0, 0) = R;
return rotation_matrix;
}

特殊情况验证:

  • 当 n⃗=(0,0,1)\vec{n} = (0, 0, 1) 时,退化为绕 Z 轴旋转
  • 当 θ=0\theta = 0 时,R=IR = I(恒等变换)
  • 当 θ=π\theta = \pi 时,R=2n⃗n⃗T−IR = 2\vec{n}\vec{n}^T - I(关于轴的 180° 旋转)

2.2 仿射变换理论#

2.2.1 仿射变换的数学特性#

仿射变换的严格定义#

数学定义:仿射变换是线性变换与平移的复合:

T(v⃗)=Av⃗+b⃗T(\vec{v}) = A\vec{v} + \vec{b}

其中 A∈Rn×nA \in \mathbb{R}^{n \times n} 是线性变换矩阵,b⃗∈Rn\vec{b} \in \mathbb{R}^n 是平移向量。

点与向量的变换:

  • 点的变换:P′=AP+b⃗P' = AP + \vec{b}
  • 向量的变换:v⃗′=Av⃗\vec{v}' = A\vec{v}(向量不受平移影响)

仿射变换的几何性质#

保持性质(不变量):

  1. 直线性保持:直线 L:p⃗(t)=p0⃗+td⃗L: \vec{p}(t) = \vec{p_0} + t\vec{d} 变换后仍为直线
  2. 平行性保持:平行直线变换后仍平行
  3. 比例保持:线段上的比例关系保持不变
  4. 重心保持:点集的重心变换后仍为变换后点集的重心

不保持的性质:

  • 长度(除非 AA 是正交矩阵)
  • 角度(除非 AA 是相似变换)
  • 面积 / 体积(缩放因子为 ∥det⁡(A)∥\|\det(A)\|)

仿射变换的分类#

类型矩阵形式保持的性质
刚体变换AA 正交,det⁡(A)=1\det(A) = 1长度、角度、面积
相似变换A=sRA = sR,s>0s > 0,RR 正交角度、形状
一般仿射变换任意可逆 AA仅平行性和比例

任何仿射变换都可以分解为基本变换的复合:T=Ttrans∘Trot∘Tscale∘TshearT = T_{\text{trans}} \circ T_{\text{rot}} \circ T_{\text{scale}} \circ T_{\text{shear}}。

2.2.2 平移变换的深度分析#

平移变换的数学本质#

为什么平移不是线性变换:线性变换必须满足 T(0⃗)=0⃗T(\vec{0}) = \vec{0},但平移 T(v⃗)=v⃗+t⃗T(\vec{v}) = \vec{v} + \vec{t} 有:

T(0⃗)=t⃗≠0⃗(t⃗≠0⃗)T(\vec{0}) = \vec{t} \neq \vec{0} \quad (\vec{t} \neq \vec{0})

齐次坐标中的平移表示#

齐次坐标矩阵:

T(tx,ty,tz)=(100tx010ty001tz0001)T(t_x, t_y, t_z) = \begin{pmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{pmatrix}

向量与点的区别:

  • 点的平移:(x,y,z,1)→(x+tx,y+ty,z+tz,1)(x, y, z, 1) \to (x+t_x, y+t_y, z+t_z, 1)
  • 向量的平移:(x,y,z,0)→(x,y,z,0)(x, y, z, 0) \to (x, y, z, 0)(不受平移影响)

平移变换的群性质#

平移变换构成一个交换群 (T,∘)(T, \circ):

  1. 封闭性:T(a⃗)∘T(b⃗)=T(a⃗+b⃗)T(\vec{a}) \circ T(\vec{b}) = T(\vec{a} + \vec{b})
  2. 结合律、单位元 T(0⃗)T(\vec{0})、逆元 T(a⃗)−1=T(−a⃗)T(\vec{a})^{-1} = T(-\vec{a})
  3. 交换律:T(a⃗)∘T(b⃗)=T(b⃗)∘T(a⃗)T(\vec{a}) \circ T(\vec{b}) = T(\vec{b}) \circ T(\vec{a})

GAMES101 项目中的应用#

Eigen::Matrix4f create_translation_matrix(float tx, float ty, float tz) {
Eigen::Matrix4f translation = Eigen::Matrix4f::Identity();
translation(0, 3) = tx;
translation(1, 3) = ty;
translation(2, 3) = tz;
return translation;
}
// Assignment 1 中的视图变换
Eigen::Matrix4f get_view_matrix(Eigen::Vector3f eye_pos) {
Eigen::Matrix4f view = Eigen::Matrix4f::Identity();
// 平移到原点(相机位置的逆变换)
Eigen::Matrix4f translate = create_translation_matrix(-eye_pos[0], -eye_pos[1], -eye_pos[2]);
view = translate * view;
return view;
}

2.2.3 变换复合的数学理论#

变换复合的基本原理#

对于变换 T1,T2,T3T_1, T_2, T_3:

(T3∘T2∘T1)(v⃗)=T3(T2(T1(v⃗)))(T_3 \circ T_2 \circ T_1)(\vec{v}) = T_3(T_2(T_1(\vec{v})))

在齐次坐标中对应矩阵乘法:T3∘T2∘T1⟷M3M2M1T_3 \circ T_2 \circ T_1 \longleftrightarrow M_3 M_2 M_1

WARNING

矩阵乘法的顺序与变换应用顺序相反!变换顺序是 T1→T2→T3T_1 \to T_2 \to T_3,矩阵乘法顺序是 M3M2M1M_3 M_2 M_1。

变换顺序的重要性#

非交换性示例:考虑平移 T(1,0,0)T(1, 0, 0) 和绕 Z 轴旋转 90°90°:

  • 先平移后旋转:R90°∘T(1,0,0):(0,0,0)→(1,0,0)→(0,1,0)R_{90°} \circ T(1,0,0): (0,0,0) \to (1,0,0) \to (0,1,0)
  • 先旋转后平移:T(1,0,0)∘R90°:(0,0,0)→(0,0,0)→(1,0,0)T(1,0,0) \circ R_{90°}: (0,0,0) \to (0,0,0) \to (1,0,0)

结果不同!

标准变换序列(SRT 分解)#

Mtotal=T⋅R⋅SM_{\text{total}} = T \cdot R \cdot S

为什么采用 SRT 顺序:

  1. Scale:在局部坐标系中进行,不影响物体朝向
  2. Rotate:在缩放后的坐标系中进行,保持物体形状
  3. Translate:最后移动到世界坐标系中的目标位置
// 错误顺序:RST(缩放会影响已旋转的物体,可能导致非均匀拉伸)
Matrix4f wrong_order = T * S * R;
// 正确顺序:SRT
Matrix4f correct_order = T * R * S;

图形学中的变换链#

完整的 MVP 变换链:

屏幕坐标=Mviewport⋅Mprojection⋅Mview⋅Mmodel⋅局部坐标\text{屏幕坐标} = M_{\text{viewport}} \cdot M_{\text{projection}} \cdot M_{\text{view}} \cdot M_{\text{model}} \cdot \text{局部坐标}
阶段作用
模型变换(Model)局部坐标 → 世界坐标
视图变换(View)世界坐标 → 观察坐标
投影变换(Projection)观察坐标 → 裁剪坐标
视口变换(Viewport)NDC → 屏幕坐标

复杂物体的变换分解:

Matrix4f create_complex_transform(Vector3f scale,
Vector3f rotation_angles,
Vector3f translation) {
// 1. 创建基本变换矩阵
Matrix4f S = create_scaling_matrix(scale.x, scale.y, scale.z);
Matrix4f Rx = create_rotation_x(rotation_angles.x);
Matrix4f Ry = create_rotation_y(rotation_angles.y);
Matrix4f Rz = create_rotation_z(rotation_angles.z);
Matrix4f T = create_translation_matrix(translation.x, translation.y, translation.z);
// 2. 按 SRT 顺序复合(旋转顺序:X → Y → Z)
Matrix4f R = Rz * Ry * Rx;
Matrix4f transform = T * R * S;
return transform;
}

2.3 法向量变换(规范位置)#

NOTE

本节是全系列法向量变换的规范推导位置。Part 2 光栅化管线中出现的法向量变换只展示代码实现,完整推导请回到本节。

2.3.1 为什么法向量不能直接用模型矩阵变换#

一个看似自然的想法是,用物体的模型矩阵 M\mathbf{M} 同时变换顶点和法向量。但在存在非均匀缩放时,这种做法会破坏法向量与切面的垂直关系。

直观反例:想象一个球被沿 X 方向拉伸成橄榄球。顶点坐标按 M\mathbf{M} 变换是正确的。但如果法向量也按 M\mathbf{M} 变换,它会倾向于继续指向”原来的球面外”方向,而不是新椭球面的真实法线。

2.3.2 完整推导:逆转置矩阵#

起点:设切向量 t⃗\vec{t} 在切平面内,法向量 n⃗\vec{n} 与切平面垂直:

n⃗⋅t⃗=0\vec{n} \cdot \vec{t} = 0

切向量沿着曲面方向、和顶点差向量一样按 M\mathbf{M} 变换:

t⃗′=Mt⃗\vec{t}' = \mathbf{M} \vec{t}

设变换后的法向量为 n⃗′=Nn⃗\vec{n}' = \mathbf{N} \vec{n},其中 N\mathbf{N} 是我们要求的法向量变换矩阵。变换后仍需保持垂直:

n⃗′⋅t⃗′=0\vec{n}' \cdot \vec{t}' = 0

写成矩阵形式(点积写作行向量乘列向量):

(Nn⃗)T(Mt⃗)=0  ⟹  n⃗TNTMt⃗=0(\mathbf{N} \vec{n})^T (\mathbf{M} \vec{t}) = 0 \implies \vec{n}^T \mathbf{N}^T \mathbf{M} \vec{t} = 0

原始条件 n⃗⋅t⃗=0\vec{n} \cdot \vec{t} = 0 写成矩阵形式为 n⃗Tt⃗=0\vec{n}^T \vec{t} = 0。两式对任意切向量 t⃗\vec{t} 都要成立,意味着:

NTM=I\mathbf{N}^T \mathbf{M} = \mathbf{I}

解得:

N=(M−1)T=(MT)−1\boxed{\mathbf{N} = (\mathbf{M}^{-1})^T = (\mathbf{M}^T)^{-1}}

法向量应当使用模型矩阵的逆转置(inverse transpose) 进行变换。

📌 几种特殊 M\mathbf{M} 下的法向量矩阵(完整讨论)#

情况 1:正交矩阵(纯旋转)

若 M\mathbf{M} 是正交矩阵(旋转),则 M−1=MT\mathbf{M}^{-1} = \mathbf{M}^T,于是:

N=(MT)−1=M\mathbf{N} = (\mathbf{M}^T)^{-1} = \mathbf{M}

结论:纯旋转下可以直接用模型矩阵变换法向量。

情况 2:均匀缩放

若 M=sI\mathbf{M} = s\mathbf{I},则 M−1=1sI\mathbf{M}^{-1} = \frac{1}{s}\mathbf{I},于是:

N=1sI\mathbf{N} = \frac{1}{s}\mathbf{I}

结论:法向量长度缩小 1/s1/s 倍,方向不变。实际使用时再做一次归一化,所以和直接用 M\mathbf{M} 等价。

情况 3:非均匀缩放

若 M=diag(sx,sy,sz)\mathbf{M} = \text{diag}(s_x, s_y, s_z),则:

N=diag(1/sx,1/sy,1/sz)\mathbf{N} = \text{diag}(1/s_x, 1/s_y, 1/s_z)

结论:各分量取倒数,方向会被显著改变——这是非均匀缩放下必须用逆转置的根本原因。

情况 4:刚体变换(旋转 + 平移)

平移不影响线性部分的 3×33\times 3 子矩阵,法向量变换只看旋转部分。取 M\mathbf{M} 左上角 3×33\times 3 的逆转置即可。

2.3.3 工程实现#

// 计算法向量变换矩阵
Eigen::Matrix3f compute_normal_matrix(const Eigen::Matrix4f& model_matrix) {
// 只取左上 3x3,平移不影响法向量
Eigen::Matrix3f upper_left = model_matrix.block<3, 3>(0, 0);
return upper_left.inverse().transpose();
}
// 应用法向量变换
Eigen::Vector3f transform_normal(const Eigen::Vector3f& normal,
const Eigen::Matrix4f& model_matrix) {
Eigen::Matrix3f normal_matrix = compute_normal_matrix(model_matrix);
Eigen::Vector3f transformed = normal_matrix * normal;
return transformed.normalized(); // 必须重新归一化,抵消缩放
}

实现要点:

  • 仅取 4×44\times 4 矩阵左上角 3×33\times 3——平移项不影响法向量
  • 变换后必须重新归一化,否则光照计算会出错
  • 当模型矩阵中只有旋转和均匀缩放时,逆转置退化为原矩阵的倍数;但作为通用路径,始终走逆转置是最稳妥的

三、投影几何学#

3.1 投影变换的数学基础#

3.1.1 投影的几何原理#

投影定义:将高维空间的点映射到低维空间的过程。

投影类型:

  1. 平行投影:投影线平行
    • 正交投影:投影线垂直于投影平面
    • 斜投影:投影线不垂直于投影平面
  2. 透视投影:投影线汇聚于一点(视点)

3.1.2 透视投影的完整数学推导#

透视投影的几何基础#

标准设置:

  • 视点位于坐标原点:E=(0,0,0)E = (0, 0, 0)
  • 投影平面垂直于 Z 轴:Π:z=−d\Pi: z = -d(d>0d > 0)
  • 观察方向沿负 Z 轴方向

相似三角形推导#

对于空间中的点 P(x,y,z)P(x, y, z)(其中 z<0z < 0),从视点向 PP 发出的射线与投影平面的交点 P′(x′,y′,−d)P'(x', y', -d) 满足:

x′x=d−z  ⟹  x′=−dxz\frac{x'}{x} = \frac{d}{-z} \implies x' = -\frac{dx}{z}y′y=d−z  ⟹  y′=−dyz\frac{y'}{y} = \frac{d}{-z} \implies y' = -\frac{dy}{z}

透视除法的本质:(x,y,z)→(−dx/z,−dy/z,−d)(x, y, z) \to (-dx/z, -dy/z, -d)

齐次坐标中的透视投影#

矩阵推导:设 w′=−z/dw' = -z/d,则透视除法后:

  • x′/w′=−dx/zx' / w' = -dx/z
  • y′/w′=−dy/zy' / w' = -dy/z

得到简单透视投影矩阵:

Psimple=(10000100001000−1/d0)P_{\text{simple}} = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & -1/d & 0 \end{pmatrix}

深度保持的处理#

简单透视投影会丢失深度信息。修改 Z 分量使其保持深度顺序,要求:

  • 近平面 z=−nz = -n 映射到 z′=−1z' = -1
  • 远平面 z=−fz = -f 映射到 z′=1z' = 1

3.1.3 标准透视投影矩阵的完整推导#

视锥体(View Frustum)的定义#

视锥体参数:

  • fov\text{fov}:垂直视场角(Field of View)
  • aspect\text{aspect}:宽高比 =width/height= \text{width} / \text{height}
  • nn:近裁剪面距离
  • ff:远裁剪面距离

投影平面尺寸的计算#

在近平面 z=−nz = -n 处,视锥体尺寸为:

top=n⋅tan⁡(fov2),bottom=−top\text{top} = n \cdot \tan\left(\frac{\text{fov}}{2}\right), \quad \text{bottom} = -\text{top}right=top⋅aspect,left=−right\text{right} = \text{top} \cdot \text{aspect}, \quad \text{left} = -\text{right}

一般透视投影矩阵#

P=(2nr−l0r+lr−l002nt−bt+bt−b000−f+nf−n−2fnf−n00−10)P = \begin{pmatrix} \frac{2n}{r-l} & 0 & \frac{r+l}{r-l} & 0 \\ 0 & \frac{2n}{t-b} & \frac{t+b}{t-b} & 0 \\ 0 & 0 & -\frac{f+n}{f-n} & -\frac{2fn}{f-n} \\ 0 & 0 & -1 & 0 \end{pmatrix}

对称视锥体的简化#

当 l=−rl = -r、b=−tb = -t 时,矩阵简化为:

Psymmetric=(1aspect⋅tan⁡(fov/2)00001tan⁡(fov/2)0000−f+nf−n−2fnf−n00−10)P_{\text{symmetric}} = \begin{pmatrix} \frac{1}{\text{aspect} \cdot \tan(\text{fov}/2)} & 0 & 0 & 0 \\ 0 & \frac{1}{\tan(\text{fov}/2)} & 0 & 0 \\ 0 & 0 & -\frac{f+n}{f-n} & -\frac{2fn}{f-n} \\ 0 & 0 & -1 & 0 \end{pmatrix}

GAMES101 Assignment 1 项目实现#

Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio,
float zNear, float zFar) {
Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();
// 视场角从度转换为弧度
float eye_fov_rad = eye_fov * MY_PI / 180.0f;
float t = std::tan(eye_fov_rad / 2.0f);
// 根据对称视锥体的简化公式填充矩阵
projection(0, 0) = 1.0f / (aspect_ratio * t); // X 缩放
projection(1, 1) = 1.0f / t; // Y 缩放
projection(2, 2) = -(zFar + zNear) / (zFar - zNear); // Z 线性系数
projection(2, 3) = -2.0f * zFar * zNear / (zFar - zNear); // Z 常数系数
projection(3, 2) = -1.0f; // 透视除法触发器
projection(3, 3) = 0.0f;
return projection;
}

变换过程验证:

// 测试近平面上的点
Vector4f near_point(0, 0, -zNear, 1);
Vector4f projected = projection * near_point;
// projected = (0, 0, zNear, zNear),透视除法后:(0, 0, 1) ✓
// 测试远平面上的点
Vector4f far_point(0, 0, -zFar, 1);
Vector4f projected_far = projection * far_point;
// 透视除法后的 z 坐标应该接近 -1

在渲染管线中的应用:

// Assignment 1 中的完整变换链
void rasterizer::draw(pos_buf_id pos_buffer, ind_buf_id ind_buffer, Primitive type) {
auto& buf = pos_buf[pos_buffer.pos_id];
auto& ind = ind_buf[ind_buffer.ind_id];
// 构建 MVP 矩阵
Eigen::Matrix4f mvp = projection * view * model;
for (auto& i : ind) {
Triangle t;
// 应用 MVP 变换
Eigen::Vector4f v[] = {
mvp * to_vec4(buf[i[0]], 1.0f),
mvp * to_vec4(buf[i[1]], 1.0f),
mvp * to_vec4(buf[i[2]], 1.0f)
};
// 透视除法:齐次坐标 → NDC
for (auto& vec : v) vec /= vec.w();
// 视口变换:NDC → 屏幕坐标
for (auto& vert : v) {
vert.x() = 0.5f * width * (vert.x() + 1.0f);
vert.y() = 0.5f * height * (vert.y() + 1.0f);
vert.z() = vert.z() * f1 + f2;
}
for (int i = 0; i < 3; ++i) t.setVertex(i, v[i].head<3>());
rasterize_wireframe(t);
}
}

3.1.4 深度缓冲区与 Z-Fighting 问题#

深度值的非线性分布#

经过透视投影和透视除法后:

zbuffer=12(f+nf−n+2fnz(f−n))z_{\text{buffer}} = \frac{1}{2}\left(\frac{f+n}{f-n} + \frac{2fn}{z(f-n)}\right)

非线性特性:深度精度定义为 dzbufferdz=fnz2(f−n)\frac{dz_{\text{buffer}}}{dz} = \frac{fn}{z^2(f-n)},与 z2z^2 成反比,距离越远精度越低。

Z-Fighting 现象#

当两个表面非常接近时,由于深度缓冲区精度限制,会出现闪烁现象。

解决方法:

  1. 优化近远平面比值:减小 f/nf/n 比值

    // 不好的设置
    float near = 0.1f, far = 10000.0f; // 比值 = 100000
    // 更好的设置
    float near = 1.0f, far = 1000.0f; // 比值 = 1000
  2. 多边形偏移(Polygon Offset):

    glEnable(GL_POLYGON_OFFSET_FILL);
    glPolygonOffset(factor, units);
    // z' = z + factor * dz/dx + units * r
  3. 对数深度缓冲区:zlog⁡=log⁡(z/n)log⁡(f/n)z_{\log} = \frac{\log(z/n)}{\log(f/n)}

  4. 反向 Z 缓冲区:将远平面映射到 0,近平面映射到 1,利用浮点数在 0 附近精度更高的特性

GAMES101 项目中的深度处理#

// Assignment 2 中的深度测试实现
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
auto v = t.toVector4();
for (int x = bbox_min_x; x <= bbox_max_x; x++) {
for (int y = bbox_min_y; y <= bbox_max_y; y++) {
auto [alpha, beta, gamma] = computeBarycentric2D(x, y, t.v);
if (alpha >= 0 && beta >= 0 && gamma >= 0) {
// 插值计算深度值
float z_interpolated = alpha * v[0].z()
+ beta * v[1].z()
+ gamma * v[2].z();
// 深度测试
if (z_interpolated < depth_buf[get_index(x, y)]) {
depth_buf[get_index(x, y)] = z_interpolated;
set_pixel(Vector3f(x, y, 1.0f), t.getColor());
}
}
}
}
}

3.2 视图变换#

3.2.1 摄像机模型#

摄像机参数:

  • eye:摄像机位置
  • target:观察目标点
  • up:上方向向量

局部坐标系构建:

Vector3f forward = (target - eye).normalized(); // 前方向(-z 轴)
Vector3f right = forward.cross(up).normalized(); // 右方向(x 轴)
Vector3f camera_up = right.cross(forward); // 上方向(y 轴)

3.2.2 视图矩阵推导#

目标:将世界坐标系变换到摄像机坐标系,分两步:

  1. 平移:将摄像机移动到原点
  2. 旋转:将摄像机坐标轴与世界坐标轴对齐

平移矩阵:

T=(100−eyex010−eyey001−eyez0001)\mathbf{T} = \begin{pmatrix} 1 & 0 & 0 & -\text{eye}_x \\ 0 & 1 & 0 & -\text{eye}_y \\ 0 & 0 & 1 & -\text{eye}_z \\ 0 & 0 & 0 & 1 \end{pmatrix}

旋转矩阵:

R=(rightxrightyrightz0upxupyupz0−forwardx−forwardy−forwardz00001)\mathbf{R} = \begin{pmatrix} \text{right}_x & \text{right}_y & \text{right}_z & 0 \\ \text{up}_x & \text{up}_y & \text{up}_z & 0 \\ -\text{forward}_x & -\text{forward}_y & -\text{forward}_z & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}

完整视图矩阵:V=R⋅T\mathbf{V} = \mathbf{R} \cdot \mathbf{T}

Eigen::Matrix4f get_view_matrix(Eigen::Vector3f eye_pos) {
Eigen::Matrix4f view = Eigen::Matrix4f::Identity();
Eigen::Matrix4f translate;
translate << 1, 0, 0, -eye_pos[0],
0, 1, 0, -eye_pos[1],
0, 0, 1, -eye_pos[2],
0, 0, 0, 1;
view = translate * view;
return view;
}

四、微积分在图形学中的应用#

4.1 参数曲线的微分几何理论#

4.1.1 参数曲线的数学表示#

参数方程的一般形式:

r⃗(t)=(x(t)y(t)z(t)),t∈[a,b]\vec{r}(t) = \begin{pmatrix} x(t) \\ y(t) \\ z(t) \end{pmatrix}, \quad t \in [a, b]

参数化的优势:

  • 统一表示:直线、圆、椭圆等都可用参数方程统一描述
  • 方向性:参数增加方向给出曲线的自然定向
  • 计算便利:微分、积分运算转化为对参数的运算

一阶导数(切向量 / 速度向量):

r⃗′(t)=dr⃗dt=(x′(t)y′(t)z′(t))\vec{r}'(t) = \frac{d\vec{r}}{dt} = \begin{pmatrix} x'(t) \\ y'(t) \\ z'(t) \end{pmatrix}

几何与物理意义:

  • 几何:r⃗′(t)\vec{r}'(t) 是曲线在点 r⃗(t)\vec{r}(t) 处的切向量
  • 物理:若 tt 表示时间,r⃗′(t)\vec{r}'(t) 是质点的瞬时速度向量
  • 方向:指向参数增加的方向

单位切向量:T⃗(t)=r⃗′(t)∥r⃗′(t)∥\vec{T}(t) = \frac{\vec{r}'(t)}{\|\vec{r}'(t)\|}

4.1.2 曲率理论与计算#

曲率的数学定义#

曲率 κ(t)\kappa(t) 描述曲线偏离直线的程度:

κ(t)=∥dT⃗dt∥\kappa(t) = \left\|\frac{d\vec{T}}{dt}\right\|

计算公式:

κ(t)=∥r⃗′(t)×r⃗′′(t)∥∥r⃗′(t)∥3\kappa(t) = \frac{\|\vec{r}'(t) \times \vec{r}''(t)\|}{\|\vec{r}'(t)\|^3}

二维情况的特殊形式(y=f(x)y = f(x)):

κ=∣f′′(x)∣(1+(f′(x))2)3/2\kappa = \frac{|f''(x)|}{(1 + (f'(x))^2)^{3/2}}

曲率的几何意义#

  • κ=0\kappa = 0:曲线为直线
  • κ>0\kappa > 0:曲线弯曲,数值越大弯曲程度越大
  • κ=1/R\kappa = 1/R:RR 为曲率半径(密切圆的半径)

在计算机图形学中的应用#

自适应曲线细分:

float compute_curvature(const Vector3f& p0, const Vector3f& p1, const Vector3f& p2) {
Vector3f v1 = p1 - p0;
Vector3f v2 = p2 - p1;
Vector3f cross_product = v1.cross(v2);
float numerator = cross_product.norm();
float denominator = std::pow(v1.norm(), 3);
return (denominator > 1e-6f) ? numerator / denominator : 0.0f;
}
void adaptive_subdivision(const BezierCurve& curve, float tolerance) {
for (float t = 0.0f; t < 1.0f; ) {
float curvature = compute_curvature_at(curve, t);
// 在高曲率处使用更小的步长
float step = std::min(tolerance / std::max(curvature, 1e-3f), 0.1f);
render_curve_segment(curve, t, t + step);
t += step;
}
}

动画路径平滑:通过控制曲率连续性确保动画的平滑过渡。

4.2 积分理论在渲染中的应用#

4.2.1 渲染方程的数学基础#

NOTE

本节只把渲染方程作为”微积分在图形学中的应用”引子。完整的辐射度量学体系、BRDF 所有性质、Neumann 级数收敛性、路径追踪估计量请见 Part 4:光线追踪与全局光照。

渲染方程的物理推导#

能量守恒原理:在稳态条件下,表面某点的出射辐射度等于自发光加上所有入射光线经反射后的贡献。

渲染方程的完整形式:

Lo(p,ωo)=Le(p,ωo)+∫Ωfr(p,ωi,ωo)Li(p,ωi)cos⁡θi dωiL_o(\mathbf{p}, \omega_o) = L_e(\mathbf{p}, \omega_o) + \int_{\Omega} f_r(\mathbf{p}, \omega_i, \omega_o) L_i(\mathbf{p}, \omega_i) \cos\theta_i\, d\omega_i
符号含义
Lo(p,ωo)L_o(\mathbf{p}, \omega_o)点 p\mathbf{p} 沿方向 ωo\omega_o 的出射辐射度
Le(p,ωo)L_e(\mathbf{p}, \omega_o)点 p\mathbf{p} 的自发光辐射度
fr(p,ωi,ωo)f_r(\mathbf{p}, \omega_i, \omega_o)双向反射分布函数(BRDF)
Li(p,ωi)L_i(\mathbf{p}, \omega_i)沿方向 ωi\omega_i 的入射辐射度
Ω\Omega以点 p\mathbf{p} 为中心的上半球立体角
θi\theta_i入射方向与表面法向量的夹角

📌 BRDF 的数学定义与三条核心性质#

定义:

fr(p,ωi,ωo)=dLo(p,ωo)dEi(p,ωi)f_r(\mathbf{p}, \omega_i, \omega_o) = \frac{dL_o(\mathbf{p}, \omega_o)}{dE_i(\mathbf{p}, \omega_i)}

刻画了在点 p\mathbf{p} 处,来自方向 ωi\omega_i 的单位辐照度有多少比例被反射到方向 ωo\omega_o。

三条核心性质:

  1. 非负性:fr≥0f_r \geq 0。反射能量不可为负。

  2. Helmholtz 互易性:fr(p,ωi,ωo)=fr(p,ωo,ωi)f_r(\mathbf{p}, \omega_i, \omega_o) = f_r(\mathbf{p}, \omega_o, \omega_i)。这是光路可逆的直接推论,也是路径追踪能够从相机反向追踪光线的理论基础。

  3. 能量守恒:

    ∫Ωfr(p,ωi,ωo)cos⁡θo dωo≤1\int_{\Omega} f_r(\mathbf{p}, \omega_i, \omega_o) \cos\theta_o \, d\omega_o \leq 1

    反射的总能量不能超过入射能量(否则违反热力学第二定律)。

具体 BRDF 的推导(Lambert、Phong、Cook-Torrance 等)见 Part 4 §20.1.2。

4.2.2 蒙特卡洛积分理论#

蒙特卡洛方法的数学基础#

基本原理:对于积分 I=∫Df(x)dxI = \int_D f(\mathbf{x}) d\mathbf{x},蒙特卡洛估计为:

I^=1N∑i=1Nf(Xi)p(Xi)\hat{I} = \frac{1}{N} \sum_{i=1}^N \frac{f(\mathbf{X}_i)}{p(\mathbf{X}_i)}

其中 Xi\mathbf{X}_i 是根据概率密度函数 p(x)p(\mathbf{x}) 采样的随机变量。

估计量的性质:

  • 无偏性:E[I^]=IE[\hat{I}] = I
  • 方差:Var[I^]=1N∫D(f(x)p(x)−I)2p(x)dx\text{Var}[\hat{I}] = \frac{1}{N} \int_D \left(\frac{f(\mathbf{x})}{p(\mathbf{x})} - I\right)^2 p(\mathbf{x}) d\mathbf{x}
  • 收敛性:I^→PI\hat{I} \xrightarrow{P} I 当 N→∞N \to \infty

重要性采样的数学优化#

方差最小化:最优的概率密度函数为:

p∗(x)=∣f(x)∣∫D∣f(y)∣dyp^*(\mathbf{x}) = \frac{|f(\mathbf{x})|}{\int_D |f(\mathbf{y})| d\mathbf{y}}

此时方差为零,但实际中难以实现。

实用策略:选择 p(x)∝∣f(x)∣p(\mathbf{x}) \propto |f(\mathbf{x})| 可以显著减少方差。

在路径追踪中的实现#

Vector3f monte_carlo_integration(const Intersection& hit_point, int samples) {
Vector3f color(0, 0, 0);
const Vector3f& normal = hit_point.normal;
for (int i = 0; i < samples; ++i) {
// 重要性采样:按余弦分布采样
Vector3f sample_dir = cosine_weighted_hemisphere_sample();
// 计算采样概率密度
float cos_theta = std::max(0.0f, normal.dot(sample_dir));
float pdf = cos_theta / M_PI; // 余弦加权采样的 PDF
if (pdf > 1e-6f) {
// 计算 BRDF 值
Vector3f brdf_value = evaluate_brdf(hit_point, sample_dir);
// 递归追踪光线
Vector3f incoming_radiance = trace_ray(hit_point.position, sample_dir);
// 蒙特卡洛估计
color += brdf_value * incoming_radiance * cos_theta / pdf;
}
}
return color / static_cast<float>(samples);
}
// 余弦加权半球采样
Vector3f cosine_weighted_hemisphere_sample() {
float u1 = random_float();
float u2 = random_float();
float cos_theta = std::sqrt(u1);
float sin_theta = std::sqrt(1.0f - u1);
float phi = 2.0f * M_PI * u2;
return Vector3f(sin_theta * std::cos(phi),
sin_theta * std::sin(phi),
cos_theta);
}

小结#

本部分覆盖了计算机图形学最核心的数学根基:

  • 线性代数提供向量和矩阵运算这一最底层的表达工具
  • 几何变换把线性代数嵌入到几何世界,让我们可以用矩阵描述一切刚体和仿射运动
  • 投影几何把三维场景投射到二维屏幕,是渲染管线中最”魔法”的一步
  • 微积分则承担了曲线/曲面建模和渲染方程数值求解的重担

这些内容后面会在光栅化管线、光线追踪、几何建模、动画仿真等章节反复出现,建议在读后续章节前确保这里的推导都能独立复现一遍。

计算机图形学笔记(一):数学基础与理论根基
https://kyc001.github.io/posts/计算机图形学笔记一/
作者
kyc001
发布于
2025-07-22
许可协议
CC BY-NC-SA 4.0