图形学基础:4×4 矩阵
齐次坐标、存储约定、基本变换、乘法与合成顺序、正交矩阵、MVP
这是我「图形学基础」系列的第二篇。上一篇聊 3D 向量时留了个悬念:点和向量到底怎么从数学上彻底分开?本篇的主角 4×4 矩阵就来兑现这个承诺——顺便把图形学里所有的空间变换,统统收编成「一次矩阵乘法」。
4×4 矩阵是图形学里表达空间变换的通用语言:物体的平移、旋转、缩放,相机的投影,最终全部归结为若干个 4×4 矩阵的乘法。 本篇聚焦:为什么偏偏是 4×4、矩阵在内存里怎么存怎么读、变换如何组合,以及转置 / 行列式 / 逆 / 正交这些概念为什么值得在面试前多看一眼。
目录
- 一、为什么偏偏是 4×4:齐次坐标
- 二、矩阵怎么读:行、列与内存布局
- 三、三大基本变换:平移、缩放、旋转
- 四、矩阵乘法:一条从右往左的流水线
- 五、转置、行列式、逆矩阵
- 六、正交矩阵:旋转矩阵的免费午餐
- 七、特征值与特征向量
- 八、坐标空间变换链:MVP
- 九、核心概念速查
一、为什么偏偏是 4×4:齐次坐标
1.1 平移:3×3 表达不了的加法
无论你多喜欢 3×3 矩阵,它都干不了一件事:平移。
3×3 矩阵乘向量只能表达线性变换——旋转、缩放、剪切、反射。线性变换有条铁律:原点必须待在原地不动。旋转像拧魔方,缩放像拉面团,剪切像推倒一摞扑克牌,原点始终是钉死的钉子。而平移是纯粹的加法:
它要把整个空间连根拔起搬走,连原点都不放过。加法写不成「矩阵乘向量」的形式——于是我们引入第 4 个分量 ,把三维点扩展成四维齐次坐标 (Homogeneous Coordinates):
1.2 引入 w 分量:把加法塞回乘法
解决方案简单又优雅:多一个分量,就多一列可以藏私货。平移量塞进矩阵的第 4 列:
当左上角 是单位阵时,结果就是 ——纯平移。加法就这样被「伪装」成了乘法。
1.3 w 的两种语义:点与方向
不只是个补丁,它正好兑现了上一篇的承诺——从数学上区分点和方向:
| 值 | 含义 | 例子 |
|---|---|---|
| 点 (Point):受平移影响 | 顶点坐标 | |
| 方向 (Direction):不受平移影响 | 法向量、方向向量 | |
| 透视投影后的点,需透视除法 还原 | 投影矩阵输出 |
想想也合理:把整座城市向东平移一公里,「北」这个方向并不会因此变成「东北」——方向天生对平移免疫,所以它的 ;而房子的位置确实挪了,所以点是 。
面试关键点:用 区分「方向」和「点」,是齐次坐标的核心技巧。对应到代码就是两个接口:
transformDirection()按 (忽略平移列),transformPoint()按 (含平移),见 3.4 节。
4×4 的真正好处在于:平移、旋转、缩放、投影全部统一为一种运算——矩阵乘法,而且可以任意串联合并成一个矩阵。一百万个顶点各自做十步变换,不如先把十步合成一步,再让每个顶点只乘一次。
二、矩阵怎么读:行、列与内存布局
本节是面试最高频的混淆点,没有之一。先记住总纲:「列向量 / 行向量」是数学约定,「列主序 / 行主序」是内存布局——两组概念互相独立,却总被搅在一起讨论。
2.1 每一列就是一个基向量
在列向量约定下,矩阵 的第 列就是「原始基向量 变换后的落点」:
可以把它想象成给坐标系拍「变身后的合影」:x、y、z 三根轴原本分别朝向 、、,变换后各自落到哪里,就把落点按列写进矩阵。
例如纯旋转矩阵的左上 ,三列就是变换后的 X/Y/Z 三个坐标轴方向。所以想读「物体朝向」,取矩阵前三列即可——一个运算都不用做。
2.2 列向量 vs 行向量:一对数学约定
这是约定 (Convention),不是存储格式。它决定的是「矩阵乘向量」怎么写:
列向量约定 (本文 / OpenGL / GLM): 行向量约定 (DirectX 默认 / 部分教材):
[ m00 m01 m02 m03 ] [x] [ x y z 1 ] [ m00 m10 m20 m30 ]
[ m10 m11 m12 m13 ] × [y] [ m01 m11 m21 m31 ]
[ m20 m21 m22 m23 ] [z] [ m02 m12 m22 m32 ]
[ m30 m31 m32 m33 ] [1] [ m03 m13 m23 m33 ]
M × v vᵀ × Mᵀ
写法:M * v 写法:v * M
组合:从右到左执行 组合:从左到右执行
| 对比项 | 列向量约定(本文) | 行向量约定 |
|---|---|---|
| 变换写法 | ||
| 组合顺序 | ,先 S 后 R 后 T | ,先 S 后 R 后 T |
| 平移位置 | 矩阵第 4 列(上三行) | 矩阵第 4 行(前三列) |
| 互转关系 | 列约定的 = 行约定的 | — |
⚠️ 同一个数学变换,两种约定下的矩阵互为转置。 跨引擎搬代码(如 GLM → DXMath)时这是第一大坑:公式抄对了,矩阵却是「镜像」的。读任何资料前,先看它用哪种约定。
本文与 OpenGL / GLM / PBRT 一致,统一采用列向量约定,所有公式按 推导。
2.3 列主序 vs 行主序:一对内存布局
矩阵在内存里终究是 16 个连续的数。同一列的元素挨在一起存,就叫列主序 (Column-Major);同一行挨在一起,就叫行主序 (Row-Major)。这是纯工程概念。
列主序下,数组 m[16] 的布局长这样:
col0 col1 col2 col3 ← 数学列
┌─────┬─────┬─────┬─────┐
row0 │ m[0]│ m[4]│ m[8]│m[12]│ ← 数学第 0 行
row1 │ m[1]│ m[5]│ m[9]│m[13]│ ← 数学第 1 行
row2 │ m[2]│ m[6]│m[10]│m[14]│ ← 数学第 2 行
row3 │ m[3]│ m[7]│m[11]│m[15]│ ← 数学第 3 行
└─────┴─────┴─────┴─────┘
连续 连续 连续 连续
即:
m[0..3]= 第 0 列(m00, m10, m20, m30)m[4..7]= 第 1 列m[8..11]= 第 2 列m[12..15]= 第 3 列
下标公式:
行主序(C/C++ 原生二维数组、DirectX Math)则反过来:同一行连续,下标是 m[row*4+col]。
2.4 两组概念,互相独立(重要澄清)
| 列主序 | 行主序 | |
|---|---|---|
| 是什么 | 内存存储布局 | 内存存储布局 |
| 决定 | 数组下标怎么算 | 数组下标怎么算 |
| 列向量约定 | 行向量约定 | |
|---|---|---|
| 是什么 | 数学约定 | 数学约定 |
| 决定 | 矩阵×向量的写法、平移放第几列/行 | 同左 |
这两组是正交的! 理论上可以任意组合(列主序+列向量、行主序+行向量、甚至行主序+列向量)。OpenGL/GLM 用「列主序 + 列向量」,DirectX Math 默认「行主序 + 行向量」。巧合的是,当存储约定与向量约定「配套」时(列主序配列向量、行主序配行向量),同一份 16 个浮点数的二进制内容表示的数学矩阵恰好相同——所以 GLM 和 DXMath 的常量数组常常长得一模一样,只是解释方式不同。这正是新手最容易绕晕的地方。
本文统一采用 「列主序 + 列向量」,与 OpenGL / GLM / PBRT 完全一致。
2.5 代码表示:Matrix4
一个典型的矩阵类型用模板结构体实现,和上一篇的 Vec3 是一个套路:
template <typename T>
struct Matrix4 {
// 列主序存储:m[0..3] 是数学第 0 列,m[4..7] 第 1 列,m[8..11] 第 2 列,m[12..15] 第 3 列
T m[16];
// 数学记法 M[row][col] 的读写入口;注意参数顺序是 (col, row)!
T& operator()(int col, int row) { return m[col * 4 + row]; }
const T& operator()(int col, int row) const { return m[col * 4 + row]; }
static Matrix4 identity(); // 单位矩阵
Matrix4 transposed() const; // 转置
T determinant() const; // 行列式
Matrix4 inverted() const; // 逆矩阵(奇异时返回单位矩阵兜底)
bool isOrthogonal(T eps) const; // 是否正交矩阵
Vec3<T> transformPoint(const Vec3<T>& p) const; // 按 w=1 变换点(含平移)
Vec3<T> transformDirection(const Vec3<T>& d) const; // 按 w=0 变换方向(不含平移)
};
using Matrix4f = Matrix4<float>;
using Matrix4d = Matrix4<double>;
要点:
- 16 个数存成一维数组
m[16],列主序; operator()(col, row)负责把「数学下标」翻译成「内存下标」——读代码时切勿按惯性当成(row, col)!- 工厂函数
translation() / scaling() / rotationX/Y/Z()用来造基本变换(第三节); transformPoint / transformDirection对应 / 两种语义(见 3.4)。
using namespace gm;
Matrix4f I = Matrix4f::identity(); // 单位矩阵:乘谁都等于没乘
Matrix4f T = translation(Vec3f(1.0f, 2.0f, 3.0f));
Vec3f p = T.transformPoint(Vec3f(1, 1, 1)); // (2, 3, 4)
三、三大基本变换:平移、缩放、旋转
3.1 平移矩阵 translation()
平移量住在第 4 列的上三行(列向量约定),列主序下恰好是 m[12]、m[13]、m[14]。所以调试时想知道「这个矩阵把物体挪到哪了」,直接看这三个数就行。典型实现:
template <typename T>
Matrix4<T> translation(const Vec3<T>& t) {
Matrix4<T> r = Matrix4<T>::identity();
r(3, 0) = t.x; // M[0][3] = tx
r(3, 1) = t.y; // M[1][3] = ty
r(3, 2) = t.z; // M[2][3] = tz
return r;
}
注意这里 r(3, 0) 的含义:第 3 列第 0 行——参数是 (col, row),别看反了。
3.2 缩放矩阵 scaling()
一个对角矩阵,缩放因子躺在对角线上,没什么花头:
template <typename T>
Matrix4<T> scaling(const Vec3<T>& s) {
Matrix4<T> r = Matrix4<T>::identity();
r(0, 0) = s.x; // M[0][0]
r(1, 1) = s.y; // M[1][1]
r(2, 2) = s.z; // M[2][2]
return r;
}
using namespace gm;
Matrix4f S = scaling(Vec3f(2.0f, 3.0f, 4.0f));
Vec3f ps = S.transformPoint(Vec3f(1, 1, 1)); // (2, 3, 4)
// 缩放矩阵的行列式 det = sx*sy*sz = 24(第五节会用到)
3.3 旋转矩阵 rotationX / rotationY / rotationZ
右手坐标系,角度为弧度,逆时针为正(从轴的正方向看向原点)。
绕 X 轴(Y→Z 平面内旋转):
绕 Y 轴(Z→X 平面内旋转):
绕 Z 轴(X→Y 平面内旋转):
记忆窍门:绕哪个轴旋转,该轴对角元素就是 1;剩下两轴构成 的 。注意 中 的符号位置与 相反(因为叉积循环 X×Y=Z、Y×Z=X、Z×X=Y 的方向)。
以 rotationZ 为例的典型实现:
template <typename T>
Matrix4<T> rotationZ(T radians) {
Matrix4<T> r = Matrix4<T>::identity();
const T c = std::cos(radians);
const T s = std::sin(radians);
r(0, 0) = c; r(1, 0) = -s; // M[0][0]=c, M[0][1]=-s
r(0, 1) = s; r(1, 1) = c; // M[1][0]=s, M[1][1]=c
return r;
}
using namespace gm;
Matrix4f Rz = rotationZ(static_cast<float>(M_PI) / 2.0f); // 绕 Z 转 90°
Vec3f rx = Rz.transformDirection(Vec3f(1, 0, 0)); // (0, 1, 0) X 轴→Y 轴
3.4 点与方向的分野:transformPoint / transformDirection
第一节说 是点、 是方向,落到代码里就是下面这对函数——它们的差别只有一行:
// w = 1:第 4 列(平移)参与运算 —— 变换「点」
template <typename T>
Vec3<T> Matrix4<T>::transformPoint(const Vec3<T>& p) const {
return Vec3<T>(
m[0] * p.x + m[4] * p.y + m[8] * p.z + m[12],
m[1] * p.x + m[5] * p.y + m[9] * p.z + m[13],
m[2] * p.x + m[6] * p.y + m[10] * p.z + m[14]);
}
// w = 0:无视第 4 列,只留左上 3×3 的线性部分 —— 变换「方向」
template <typename T>
Vec3<T> Matrix4<T>::transformDirection(const Vec3<T>& d) const {
return Vec3<T>(
m[0] * d.x + m[4] * d.y + m[8] * d.z,
m[1] * d.x + m[5] * d.y + m[9] * d.z,
m[2] * d.x + m[6] * d.y + m[10] * d.z);
}
尾部那三个 + m[12/13/14],就是 与 在代码里的全部区别。
using namespace gm;
Matrix4f T = translation(Vec3f(1, 2, 3));
Vec3f p(1, 1, 1);
Vec3f pt = T.transformPoint(p); // (2, 3, 4) 点:跟着走
Vec3f pd = T.transformDirection(p); // (1, 1, 1) 方向:平移免疫
易错点:把顶点位置错喂给
transformDirection(或反过来),模型会「散架」或「飘走」。变换法线、光线方向用 direction;变换顶点、位置用 point。
四、矩阵乘法:一条从右往左的流水线
4.1 乘法规则:行点列
两个 4×4 矩阵相乘 ,每个元素都是「A 的行」点「B 的列」:
列主序下的典型实现就是教科书式的三重循环:
Matrix4 operator*(const Matrix4& o) const {
Matrix4 r;
for (int col = 0; col < 4; ++col)
for (int row = 0; row < 4; ++row) {
T sum = 0;
for (int k = 0; k < 4; ++k)
sum += (*this)(k, row) * o(col, k); // C[row][col] = Σ A[row][k]·B[k][col]
r(col, row) = sum;
}
return r;
}
一个矩阵乘法 = 64 次乘加。CPU 看着嫌多,GPU 每帧做几百万次,眼都不眨。
4.2 不可交换:先穿袜子还是先穿鞋
| 性质 | 公式 | 说明 |
|---|---|---|
| 结合律 | ✅ 可任意加括号 | |
| 不满足交换律 | ❌ 顺序至关重要 | |
| 单位矩阵 | — |
顺序为什么重要?生活里也一样:先穿袜子再穿鞋没问题,先穿鞋再穿袜子就很离谱——每一步都「正确」,但顺序一换,结果全变。数值例子(绕 Z 转 90° vs 平移 (1,0,0)):
- :先把 平移到 ,再绕 Z 转 90° →
- :先把 转到 ,再平移 →
同样的两个矩阵,乘的顺序一换,结果完全不同。
4.3 TRS 合成:从右往左读
列向量约定下写成 ,最右边的矩阵先作用于向量,从右到左依次执行:
v ──S──> 缩放后的 v ──R──> 旋转后 ──T──> 平移后 = 最终位置
▲
写成代码:TRS * v
(写在最左的 T 反而最后执行)
像函数嵌套 :最贴近 的 先执行。典型用法:先缩放物体到目标尺寸,再旋转朝向,最后平移到世界坐标——三步合成一个矩阵:
using namespace gm;
// 列向量约定:T*R*S*v 表示先缩放、再旋转、最后平移
Matrix4f TRS = translation(Vec3f(10, 0, 0))
* rotationZ(static_cast<float>(M_PI) / 2.0f)
* scaling(Vec3f(1, 1, 1));
// 取点 (1,0,0):绕Z转90° → (0,1,0),再平移(10,0,0) → (10,1,0)
Vec3f combo = TRS.transformPoint(Vec3f(1, 0, 0)); // (10, 1, 0)
⚠️ 行向量约定正好相反:,代码写成
v * S * R * T,从左到右执行。两边「执行顺序」一致,但矩阵书写顺序相反。面试被问「矩阵相乘顺序」,先声明你用哪种约定,再答执行顺序,稳。
口诀:「列向量约定,从右往左读——离向量最近的先动手。」
五、转置、行列式、逆矩阵
5.1 转置 transposed():行列互换
转置就是把矩阵沿对角线「翻面」: 的第 行第 列 = 的第 行第 列。
Matrix4 transposed() const {
Matrix4 r;
for (int col = 0; col < 4; ++col)
for (int row = 0; row < 4; ++row)
r(col, row) = (*this)(row, col); // 行列对调
return r;
}
两条重要性质:
用途集中在三处:
- 正交矩阵求逆:若 正交,则 (第六节的主角);
- 列/行向量约定互转:两种约定下的同一变换互为转置;
- 法线变换:变换法线要用模型矩阵左上 的逆转置(法线变换专题再展开)。
5.2 行列式 determinant():体积的缩放因子
计算上,4×4 行列式按第 0 行拉普拉斯展开,降成四个 3×3 子行列式(3×3 再用萨鲁斯法则):
T determinant() const {
// 按第 0 行展开:det = Σ (-1)^j · M[0][j] · 去掉第 0 行第 j 列的 3×3 余子式
T det = 0;
for (int j = 0; j < 4; ++j)
det += ((j % 2 == 0) ? T(1) : T(-1)) * (*this)(j, 0) * minor3(j, 0);
return det;
}
几何意义比公式重要得多:想象一块单位体积的橡皮泥塞进变换机器, 就是出来时的体积。
- :保持手性(纯旋转/缩放,没翻面);
- :含镜像反射——像把一张印章盖反过来,「翻面了」;
- :空间被压扁(降维成一张纸甚至一条线),信息丢了,不可逆——一张纸压不出原来的立体橡皮泥。
| 矩阵 | 含义 | |
|---|---|---|
| 单位阵 | 体积不变 | |
| 缩放 | 各轴缩放因子之积 | |
| 旋转 | 保体积、保手性 | |
| 反射 | 翻面,体积不变 |
using namespace gm;
Matrix4f::identity().determinant(); // 1
scaling(Vec3f(2, 3, 4)).determinant(); // 24 = 2*3*4
rotationZ(0.5f).determinant(); // 1(旋转保体积)
5.3 逆矩阵 inverted():撤销一个变换
如果矩阵 是「做一件事」,逆矩阵 就是「撤销这件事」:。可逆的充要条件是 ——被压扁的空间回不去。
通用求法是伴随矩阵法 (Adjugate / Cofactor):
Matrix4 inverted() const {
const T det = determinant();
if (std::fabs(det) <= std::numeric_limits<T>::epsilon())
return identity(); // 奇异矩阵兜底:压扁的橡皮泥回不去,退回单位矩阵
Matrix4 r;
const T invDet = T(1) / det;
for (int col = 0; col < 4; ++col)
for (int row = 0; row < 4; ++row)
r(col, row) = cofactorSign(row, col) * minor3(row, col) * invDet;
// 注意余子式按下标「转置」放回 —— 这正是伴随矩阵的定义
return r;
}
实话实说:通用 4×4 伴随矩阵法要算 16 个 3×3 行列式,计算量不小。图形学里更常用高斯消元,以及对仿射变换、正交矩阵的特化求逆——后者是第六节的内容,也是面试最爱。
验证:逆矩阵乘回原矩阵应得到单位阵。
using namespace gm;
Matrix4f A = translation(Vec3f(1, 2, 3));
Matrix4f Ainv = A.inverted();
Matrix4f prod = A * Ainv; // == I(单位矩阵)
六、正交矩阵:旋转矩阵的免费午餐
正交矩阵大概是图形学里「性价比」最高的概念:旋转矩阵都是正交矩阵,而正交矩阵的逆就是转置——上百次乘法的求逆,瞬间变成 16 个元素挪个位置。
6.1 定义:Q 的转置就是它的逆
若方阵 满足
则称 为正交矩阵,且
bool isOrthogonal(T eps = T(1e-5)) const {
Matrix4 prod = (*this) * transposed(); // Q · Qᵀ
return prod == identity(); // 实际比较按 eps 容差
}
6.2 等价刻画:四种说法一回事
以下四条任选其一即可判定正交:
- (列向量组两两正交且长度为 1)
- (行向量组两两正交且长度为 1)
- 行(列)向量组构成标准正交基 (Orthonormal Basis)
第 4 条最有画面感:矩阵的每一列都是单位向量、且任意两列互相垂直——这个矩阵里住着一组标准正交基。
6.3 几何意义:保长、保角、保体积
正交变换不改变向量的长度、两向量的夹角、空间的体积:
几何上,正交变换只可能是刚体旋转或反射(含镜像)——没有缩放、没有剪切,像真正的刚体,只转不改大小。
6.4 从行列式看:旋转还是反射
由 得 ,故:
| 含义 | |
|---|---|
| 纯旋转 (Proper Rotation),保持手性 | |
| 含反射 (Reflection),翻转手性 |
6.5 旋转矩阵求逆 = 转置
及其任意组合都是正交矩阵(),因此:
转置只是 个元素换位置,而通用 4×4 求逆要上百次乘法。动画、骨骼蒙皮、相机视图矩阵这些每帧大量求逆的场景,这个优化是硬通货。
using namespace gm;
Matrix4f Rz = rotationZ(static_cast<float>(M_PI) / 2.0f);
Rz.isOrthogonal(); // true —— 旋转矩阵是正交矩阵
translation(Vec3f(1, 2, 3)).isOrthogonal(); // false —— 平移矩阵不是正交矩阵
注意:纯平移矩阵不是正交矩阵(它的第 4 列 不是单位向量)。但「旋转 + 平移」组成的刚体变换仍有廉价求逆:旋转部分转置、平移部分取反,再重新组装——完整 4×4 仿射求逆可分解为「左上 求逆 + 平移修正」。
6.6 速查:常见变换谁是正交的
| 变换 | 正交? | 求逆方式 | |
|---|---|---|---|
| 旋转 | ✅ | ||
| 反射 | ✅ | ||
| 纯平移 | ❌ | 平移量取反 | |
| 非均匀缩放 | ❌ | 对角元素取倒数 | |
| 旋转 + 平移(刚体) | ❌(4×4) | 旋转转置 + 平移修正 | |
| 投影矩阵 | ❌ | — | 通常不求逆,或用专门公式 |
七、特征值与特征向量
7.1 定义:方向不变的方向
对方阵 ,若存在标量 和非零向量 满足
则称 为 的特征值 (Eigenvalue), 为对应的特征向量 (Eigenvector)。
7.2 几何意义:倔强的方向与拉伸倍数
大多数向量经过变换后会「拐弯」,但总有几个方向特别倔强——变换前后方向不变(或恰好反向),只是被拉长或压短。这些倔强的方向就是特征向量,拉长压短的倍数就是特征值:
- :沿该方向拉伸;
- :沿该方向压缩;
- :反向缩放(含反射成分);
- :该方向被压扁到零(矩阵奇异)。
7.3 旋转矩阵的特征值:旋转轴的秘密
3D 旋转矩阵的特征值很有特点:必有一个 ——对应的特征向量正是旋转轴(旋转中岿然不动的方向);另两个是一对共轭复数 (模为 1,对应旋转平面)。这也给出了「从旋转矩阵反提取旋转轴 / 旋转角」的思路。
7.4 数值解法:幂迭代
精确求一般 4×4 矩阵的特征值,需求解 4 次特征多项式 ,公式繁琐到没人手写。实用做法是数值迭代——幂迭代法 (Power Iteration) 求主特征值(绝对值最大的那个):
原理:对任意非零向量 反复乘 , 会逐渐「对齐」到主特征向量方向——就像反复对折一张纸,不管初始多歪,最终都会对齐折痕。对齐后的缩放比即为主特征值。
b₀ (任意非零)
↓ 归一化
v₀ = normalize(b₀)
↓ 反复迭代
v_{k+1} = normalize(A v_k)
λ_{k+1} ≈ v_k^T A v_k (Rayleigh 商近似)
↓ 收敛
λ → 主特征值
Matrix4f M = /* 某矩阵 */;
float lambda = M.maxEigenvalue(); // 默认迭代 50 次
float lambda2 = M.maxEigenvalue(200, 1e-8f); // 自定义迭代次数与收敛阈值
实现要点:
- 只取左上 线性部分(忽略平移列),聚焦线性变换本身;
- 用 Rayleigh 商 估计特征值;
- 收敛条件:相邻两次估计差值 ;
- 限制:要求矩阵有占优主特征值(),否则可能不收敛或收敛很慢。
补充:对称矩阵的特征值全是实数、特征向量相互正交,可用雅可比 (Jacobi) 对角化精确求解。图形学里的惯性张量、协方差矩阵(PCA)常用此法;幂迭代则是不要求对称性的快速估计。
八、坐标空间变换链:MVP
一个顶点从「出生」(建模软件里定下的局部坐标)到「上屏」(变成屏幕上的像素),要闯过一道道关卡,而每一道关卡都是一次矩阵乘法。
8.1 一个顶点的旅程
局部坐标 世界坐标 相机/观察坐标 裁剪坐标
(Local/Object) ──M──> (World) ──V──> (View/Camera) ──P──> (Clip)
│
透视除法 ÷w
▼
NDC 坐标
│
视口变换
▼
屏幕坐标
合成矩阵(列向量约定,从右到左):
| 矩阵 | 作用 | 典型构造 |
|---|---|---|
| Model (M) | 局部 → 世界:物体的平移/旋转/缩放 | translation()*rotationY()*scaling() |
| View (V) | 世界 → 相机:把相机移到原点、看向 -Z | 相机逆变换() |
| Projection (P) | 相机 → 裁剪:定义视锥、透视/正交 | 透视:fov/aspect/near/far;正交:左右上下前后 |
8.2 透视投影 vs 正交投影
- 透视投影 (Perspective):近大远小, 分量不再恒为 1(含 ),需透视除法。平行线会汇聚到灭点——像站在铁路上望远处,两条铁轨在天边交汇。模拟人眼,主流 3D 游戏都用它。
- 正交投影 (Orthographic):无近大远小, 恒为 1,平行线仍平行——像工程制图,尺寸就是一切。常用于 CAD、2D 游戏、阴影贴图。
8.3 View 矩阵:lookAt 的思路
视图矩阵的本质是「把相机请回原点、朝向 」的刚体变换,等于相机自身世界变换矩阵的逆。与其让相机走向世界,不如让整个世界反向搬运过来——两种说法数学等价:
由于相机的旋转部分是正交的,求逆直接用转置(第 6 节的免费午餐在这里兑现)。
8.4 拼装一个完整的 MVP
using namespace gm;
// Model:把物体搬到世界 (1,2,3),绕 Y 转 45°,整体放大 2 倍
Matrix4f M = translation(Vec3f(1, 2, 3))
* rotationY(static_cast<float>(M_PI) / 4.0f)
* scaling(Vec3f(2, 2, 2));
// View:相机世界变换的逆(旋转部分正交,可用转置加速求逆)
Matrix4f V = cameraWorld.inverted();
// Projection:透视投影(fov / aspect / near / far),推导留给后续相机篇
Matrix4f P = perspective(fovY, aspect, near, far);
// 合成 MVP:列向量约定下 P·V·M,M 离向量最近、最先执行
Matrix4f MVP = P * V * M;
Vec3f worldP = M.transformPoint(localP); // 也可以分步:局部 → 世界
记忆:MVP 写法 中,最靠近向量的 先执行——与第四节「从右到左」完全一致。三个矩阵预先合体成一个,顶点着色器里每个顶点只乘一次,这正是「合成」的意义。
九、核心概念速查
齐次坐标 / 4×4
- 平移是加法,3×3 表达不了;引入 (点 、方向 )统一为矩阵乘法。
transformPoint()含平移(),transformDirection()不含平移()。
存储 vs 约定(最易混淆)
- 列主序/行主序是内存布局;列向量/行向量是数学约定,两者正交。
- 本文:列主序 + 列向量(同 OpenGL/GLM)。下标
m[col*4+row],operator()(col,row)返回 。 - 平移在第 4 列(列向量约定);若在第 4 行则是行向量约定。
变换顺序
- 列向量 : 从右到左,先 S 后 R 后 T。
- 行向量 : 从左到右,执行顺序相同,矩阵书写顺序相反。
- 矩阵乘法不可交换 ,但满足结合律。
行列式
- = 体积缩放因子; 奇异不可逆(降维); 含反射。
- 旋转 ,缩放 。
逆矩阵
- 通用:伴随矩阵法 ;实用中更多用高斯消元或特化求逆。
- 正交矩阵:(转置即可,旋转矩阵高频优化)。
- 不可逆。
正交矩阵(重点)
- ,行/列构成标准正交基。
- 保长、保角、保体积;(+1 旋转,−1 含反射)。
- 旋转矩阵是正交矩阵 → 求逆只需转置。平移/缩放/投影不是正交矩阵。
特征值
- :特征向量方向不变, 是缩放因子。
- 旋转矩阵 对应旋转轴。
maxEigenvalue()用幂迭代法求主特征值(数值近似);精确解需解 4 次多项式,不实用。- 对称矩阵特征值全实数、特征向量正交,可用雅可比对角化。
MVP
- , 先执行(从右到左)。
- Model:局部→世界;View:世界→相机(=相机世界矩阵的逆);Projection:相机→裁剪(透视/正交)。
必记口诀
- 点 ,方向 :点会跟着平移走,方向对平移免疫。
- 每一列是变身后的基向量:读朝向,取前三列。
- 列向量约定,从右往左读:,先缩放、再旋转、最后平移。
- 是体积:正 = 保手性,负 = 翻面,零 = 压扁不可逆。
- 旋转的逆 = 转置:正交矩阵的免费午餐。
- MVP 离向量最近的先执行:。
评论
由 Giscus 驱动,需使用 GitHub 账号登录后评论。