GAMES101 图形学Transform全解:从几何意义到完整推导
GAMES101 图形学Transform全解:从几何意义到完整推导
前言:学变换,先确定「参考系」
图形学里的变换,从来都不是背下 4 × 4 4×4 4×4 矩阵就完事了。大部分理解偏差和计算错误,是因为没盯紧当前坐标到底是「相对谁」描述的。
从模型的顶点数据,到最终屏幕上的像素,一个顶点会依次穿过6个完全不同的坐标空间:局部空间(local) → 世界空间(world) → 观察空间(view) → 裁剪空间(clip) → 标准化设备坐标(NDC) → 屏幕空间(screen)。而我们常说的MVP矩阵,正是完成前4步空间转换的核心工具,整条渲染管线最核心的公式,就是这一行:
p c l i p = P ⋅ V ⋅ M ⋅ p l o c a l p_{clip} = P \cdot V \cdot M \cdot p_{local} pclip=P⋅V⋅M⋅plocal
本文会把这个公式的每一步拆解得明明白白,从几何意义到公式推导,从易混点辨析到引擎实践,带你理解图形学变换的核心逻辑。
一、Model 矩阵:把模型从局部空间「搬进」世界空间
Model 矩阵(简称 M 矩阵)的核心任务:将模型局部坐标系下的顶点坐标,转换为世界坐标系下的坐标,公式表达为:
p w o r l d = M ⋅ p l o c a l p_{world} = M \cdot p_{local} pworld=M⋅plocal
1.1 前置知识:为什么要用4维齐次坐标?
在正式讲M矩阵之前,必须先解决一个核心问题:3维空间的点,为什么非要用4维向量和 4 × 4 4×4 4×4 矩阵来计算?
答案很简单:平移变换无法用 3 × 3 3×3 3×3 线性变换矩阵表示。
线性变换有一个核心约束:必须满足「原点映射到原点」。而平移变换的本质,是把空间中所有点都加上一个偏移量,比如把点 ( x , y , z ) (x,y,z) (x,y,z) 平移到 ( x + t x , y + t y , z + t z ) (x+t_x,y+t_y,z+t_z) (x+tx,y+ty,z+tz) ,这个操作会让原点 ( 0 , 0 , 0 ) (0,0,0) (0,0,0) 变成 ( t x , t y , t z ) (t_x,t_y,t_z) (tx,ty,tz) ,天然不满足线性变换的约束。
为了把「平移+旋转+缩放」这些仿射变换,统一成线性变换的形式,我们引入了齐次坐标:
- 3维空间中的点,扩展为4维向量 ( x , y , z , 1 ) (x,y,z,1) (x,y,z,1) , w w w 分量为1表示「点」;
- 3维空间中的方向向量,扩展为4维向量 ( x , y , z , 0 ) (x,y,z,0) (x,y,z,0) , w w w 分量为0表示「向量」(平移对方向无影响)。
基于齐次坐标,我们就能用 4 × 4 4×4 4×4 矩阵,把所有仿射变换统一成「矩阵×向量」的形式,这也是整个变换体系的数学基础。
1.2 基础变换矩阵的完整形式与推导
M 矩阵本质上是「平移(T)、旋转( R)、缩放(S)」三个基础变换的组合,我们先把每个基础变换的矩阵形式和几何意义理清。
(1)缩放矩阵 S
缩放变换的核心,是沿 x 、 y 、 z x、y、z x、y、z 三个轴分别对坐标进行缩放,缩放因子为 s x , s y , s z s_x,s_y,s_z sx,sy,sz ,其齐次坐标下的矩阵形式为:
S ( s x , s y , s z ) = [ s x 0 0 0 0 s y 0 0 0 0 s z 0 0 0 0 1 ] S(s_x,s_y,s_z) = \begin{bmatrix} s_x & 0 & 0 & 0 \\ 0 & s_y & 0 & 0 \\ 0 & 0 & s_z & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} S(sx,sy,sz)= sx0000sy0000sz00001
推导验证:对局部点 p l o c a l = ( x , y , z , 1 ) T p_{local}=(x,y,z,1)^T plocal=(x,y,z,1)T 做缩放变换:
S ⋅ p l o c a l = [ s x 0 0 0 0 s y 0 0 0 0 s z 0 0 0 0 1 ] ⋅ [ x y z 1 ] = [ s x x s y y s z z 1 ] S \cdot p_{local} = \begin{bmatrix} s_x & 0 & 0 & 0 \\ 0 & s_y & 0 & 0 \\ 0 & 0 & s_z & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \cdot \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} = \begin{bmatrix} s_x x \\ s_y y \\ s_z z \\ 1 \end{bmatrix} S⋅plocal= sx0000sy0000sz00001 ⋅ xyz1 = sxxsyyszz1
完全符合缩放的几何预期,当 s x = s y = s z = 1 s_x=s_y=s_z=1 sx=sy=sz=1 时,为单位矩阵,无缩放效果。
(2)旋转矩阵 R
旋转是三个基础变换里最复杂的,通常先从绕坐标轴的旋转入手,后续会用罗德里格斯公式(Rodrigues’ rotation formula)扩展到绕任意轴旋转。
关于罗德里格斯公式的详细推导过程,可参考另一篇专门讨论此内容的文章 罗德里格斯公式(Rodrigues’ rotation formula)推导。
图形学中默认右手定则定义旋转正方向:右手握住旋转轴,大拇指指向轴的正方向,四指弯曲的方向就是旋转的正方向。
- 绕 x x x 轴旋转 θ θ θ 角的旋转矩阵:
R x ( θ ) = [ 1 0 0 0 0 cos θ − sin θ 0 0 sin θ cos θ 0 0 0 0 1 ] R_x(\theta) = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & \cos\theta & -\sin\theta & 0 \\ 0 & \sin\theta & \cos\theta & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} Rx(θ)= 10000cosθsinθ00−sinθcosθ00001
- 绕y轴旋转θ角的旋转矩阵(注意符号与 x 、 z x、z x、z 轴相反,源于右手系坐标轴叉乘顺序):
R y ( θ ) = [ cos θ 0 sin θ 0 0 1 0 0 − sin θ 0 cos θ 0 0 0 0 1 ] R_y(\theta) = \begin{bmatrix} \cos\theta & 0 & \sin\theta & 0 \\ 0 & 1 & 0 & 0 \\ -\sin\theta & 0 & \cos\theta & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} Ry(θ)= cosθ0−sinθ00100sinθ0cosθ00001
- 绕z轴旋转θ角的旋转矩阵:
R z ( θ ) = [ cos θ − sin θ 0 0 sin θ cos θ 0 0 0 0 1 0 0 0 0 1 ] R_z(\theta) = \begin{bmatrix} \cos\theta & -\sin\theta & 0 & 0 \\ \sin\theta & \cos\theta & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} Rz(θ)= cosθsinθ00−sinθcosθ0000100001
核心性质:旋转矩阵是正交矩阵,满足 R T = R − 1 R^T = R^{-1} RT=R−1 ,即旋转矩阵的转置等于其逆矩阵。这个性质在后续 View 矩阵的推导中至关重要。
(3)平移矩阵 T
平移变换的核心,是给 x 、 y 、 z x、y、z x、y、z 三个轴分别加上偏移量 t x , t y , t z t_x,t_y,t_z tx,ty,tz ,其齐次坐标下的矩阵形式为:
T ( t x , t y , t z ) = [ 1 0 0 t x 0 1 0 t y 0 0 1 t z 0 0 0 1 ] T(t_x,t_y,t_z) = \begin{bmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{bmatrix} T(tx,ty,tz)= 100001000010txtytz1
推导验证:对点 p = ( x , y , z , 1 ) T p=(x,y,z,1)^T p=(x,y,z,1)T 做平移变换:
T ⋅ p = [ 1 0 0 t x 0 1 0 t y 0 0 1 t z 0 0 0 1 ] ⋅ [ x y z 1 ] = [ x + t x y + t y z + t z 1 ] T \cdot p = \begin{bmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{bmatrix} \cdot \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} = \begin{bmatrix} x+t_x \\ y+t_y \\ z+t_z \\ 1 \end{bmatrix} T⋅p= 100001000010txtytz1 ⋅ xyz1 = x+txy+tyz+tz1
完全符合平移的几何预期;而对方向向量 ( x , y , z , 0 ) T (x,y,z,0)^T (x,y,z,0)T ,平移后结果不变,也符合「平移不影响方向」的物理意义。
1.3 变换组合顺序:为什么必须是 M = T · R · S ?
在列向量右乘的约定下,M矩阵的标准组合形式是:
M = T ⋅ R ⋅ S M = T \cdot R \cdot S M=T⋅R⋅S
核心规则:矩阵乘法的执行顺序,是从右到左。也就是说,顶点会先执行缩放(S),再执行旋转( R),最后执行平移(T),这个顺序绝对不能随意交换。
为什么?因为矩阵乘法不满足交换律,交换顺序后,几何意义会完全改变。我们用一个最直观的反例就能说明:
例子:有一个点 p = ( 1 , 0 , 0 , 1 ) T p=(1,0,0,1)^T p=(1,0,0,1)T ,先绕 z z z 轴旋转 90 ° 90° 90°,再沿 x x x 轴平移 3 个单位;和先平移 3 个单位,再绕 z z z 轴旋转 90 ° 90° 90°,结果天差地别。
- 先旋转后平移(T·R):
- 旋转后点变为 ( 0 , 1 , 0 , 1 ) T (0,1,0,1)^T (0,1,0,1)T ,平移后变为 ( 3 , 1 , 0 , 1 ) T (3,1,0,1)^T (3,1,0,1)T
- 先平移后旋转(R·T):
- 平移后点变为 ( 4 , 0 , 0 , 1 ) T (4,0,0,1)^T (4,0,0,1)T ,旋转后变为 ( 0 , 4 , 0 , 1 ) T (0,4,0,1)^T (0,4,0,1)T
两个结果完全不同,这就是顺序的核心意义。而我们之所以固定「缩放→旋转→平移」的顺序,是为了避免变换之间的干扰:
- 先缩放,再旋转,不会让缩放影响旋转轴的方向;
- 最后平移,不会让平移被旋转和缩放改变偏移方向。
这个顺序本质上是在定义:模型局部坐标系如何完整地嵌入到世界坐标系中——缩放定义模型的大小,旋转定义模型局部轴在世界中的朝向,平移定义模型局部原点在世界中的位置。
1.4 绕世界轴还是局部轴?全看乘法位置
初学者最常问的一个问题:旋转到底是绕世界坐标系的轴,还是模型局部坐标系的轴?
答案不是固定的,完全取决于旋转矩阵乘在已有 M 矩阵的哪一侧,在列向量右乘的约定下:
- 在 M 矩阵右侧乘旋转矩阵: M n e w = M o l d ⋅ R n e w M_{new} = M_{old} \cdot R_{new} Mnew=Mold⋅Rnew
几何意义:绕模型当前的局部轴旋转。因为旋转先于M矩阵中已有的平移和旋转执行,是在模型局部空间中完成的。
- 在 M 矩阵左侧乘旋转矩阵: M n e w = R n e w ⋅ M o l d M_{new} = R_{new} \cdot M_{old} Mnew=Rnew⋅Mold
几何意义:绕世界坐标系的固定轴旋转。因为旋转后于M矩阵中已有的变换执行,是在世界空间中对整个模型的姿态做二次旋转。
1.5 引擎实践:层级变换与帧更新逻辑
很多人会误以为,物体的运动是靠矩阵不断连乘实现的,但实际引擎(比如 Unity、Unreal)的做法完全不是这样。
引擎的Transform组件,底层维护的不是一个 4 × 4 4×4 4×4 矩阵,而是模型当前帧的 p o s i t i o n position position(位置)、 r o t a t i o n rotation rotation(旋转)、 s c a l e scale scale(缩放)三个基础状态。每一帧渲染时,引擎会用这三个状态,直接重新构造当前帧的局部 M矩阵:
l o c a l M a t r i x = T ( p o s i t i o n ) ⋅ R ( r o t a t i o n ) ⋅ S ( s c a l e ) localMatrix = T(position) \cdot R(rotation) \cdot S(scale) localMatrix=T(position)⋅R(rotation)⋅S(scale)
如果模型有父节点,那么世界矩阵的计算方式为:
w o r l d M a t r i x = p a r e n t W o r l d M a t r i x ⋅ l o c a l M a t r i x worldMatrix = parentWorldMatrix \cdot localMatrix worldMatrix=parentWorldMatrix⋅localMatrix
这种做法的优势非常明显:
- 避免矩阵连乘带来的浮点误差积累;
- 方便编辑器直接修改位置、旋转、缩放参数,无需做矩阵逆运算;
- 完美适配场景的父子层级系统,层级关系清晰可控。
二、View矩阵:把世界坐标系,换成相机的「第一视角」
View 矩阵(简称 V 矩阵)是整个 MVP 里最容易被误解的部分,很多教程一句话带过:「View 变换就是把相机移到世界原点」。这句话不算错,但只说了一半,另一半才是理解的核心:我们不会真的移动相机,而是对整个世界做一个与相机位姿完全相反的变换,让所有点的坐标变成「相对相机」的描述。
2.1 View 变换的核心意义
经过M矩阵后,所有顶点都已经在世界坐标系下了,但我们渲染画面,是从相机的视角出发的。人眼看到的物体位置,从来不是「物体在世界里的绝对坐标」,而是「物体相对我们的位置和方向」,View 变换做的就是这件事。
View 变换的公式表达为:
p v i e w = V ⋅ p w o r l d p_{view} = V \cdot p_{world} pview=V⋅pworld
经过View变换后,坐标空间从世界空间切换到观察空间(相机空间),这个空间的原点不再是世界原点,而是相机的位置; x 、 y 、 z x、y、z x、y、z 轴也不再是世界坐标轴,而是相机自身的右、上、后方向轴。
2.2 核心推导:View 矩阵 = 相机位姿矩阵的逆
我们先定义:相机在世界坐标系中的位姿矩阵为 C C C ,这个 C C C 和模型的 M 矩阵完全等价——它描述了相机局部坐标系如何嵌入到世界坐标系中,同样可以拆分为平移和旋转:
C = T c ⋅ R c C = T_c \cdot R_c C=Tc⋅Rc
其中 T c T_c Tc 是相机在世界中的平移矩阵, R c R_c Rc 是相机在世界中的旋转矩阵。
现在,我们要把世界坐标系下的点,转换为相机坐标系下的点,本质上就是做一次「坐标系的逆变换」。如果相机的位姿是「先旋转,再平移到世界中的某个位置」,那么要把世界点转换到相机空间,就需要做完全相反的操作:先把世界整体平移,让相机回到世界原点;再把世界整体旋转,让相机的坐标轴和世界坐标轴对齐。
用矩阵的语言表达,就是 View 矩阵等于相机位姿矩阵的逆:
V = C − 1 V = C^{-1} V=C−1
根据矩阵逆的运算性质 ( A B ) − 1 = B − 1 A − 1 (AB)^{-1}=B^{-1}A^{-1} (AB)−1=B−1A−1 ,我们可以把V矩阵拆分为:
V = ( T c ⋅ R c ) − 1 = R c − 1 ⋅ T c − 1 V = (T_c \cdot R_c)^{-1} = R_c^{-1} \cdot T_c^{-1} V=(Tc⋅Rc)−1=Rc−1⋅Tc−1
这里就用到了旋转矩阵的核心性质:旋转矩阵是正交矩阵, R c − 1 = R c T R_c^{-1}=R_c^T Rc−1=RcT 。所以最终 V 矩阵可以写成:
V = R c T ⋅ T c − 1 V = R_c^T \cdot T_c^{-1} V=RcT⋅Tc−1
这也是所有教程里 View 矩阵的标准形式,它的几何意义无比清晰:先对世界做逆平移( T c − 1 T_c^{-1} Tc−1 ),把相机挪到原点;再对世界做逆旋转( R c T R_c^T RcT ),让相机的坐标轴与世界坐标轴对齐。
2.3 相机旋转矩阵的线性代数本质与完整构造
这是绝大多数教程的跳步重灾区,我们从线性代数「基变换」的根源出发,推导相机旋转矩阵的完整构造逻辑,讲清「为什么旋转矩阵的行是 u 、 v 、 w u、v、w u、v、w」「为什么逆旋转是转置」。
前置核心定理:坐标系变换 = 基变换
线性代数中,同一个向量,在不同坐标系下的坐标转换,本质是基向量的变换。我们先明确两个核心坐标系的基向量:
- 世界坐标系的标准正交基(世界基E): e x = ( 1 , 0 , 0 ) , e y = ( 0 , 1 , 0 ) , e z = ( 0 , 0 , 1 ) e_x=(1,0,0), e_y=(0,1,0), e_z=(0,0,1) ex=(1,0,0),ey=(0,1,0),ez=(0,0,1) ,是右手系单位正交基,也是我们的全局参考基准。
- 相机坐标系的标准正交基(相机基U): u u u (相机右方向)、 v v v (相机上方向)、 w w w (相机后方向),通过后续正交归一化得到,同样是右手系单位正交基。
任何一个空间向量 p ⃗ \vec{p} p ,都可以用任意一组基的线性组合来表示。比如:
- 用世界基表示: p ⃗ = x e x + y e y + z e z \vec{p} = x e_x + y e_y + z e_z p=xex+yey+zez ,其中 ( x , y , z ) (x,y,z) (x,y,z) 就是向量 p ⃗ \vec{p} p 在世界坐标系下的坐标,写成矩阵形式为:
p ⃗ = [ e x e y e z ] ⋅ [ x y z ] \vec{p} = \begin{bmatrix} e_x & e_y & e_z \end{bmatrix} \cdot \begin{bmatrix} x \\ y \\ z \end{bmatrix} p=[exeyez]⋅ xyz
- 用相机基表示: p ⃗ = x ′ u + y ′ v + z ′ w \vec{p} = x' u + y' v + z' w p=x′u+y′v+z′w ,其中 ( x ′ , y ′ , z ′ ) (x',y',z') (x′,y′,z′) 就是同一个向量 p ⃗ \vec{p} p 在相机坐标系下的坐标,写成矩阵形式为:
p ⃗ = [ u v w ] ⋅ [ x ′ y ′ z ′ ] \vec{p} = \begin{bmatrix} u & v & w \end{bmatrix} \cdot \begin{bmatrix} x' \\ y' \\ z' \end{bmatrix} p=[uvw]⋅ x′y′z′
因为是同一个向量,两个表达式完全相等:
[ e x e y e z ] ⋅ [ x y z ] = [ u v w ] ⋅ [ x ′ y ′ z ′ ] \begin{bmatrix} e_x & e_y & e_z \end{bmatrix} \cdot \begin{bmatrix} x \\ y \\ z \end{bmatrix} = \begin{bmatrix} u & v & w \end{bmatrix} \cdot \begin{bmatrix} x' \\ y' \\ z' \end{bmatrix} [exeyez]⋅ xyz =[uvw]⋅ x′y′z′
由于世界基E是标准单位正交基, [ e x e y e z ] \begin{bmatrix} e_x & e_y & e_z \end{bmatrix} [exeyez] 就是3阶单位矩阵 I I I ,因此左边可以直接简化为世界坐标 [ x y z ] \begin{bmatrix} x \ y \ z \end{bmatrix} [x y z] ,也就是 p ⃗ w o r l d \vec{p}_{world} pworld 。
我们把右边的 [ u v w ] \begin{bmatrix} u & v & w \end{bmatrix} [uvw] 定义为相机基变换矩阵 C C C ,它的列向量,就是相机的三个基向量 u 、 v 、 w u、v、w u、v、w 在世界坐标系下的坐标。此时公式简化为:
p ⃗ ∗ w o r l d = C ⋅ p ⃗ v i e w \vec{p}*{world} = C \cdot \vec{p}_{view} p∗world=C⋅pview
关键结论1:相机旋转矩阵 R c R_c Rc 就是基变换矩阵 C C C
这个公式的几何意义非常明确:已知一个向量在相机坐标系下的坐标 p ⃗ v i e w \vec{p}_{view} pview ,乘上基变换矩阵 C C C ,就能得到它在世界坐标系下的坐标 p ⃗ w o r l d \vec{p}_{world} pworld 。而这,正是相机旋转矩阵 R c R_c Rc 的核心作用,因此:
R c = C = [ u x v x w x u y v y w y u z v z w z ] R_c = C = \begin{bmatrix} u_x & v_x & w_x \\ u_y & v_y & w_y \\ u_z & v_z & w_z \end{bmatrix} Rc=C= uxuyuzvxvyvzwxwywz
它的列向量,永远是相机的三个基向量 u 、 v 、 w u、v、w u、v、w 。
关键结论2:逆旋转矩阵 R v i e w = R c − 1 = R c T R_{view}=R_c^{-1}=R_c^T Rview=Rc−1=RcT
我们的核心需求是反过来:已知世界坐标 p ⃗ w o r l d \vec{p}_{world} pworld ,求它在相机坐标系下的坐标 p ⃗ v i e w \vec{p}_{view} pview 。因此我们对上面的公式两边同时左乘 C C C 的逆矩阵:
C − 1 ⋅ p ⃗ w o r l d = p ⃗ v i e w C^{-1} \cdot \vec{p}_{world} = \vec{p}_{view} C−1⋅pworld=pview
这里就用到了正交矩阵的核心性质:由单位正交基构成的基变换矩阵,一定是正交矩阵,而正交矩阵的逆矩阵,等于它的转置矩阵,即 C − 1 = C T C^{-1}=C^T C−1=CT 。
C C C 的列是 u 、 v 、 w u、v、w u、v、w ,转置之后,行就变成了 u 、 v 、 w u、v、w u、v、w ,因此我们得到了逆旋转矩阵(也就是View矩阵的旋转部分)的最终形式:
R v i e w = C T = [ u x u y u z v x v y v z w x w y w z ] R_{view} = C^T = \begin{bmatrix} u_x & u_y & u_z \\ v_x & v_y & v_z \\ w_x & w_y & w_z \end{bmatrix} Rview=CT= uxvxwxuyvywyuzvzwz
关键结论3:View变换的执行顺序——先平移,后旋转
上面的推导是纯向量的旋转变换,没有考虑平移,因为平移只影响「点」的坐标,不影响「向量」的方向。
相机在世界中的位置是 e y e = ( e x , e y , e z ) eye=(e_x,e_y,e_z) eye=(ex,ey,ez) ,这意味着:相机坐标系的原点,在世界坐标系下的坐标是 e y e eye eye 。因此,世界坐标系下的任意一个点 p w o r l d p_{world} pworld ,它相对于相机原点的位置,是 p ⃗ w o r l d − e y e \vec{p}_{world} - eye pworld−eye 。
这个「相对位置」,就是我们上面推导的、需要做旋转变换的向量!因此,完整的View变换分为两步,逻辑顺序绝对不能颠倒:
-
平移变换:把世界坐标系的原点,平移到相机的位置,也就是给所有世界点减去 e y e eye eye ,得到点相对于相机的位置:
p ⃗ t r a n s l a t e d = p ⃗ w o r l d − e y e \vec{p}_{translated} = \vec{p}_{world} - eye ptranslated=pworld−eye
-
旋转变换:把这个相对位置,从世界基变换到相机基,也就是乘上我们推导的逆旋转矩阵 R v i e w R_{view} Rview ,得到最终的相机空间坐标:
p ⃗ v i e w = R v i e w ⋅ p ⃗ t r a n s l a t e d \vec{p}_{view} = R_{view} \cdot \vec{p}_{translated} pview=Rview⋅ptranslated
用矩阵乘法的形式表达,平移变换对应的逆平移矩阵 T v i e w = T c − 1 T_{view}=T_c^{-1} Tview=Tc−1 ,它的作用是给所有点平移 − e y e -eye −eye ,形式为:
T v i e w = [ 1 0 0 − e x 0 1 0 − e y 0 0 1 − e z 0 0 0 1 ] T_{view} = \begin{bmatrix} 1 & 0 & 0 & -e_x \\ 0 & 1 & 0 & -e_y \\ 0 & 0 & 1 & -e_z \\ 0 & 0 & 0 & 1 \end{bmatrix} Tview= 100001000010−ex−ey−ez1
由于矩阵乘法的执行顺序是「从右到左」,我们需要先执行平移,再执行旋转,因此完整的View矩阵必须写成:
V = R v i e w ⋅ T v i e w V = R_{view} \cdot T_{view} V=Rview⋅Tview
2.4 相机基底的正交归一化完整步骤
到这里,我们只解决了旋转矩阵的形式问题,还有一个最核心的问题没解决:怎么根据相机的参数,构造出合法的正交归一基 u 、 v 、 w u、v、w u、v、w ?
GAMES101 课程里,定义 View 矩阵的输入是三个参数:
-
eye:相机在世界中的位置,即 e y e = ( e x , e y , e z ) eye=(e_x,e_y,e_z) eye=(ex,ey,ez) -
gaze(简称g):相机的看向方向,即相机镜头指向的方向向量 -
up:世界空间中的上方向参考向量,通常为 ( 0 , 1 , 0 ) (0,1,0) (0,1,0)这里有一个巨大的坑:很多人直接拿
gaze和up作为相机的z轴和y轴,这是完全错误的。因为gaze和up往往不是正交的,直接用的话,会导致相机基底倾斜,旋转矩阵不再是正交矩阵,最终渲染出来的画面会出现拉伸、扭曲。正确的做法,是先对这三个向量做归一化 + 正交化,构造出一组正交归一的相机基底(右手系),这一步是View变换的核心,也是最容易被省略的关键步骤。
相机基底的完整构造步骤
我们约定:相机的局部坐标系, x x x 轴为右方向, y y y 轴为上方向, z z z 轴为后方向(相机看向 − z -z −z 轴,符合 GAMES101 / OpenGL 的右手系约定)。
-
构造相机 z z z 轴( w w w)
相机的看向方向是 g g g,而我们约定相机看向 − z -z −z 轴,所以相机的 z z z 轴正方向,是 g g g 的反方向。先对 g g g 做归一化,再取反,保证 z z z 轴是单位向量:
w = − n o r m a l i z e ( g ) w = - normalize(g) w=−normalize(g)
这一步的意义:把任意长度的看向方向,转换为单位长度的相机 z z z 轴,保证基底的归一性。
-
构造相机 x x x 轴( u u u)
相机的x轴是右方向,根据右手系叉乘规则,右方向 = 上参考方向 × 相机 z z z 轴。这里必须先做叉乘,再做归一化,保证 x x x 轴同时垂直于 u p up up 和 w w w,彻底解决 u p up up 和 g a z e gaze gaze 不正交的问题:
u = n o r m a l i z e ( u p × w ) u = normalize(up \times w) u=normalize(up×w)
这一步的意义:通过叉乘,得到一个同时垂直于 u p up up 和 w w w 的向量,这个向量天然和相机 z z z 轴正交,完美修正了原始 u p up up 向量的倾斜问题。
-
构造相机 y y y 轴( v v v)
现在我们已经有了正交的 x x x 轴 u u u 和 z z z 轴 w w w,只需要再做一次叉乘,就能得到真正正交于 x 、 z x、z x、z 轴的相机上方向 y y y 轴。由于 u u u 和 w w w 都是单位正交向量,叉乘结果天然是单位向量,无需再归一化:
v = w × u v = w \times u v=w×u
这一步的意义:修正原始 u p up up 向量,得到真正与相机朝向正交的上方向,最终 u 、 v 、 w u、v、w u、v、w 构成一组完美的右手正交归一基底。
2.5 View 矩阵的完整形式与数值实例验证
有了相机的正交归一基底,我们就能把旋转矩阵和平移矩阵结合,得到完整的 View 矩阵。
(1)完整的 View 矩阵展开形式
把 R v i e w R_{view} Rview 和 T v i e w T_{view} Tview 相乘,我们可以得到 View 矩阵的最终展开形式:
V = R v i e w ⋅ T v i e w = [ u x u y u z − u ⋅ e y e v x v y v z − v ⋅ e y e w x w y w z − w ⋅ e y e 0 0 0 1 ] V = R_{view} \cdot T_{view} = \begin{bmatrix} u_x & u_y & u_z & -u \cdot eye \\ v_x & v_y & v_z & -v \cdot eye \\ w_x & w_y & w_z & -w \cdot eye \\ 0 & 0 & 0 & 1 \end{bmatrix} V=Rview⋅Tview= uxvxwx0uyvywy0uzvzwz0−u⋅eye−v⋅eye−w⋅eye1
其中 u ⋅ e y e u \cdot eye u⋅eye 是向量 u u u 和 e y e eye eye 的点积,其余同理。
数值实例验证
我们用一个最简单的例子,验证 View 矩阵的正确性:
-
相机位置 e y e = ( 0 , 0 , 5 ) eye=(0,0,5) eye=(0,0,5) ,看向世界原点 ( 0 , 0 , 0 ) (0,0,0) (0,0,0) ,所以看向方向 g = ( 0 , 0 , − 5 ) g=(0,0,-5) g=(0,0,−5) ;
-
世界上方向参考 u p = ( 0 , 1 , 0 ) up=(0,1,0) up=(0,1,0) 。
第一步:构造相机基底
w = − n o r m a l i z e ( g ) = − n o r m a l i z e ( ( 0 , 0 , − 5 ) ) = ( 0 , 0 , 1 ) w = -normalize(g) = -normalize((0,0,-5)) = (0,0,1) w=−normalize(g)=−normalize((0,0,−5))=(0,0,1)
u = n o r m a l i z e ( u p × w ) = n o r m a l i z e ( ( 0 , 1 , 0 ) × ( 0 , 0 , 1 ) ) = n o r m a l i z e ( ( 1 , 0 , 0 ) ) = ( 1 , 0 , 0 ) u = normalize(up \times w) = normalize((0,1,0) \times (0,0,1)) = normalize((1,0,0)) = (1,0,0) u=normalize(up×w)=normalize((0,1,0)×(0,0,1))=normalize((1,0,0))=(1,0,0)
v = w × u = ( 0 , 0 , 1 ) × ( 1 , 0 , 0 ) = ( 0 , 1 , 0 ) v = w \times u = (0,0,1) \times (1,0,0) = (0,1,0) v=w×u=(0,0,1)×(1,0,0)=(0,1,0)
第二步:构造 View 矩阵
T v i e w = [ 1 0 0 0 0 1 0 0 0 0 1 − 5 0 0 0 1 ] , R v i e w = [ 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 ] T_{view} = \begin{bmatrix}1&0&0&0\\0&1&0&0\\0&0&1&-5\\0&0&0&1\end{bmatrix}, \quad R_{view} = \begin{bmatrix}1&0&0&0\\0&1&0&0\\0&0&1&0\\0&0&0&1\end{bmatrix} Tview= 10000100001000−51 ,Rview= 1000010000100001
V = R v i e w ⋅ T v i e w = [ 1 0 0 0 0 1 0 0 0 0 1 − 5 0 0 0 1 ] V = R_{view} \cdot T_{view} = \begin{bmatrix}1&0&0&0\\0&1&0&0\\0&0&1&-5\\0&0&0&1\end{bmatrix} V=Rview⋅Tview= 10000100001000−51
第三步:计算世界原点的观察空间坐标
世界原点 p w o r l d = ( 0 , 0 , 0 , 1 ) T p_{world}=(0,0,0,1)^T pworld=(0,0,0,1)T ,代入公式:
p v i e w = V ⋅ p w o r l d = [ 0 0 − 5 1 ] p_{view} = V \cdot p_{world} = \begin{bmatrix}0\\0\\-5\\1\end{bmatrix} pview=V⋅pworld= 00−51
结果完全符合预期:世界原点在相机的正前方5个单位处,对应相机空间的 z = − 5 z=-5 z=−5(相机看向 − z -z −z 轴),完美验证了 View 矩阵的正确性。
2.6 最常见的认知误区纠正
-
误区1:View 变换后,原点还是世界原点
正确理解:View 变换后,观察空间的原点是相机的位置,所有坐标都是相对相机描述的。
-
误区2: u p up up 向量不正交会导致画面中心偏移
正确理解:只要通过正交化步骤构造了正确的相机基底,View 变换会完全修正 u p up up 向量的偏差;画面是否偏移,是后续投影矩阵的参数决定的,和 View 变换无关。
-
误区3:View 矩阵是先旋转后平移
正确理解:View 矩阵的执行顺序是先平移( T v i e w T_{view} Tview ),后旋转( R v i e w R_{view} Rview ),和相机位姿矩阵的顺序完全相反,这是矩阵逆运算的核心规则。
三、Projection 投影矩阵:把相机可见空间,规范成 GPU 能处理的标准格式
Projection 矩阵(简称 P 矩阵),是很多初学者的第二个噩梦。大家常问:模型形状千差万别,为什么投影矩阵总在讲「把一个盒子/锥体压到规范立方体」?
答案很简单:投影矩阵从来不是针对模型设计的,而是针对相机的可见空间(视锥体)设计的。
经过View变换后,所有顶点都在相机空间里了,但相机不可能看到无限远、无限宽的世界,只能看到一个有限的空间范围,这个范围就是视锥体(View Volume)。投影矩阵的核心使命,就是:
定义相机的可见空间,剔除空间外的物体(裁剪);
把这个不规则的可见空间,统一映射到规范立方体(Canonical Cube),也就是 x 、 y 、 z ∈ [ − 1 , 1 ] x、y、z \in [-1,1] x、y、z∈[−1,1]的标准化空间,让 GPU 的后续裁剪、视口映射流程,可以复用同一套规则,不用适配不同的相机参数。
投影分为两大类:正交投影(Orthographic Projection) 和 透视投影(Perspective Projection),我们分别做完整的推导和讲解。
3.1 正交投影:从长方体到规范立方体的完整推导
正交投影的视锥体,是一个轴对齐的长方体,它的可见范围在相机空间中定义为:
x ∈ [ l , r ] , y ∈ [ b , t ] , z ∈ [ f , n ] x \in [l, r], \quad y \in [b, t], \quad z \in [f, n] x∈[l,r],y∈[b,t],z∈[f,n]
其中:
-
l l l =左边界, r r r =右边界, b b b =下边界, t t t =上边界;
-
n n n =近平面, f f f =远平面(GAMES101 约定相机看向 − z -z −z 轴,所以近平面距离为 − n -n −n,远平面距离为 − f -f −f,且 n > f n>f n>f)。
正交投影的核心,就是把这个 [ l , r ] × [ b , t ] × [ f , n ] [l,r]×[b,t]×[f,n] [l,r]×[b,t]×[f,n] 的长方体,线性映射到 [ − 1 , 1 ] × [ − 1 , 1 ] × [ − 1 , 1 ] [-1,1]×[-1,1]×[-1,1] [−1,1]×[−1,1]×[−1,1] 的规范立方体。这个映射可以拆分为两步:先平移,把长方体的中心移到世界原点;再缩放,把长方体的长宽高缩放到2,刚好填满[-1,1]的区间。

步骤1:平移矩阵 T o r t h o T_{ortho} Tortho 的推导
我们需要把长方体的中心,从 ( l + r 2 , b + t 2 , n + f 2 ) (\frac{l+r}{2}, \frac{b+t}{2}, \frac{n+f}{2}) (2l+r,2b+t,2n+f) 平移到原点 ( 0 , 0 , 0 ) (0,0,0) (0,0,0) ,所以平移的偏移量是 ( − l + r 2 , − b + t 2 , − n + f 2 ) (-\frac{l+r}{2}, -\frac{b+t}{2}, -\frac{n+f}{2}) (−2l+r,−2b+t,−2n+f) ,对应的平移矩阵为:
T o r t h o = [ 1 0 0 − l + r 2 0 1 0 − b + t 2 0 0 1 − n + f 2 0 0 0 1 ] T_{ortho} = \begin{bmatrix} 1 & 0 & 0 & -\frac{l+r}{2} \\ 0 & 1 & 0 & -\frac{b+t}{2} \\ 0 & 0 & 1 & -\frac{n+f}{2} \\ 0 & 0 & 0 & 1 \end{bmatrix} Tortho= 100001000010−2l+r−2b+t−2n+f1
步骤2:缩放矩阵 S o r t h o S_{ortho} Sortho 的推导
平移后的长方体,x轴范围是 [ − r − l 2 , r − l 2 ] [-\frac{r-l}{2}, \frac{r-l}{2}] [−2r−l,2r−l] ,y轴范围是 [ − t − b 2 , t − b 2 ] [-\frac{t-b}{2}, \frac{t-b}{2}] [−2t−b,2t−b] ,z轴范围是 [ − n − f 2 , n − f 2 ] [-\frac{n-f}{2}, \frac{n-f}{2}] [−2n−f,2n−f] 。我们需要把这个范围缩放到[-1,1],所以三个轴的缩放因子分别为 2 r − l \frac{2}{r-l} r−l2 、 2 t − b \frac{2}{t-b} t−b2 、 2 n − f \frac{2}{n-f} n−f2 ,对应的缩放矩阵为:
S o r t h o = [ 2 r − l 0 0 0 0 2 t − b 0 0 0 0 2 n − f 0 0 0 0 1 ] S_{ortho} = \begin{bmatrix} \frac{2}{r-l} & 0 & 0 & 0 \\ 0 & \frac{2}{t-b} & 0 & 0 \\ 0 & 0 & \frac{2}{n-f} & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} Sortho= r−l20000t−b20000n−f200001
完整的正交投影矩阵
和 M 矩阵一样,矩阵乘法从右到左执行,先平移后缩放,所以正交投影矩阵为:
M o r t h o = S o r t h o ⋅ T o r t h o = [ 2 r − l 0 0 − l + r r − l 0 2 t − b 0 − b + t t − b 0 0 2 n − f − n + f n − f 0 0 0 1 ] M_{ortho} = S_{ortho} \cdot T_{ortho} = \begin{bmatrix} \frac{2}{r-l} & 0 & 0 & -\frac{l+r}{r-l} \\ 0 & \frac{2}{t-b} & 0 & -\frac{b+t}{t-b} \\ 0 & 0 & \frac{2}{n-f} & -\frac{n+f}{n-f} \\ 0 & 0 & 0 & 1 \end{bmatrix} Mortho=Sortho⋅Tortho= r−l20000t−b20000n−f20−r−ll+r−t−bb+t−n−fn+f1
特殊情况:当视锥体是对称的,也就是 l = − r l=-r l=−r , b = − t b=-t b=−t 时,平移项的分子会变成0,矩阵会大幅简化,这也是我们最常用的对称正交投影矩阵:
M o r t h o = [ 1 r 0 0 0 0 1 t 0 0 0 0 2 n − f − n + f n − f 0 0 0 1 ] M_{ortho} = \begin{bmatrix} \frac{1}{r} & 0 & 0 & 0 \\ 0 & \frac{1}{t} & 0 & 0 \\ 0 & 0 & \frac{2}{n-f} & -\frac{n+f}{n-f} \\ 0 & 0 & 0 & 1 \end{bmatrix} Mortho= r10000t10000n−f2000−n−fn+f1
数值实例:正交投影的视口映射与拉伸问题
我们用一个具体的例子,看正交投影的完整流程,以及宽高比拉伸的问题:
-
正交相机的可视范围: x ∈ [ − 2 , 2 ] x∈[-2,2] x∈[−2,2] , y ∈ [ − 1 , 1 ] y∈[-1,1] y∈[−1,1] , z ∈ [ − 100 , − 0.1 ] z∈[-100,-0.1] z∈[−100,−0.1] ;
-
屏幕分辨率:400×100,宽高比4:1;
-
待变换的点: p v i e w = ( 1 , 1 , − 1 , 1 ) T p_{view}=(1,1,-1,1)^T pview=(1,1,−1,1)T (相机空间中, z = − 1 z=-1 z=−1 表示在相机前方1个单位)。
第一步:计算正交投影矩阵
M o r t h o = [ 2 4 0 0 0 0 2 2 0 0 0 0 2 99.9 − − 100.1 99.9 0 0 0 1 ] = [ 0.5 0 0 0 0 1 0 0 0 0 0.02 1.002 0 0 0 1 ] M_{ortho} = \begin{bmatrix} \frac{2}{4} & 0 & 0 & 0 \\ 0 & \frac{2}{2} & 0 & 0 \\ 0 & 0 & \frac{2}{99.9} & -\frac{-100.1}{99.9} \\ 0 & 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix} 0.5 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0.02 & 1.002 \\ 0 & 0 & 0 & 1 \end{bmatrix} Mortho= 42000022000099.92000−99.9−100.11 = 0.50000100000.020001.0021
第二步:投影到裁剪空间,做透视除法得到NDC坐标
正交投影的 w w w 分量始终为1,所以透视除法后结果不变:
p c l i p = M o r t h o ⋅ p v i e w = ( 0.5 , 1 , 0.982 , 1 ) T p_{clip} = M_{ortho} \cdot p_{view} = (0.5, 1, 0.982, 1)^T pclip=Mortho⋅pview=(0.5,1,0.982,1)T
p n d c = ( 0.5 , 1 , 0.982 ) p_{ndc} = (0.5, 1, 0.982) pndc=(0.5,1,0.982)
第三步:视口映射到屏幕坐标
视口映射的公式为:
x s c r e e n = x n d c + 1 2 × w i d t h , y s c r e e n = y n d c + 1 2 × h e i g h t x_{screen} = \frac{x_{ndc} + 1}{2} \times width, \quad y_{screen} = \frac{y_{ndc} + 1}{2} \times height xscreen=2xndc+1×width,yscreen=2yndc+1×height
代入计算:
x s c r e e n = 0.5 + 1 2 × 400 = 300 , y s c r e e n = 1 + 1 2 × 100 = 100 x_{screen} = \frac{0.5+1}{2} \times 400 = 300, \quad y_{screen} = \frac{1+1}{2} \times 100 = 100 xscreen=20.5+1×400=300,yscreen=21+1×100=100
拉伸问题分析:相机视锥体的宽高比是 ( 2 − ( − 2 ) ) : ( 1 − ( − 1 ) ) = 2 : 1 (2-(-2)):(1-(-1))=2:1 (2−(−2)):(1−(−1))=2:1 ,而屏幕的宽高比是4:1,两者不匹配,所以最终渲染的画面会出现横向拉伸。这也是为什么我们在实际开发中,必须让相机的宽高比和屏幕分辨率的宽高比保持一致。
3.2 透视投影:从截头锥到规范立方体的两步法拆解
透视投影是我们人眼和真实相机的成像模式,核心特点是「近大远小」:离相机越近的物体,看起来越大;离相机越远的物体,看起来越小。
透视投影的视锥体,不再是长方体,而是一个截头锥(Frustum):近平面小,远平面大,越往远处,可见的x/y范围越大。这就导致我们不能像正交投影那样,直接用一次平移+缩放完成映射,GAMES101课程里给出了一个天才的拆解思路:
-
第一步:Persp→Ortho:把透视的截头锥,「压成」一个和正交投影一样的长方体,保留透视的「近大远小」特性;
-
第二步:Ortho 归一化:用正交投影矩阵,把这个长方体映射到规范立方体。
用公式表达,完整的透视投影矩阵为:
M p e r s p = M o r t h o ⋅ M p e r s p → o r t h o M_{persp} = M_{ortho} \cdot M_{persp→ortho} Mpersp=Mortho⋅Mpersp→ortho

3.3 Persp→Ortho 矩阵的完整构造
这是透视投影的核心难点,我们从几何意义、约束条件、公式联立到结果验证,理清 Persp→Ortho 矩阵的每一个元素的由来。
前置核心目标与约束条件
Persp→Ortho 变换的核心目标:把透视投影的截头锥(Frustum),变换成一个轴对齐的长方体(正交投影的视锥体),同时必须满足以下5个不可违背的约束:
-
近平面上的所有点,经过变换后, x 、 y 、 z x、y、z x、y、z 坐标完全不变(近平面是成像平面,不能有任何变形);
-
远平面上的所有点,经过变换后, z z z 坐标完全不变, x 、 y x、y x、y 坐标按比例缩放,保证远平面边界与近平面对齐;
-
变换必须可以用 4 × 4 4×4 4×4 齐次矩阵表示(仿射变换),能和其他变换矩阵组合;
-
变换后,点的深度顺序保持不变(离相机近的点, z z z 值更小,保证深度缓冲正确);
-
变换后的 x 、 y x、y x、y 坐标必须满足透视的「近大远小」特性,即与深度 − z -z −z 成反比(相似三角形的核心结论)。
步骤1:从相似三角形推导透视核心公式
透视投影的「近大远小」,本质上是相似三角形的几何规律,我们用二维截面做推导:
二维截面( y − z y - z y−z 平面):相机在相机空间的原点 O ( 0 , 0 ) O(0, 0) O(0,0),看向 − z -z −z 轴;近平面是直线 z = n ( n < 0 ) z = n(n < 0) z=n(n<0),和 z z z 轴交于点 N ( 0 , n ) N(0,n) N(0,n)。
相机前方有一个空间点 P ( y , z ) , z < 0 P(y, z),z < 0 P(y,z),z<0(相机前方),连接 O P OP OP,与近平面 z = n z = n z=n 交于投影点 P ′ ( y ′ , n ) P'(y', n) P′(y′,n)
过 P P P 作 z z z 轴的垂线,垂足为 A ( 0 , z ) A(0, z) A(0,z);过 P ′ P' P′ 作 z z z 轴的垂线,垂足为 N ( 0 , n ) N(0, n) N(0,n)。

此时, △ O N P ′ △ONP' △ONP′ 和 △ O A P △OAP △OAP 是相似三角形:两个都是直角三角形,且共用顶角 ∠ A O P ∠AOP ∠AOP,满足 A A AA AA 相似判定规则。
相似三角形的对应边成比例: ∣ O N ∣ ∣ O A ∣ = ∣ N P ′ ∣ ∣ A P ∣ \frac{|ON|}{|OA|} = \frac{|NP'|}{|AP|} ∣OA∣∣ON∣=∣AP∣∣NP′∣
其中:
-
∣ O N ∣ |ON| ∣ON∣:近平面到原点的垂直距离,等于 − n -n −n;
-
∣ O A ∣ |OA| ∣OA∣:点P到原点的垂直距离,等于 − z -z −z;
-
∣ N P ′ ∣ |NP'| ∣NP′∣:投影点P’的y坐标,等于 y ′ y' y′;
-
∣ A P ∣ |AP| ∣AP∣:空间点P的y坐标,等于 y y y。
将对应边代入比例式,得到:
− n − z = y ′ y \frac{-n}{-z} = \frac{y'}{y} −z−n=yy′
交叉相乘后,得到近平面上的投影 x x x 坐标:
y ′ = n ⋅ y z y' = \frac{n \cdot y}{z} y′=zn⋅y
x x x 方向与 y y y 方向完全对称,同理可得:
x ′ = n ⋅ x z x' = \frac{n \cdot x}{z} x′=zn⋅x
这就是透视投影最核心的公式:投影后的 x 、 y x、y x、y 坐标,与深度 z z z 成反比, z z z 越小(离相机越远), x 、 y x、y x、y 越小,物体看起来就越小,完美实现了「近大远小」的效果。
步骤2:用齐次坐标解决非线性变换问题
现在我们遇到了一个核心矛盾: x ′ = n x z x' = \frac{n x}{z} x′=znx 是一个非线性运算(除以 z z z),而 4 × 4 4×4 4×4 矩阵只能做线性运算(加法、数乘),无法直接表示这个除法。
这时候,齐次坐标的第二个核心作用就体现出来了:4维齐次向量 ( X , Y , Z , W ) (X,Y,Z,W) (X,Y,Z,W) ,对应的3维笛卡尔坐标是 ( X / W , Y / W , Z / W ) (X/W, Y/W, Z/W) (X/W,Y/W,Z/W) ,其中W≠0。
也就是说,我们不需要在矩阵变换的时候直接算出最终的 x ′ x' x′,只需要让矩阵变换后的齐次坐标满足:
-
X X X 分量 = n x n x nx
-
W W W 分量 = z z z
那么在后续 GPU 自动执行的透视除法(除以 W W W 分量)之后,自然就能得到:
x ′ = X W = n x z x' = \frac{X}{W} = \frac{n x}{z} x′=WX=znx
完美解决了非线性变换的编码问题!
y y y 分量同理,我们让矩阵变换后的 Y Y Y 分量 = n y n y ny , W W W 分量 = z z z ,透视除法后就能得到 y ′ = n y z y' = \frac{n y}{z} y′=zny 。
至此,我们已经完全确定了 Persp→Ortho 矩阵的第一行、第二行、第四行:
-
第一行:要让 X = n x X = n x X=nx,所以第一行是 [ n , 0 , 0 , 0 ] ( X = n ⋅ x + 0 ⋅ y + 0 ⋅ z + 0 ⋅ 1 = n x ) [n, 0, 0, 0](X = n·x + 0·y + 0·z + 0·1 = n x) [n,0,0,0](X=n⋅x+0⋅y+0⋅z+0⋅1=nx);
-
第二行:要让 Y = n y Y = n y Y=ny,所以第二行是 [ 0 , n , 0 , 0 ] ( Y = 0 ⋅ x + n ⋅ y + 0 ⋅ z + 0 ⋅ 1 = n y ) [0, n, 0, 0](Y = 0·x + n·y + 0·z + 0·1 = n y) [0,n,0,0](Y=0⋅x+n⋅y+0⋅z+0⋅1=ny);
-
第四行:要让 W = z W = z W=z,所以第四行是 [ 0 , 0 , 1 , 0 ] ( W = 0 ⋅ x + 0 ⋅ y + 1 ⋅ z + 0 ⋅ 1 = z ) [0, 0, 1, 0](W = 0·x + 0·y + 1·z + 0·1 = z) [0,0,1,0](W=0⋅x+0⋅y+1⋅z+0⋅1=z)。
步骤3:确定第三行( Z Z Z 分量)的形式
现在只剩下矩阵的第三行,也就是 Z Z Z 分量的变换规则还没确定。这里有两个核心约束,决定了第三行的形式:
-
深度值只能和 z z z 有关,不能和 x 、 y x、y x、y 有关:如果 Z Z Z 分量和 x 、 y x、y x、y 相关,那么同一个深度 z z z 的点,会因为 x 、 y x、 y x、y 不同得到不同的 Z Z Z 值,深度缓冲会完全失效,这是绝对不允许的。因此第三行的 x 、 y x、y x、y 对应的系数必须为0。
-
Z Z Z 分量必须是关于 z z z 的线性函数:只有线性函数才能保证深度值的相对顺序不变,同时可以用 4 × 4 4×4 4×4 矩阵表示。
因此,第三行的形式只能是 [ 0 , 0 , A , B ] [0, 0, A, B] [0,0,A,B] ,其中 A A A 和 B B B 是待求的未知系数,对应的 Z Z Z 分量变换规则为:
Z = A ⋅ z + B ⋅ 1 Z = A \cdot z + B \cdot 1 Z=A⋅z+B⋅1
至此,我们得到了 Persp→Ortho 矩阵的完整待定形式:
M p e r s p → o r t h o = [ n 0 0 0 0 n 0 0 0 0 A B 0 0 1 0 ] M_{persp→ortho} = \begin{bmatrix} n & 0 & 0 & 0 \\ 0 & n & 0 & 0 \\ 0 & 0 & A & B \\ 0 & 0 & 1 & 0 \end{bmatrix} Mpersp→ortho= n0000n0000A100B0
步骤4:用边界条件联立方程,求解 A A A 和 B B B
我们用约束1和约束2的边界条件,来求解 A A A 和 B B B:近平面和远平面上的点,经过变换和透视除法后, z z z 坐标保持不变。
边界条件1:近平面上的点, z = n z = n z=n
取近平面中心的点 p n e a r = ( 0 , 0 , n , 1 ) T p_{near}=(0,0,n,1)^T pnear=(0,0,n,1)T ,代入矩阵做变换:
M p e r s p → o r t h o ⋅ p n e a r = [ n ⋅ 0 + 0 ⋅ 0 + 0 ⋅ n + 0 ⋅ 1 0 ⋅ 0 + n ⋅ 0 + 0 ⋅ n + 0 ⋅ 1 0 ⋅ 0 + 0 ⋅ 0 + A ⋅ n + B ⋅ 1 0 ⋅ 0 + 0 ⋅ 0 + 1 ⋅ n + 0 ⋅ 1 ] = [ 0 0 A n + B n ] M_{persp→ortho} \cdot p_{near} = \begin{bmatrix} n \cdot 0 + 0 \cdot 0 + 0 \cdot n + 0 \cdot 1 \\ 0 \cdot 0 + n \cdot 0 + 0 \cdot n + 0 \cdot 1 \\ 0 \cdot 0 + 0 \cdot 0 + A \cdot n + B \cdot 1 \\ 0 \cdot 0 + 0 \cdot 0 + 1 \cdot n + 0 \cdot 1 \end{bmatrix} = \begin{bmatrix} 0 \\ 0 \\ A n + B \\ n \end{bmatrix} Mpersp→ortho⋅pnear= n⋅0+0⋅0+0⋅n+0⋅10⋅0+n⋅0+0⋅n+0⋅10⋅0+0⋅0+A⋅n+B⋅10⋅0+0⋅0+1⋅n+0⋅1 = 00An+Bn
根据约束条件,透视除法后的z坐标必须等于原来的 z = − n z = -n z=−n,即:
Z W = A n + B n = n \frac{Z}{W} = \frac{A n + B}{n} = n WZ=nAn+B=n
两边同时乘以 n n n,化简得到第一个方程:
A n + B = n 2 方程 ( 1 ) A n + B = n^2 \quad 方程(1) An+B=n2方程(1)
边界条件2:远平面上的点,z = f
取远平面中心的点 p f a r = ( 0 , 0 , f , 1 ) T p_{far}=(0,0,f,1)^T pfar=(0,0,f,1)T ,代入矩阵做变换:
M p e r s p → o r t h o ⋅ p f a r = [ n ⋅ 0 + 0 ⋅ 0 + 0 ⋅ f + 0 ⋅ 1 0 ⋅ 0 + n ⋅ 0 + 0 ⋅ f + 0 ⋅ 1 0 ⋅ 0 + 0 ⋅ 0 + A ⋅ f + B ⋅ 1 0 ⋅ 0 + 0 ⋅ 0 + 1 ⋅ f + 0 ⋅ 1 ] = [ 0 0 A f + B f ] M_{persp→ortho} \cdot p_{far} = \begin{bmatrix} n \cdot 0 + 0 \cdot 0 + 0 \cdot f + 0 \cdot 1 \\ 0 \cdot 0 + n \cdot 0 + 0 \cdot f + 0 \cdot 1 \\ 0 \cdot 0 + 0 \cdot 0 + A \cdot f + B \cdot 1 \\ 0 \cdot 0 + 0 \cdot 0 + 1 \cdot f + 0 \cdot 1 \end{bmatrix} = \begin{bmatrix} 0 \\ 0 \\ A f + B \\ f \end{bmatrix} Mpersp→ortho⋅pfar= n⋅0+0⋅0+0⋅f+0⋅10⋅0+n⋅0+0⋅f+0⋅10⋅0+0⋅0+A⋅f+B⋅10⋅0+0⋅0+1⋅f+0⋅1 = 00Af+Bf
同理,透视除法后的 z z z 坐标必须等于原来的 z = − f z = -f z=−f,即:
Z W = A f + B f = f \frac{Z}{W} = \frac{A f + B}{f} = f WZ=fAf+B=f
两边同时乘以 f f f,化简得到第二个方程:
A f + B = f 2 方程 ( 2 ) A f + B = f^2 \quad 方程(2) Af+B=f2方程(2)
联立方程求解 A A A 和 B B B
最终可以求得:
A = n + f A = n + f A=n+f
B = − n f B = -n f B=−nf
步骤5:得到完整的 Persp→Ortho 矩阵
把求解得到的 A = n + f A = n + f A=n+f、 B = − n f B = -n f B=−nf代入待定矩阵,我们就得到了最终的 Persp→Ortho 矩阵:
M p e r s p → o r t h o = [ n 0 0 0 0 n 0 0 0 0 n + f − n f 0 0 1 0 ] M_{persp→ortho} = \begin{bmatrix} n & 0 & 0 & 0 \\ 0 & n & 0 & 0 \\ 0 & 0 & n + f & -n f \\ 0 & 0 & 1 & 0 \end{bmatrix} Mpersp→ortho= n0000n0000n+f100−nf0
步骤6:结果验证
我们用近平面和远平面的任意点,验证这个矩阵是否满足所有约束:
验证1:近平面任意点的变换
取近平面上的任意点 p = ( x , y , n , 1 ) T p=(x,y,n,1)^T p=(x,y,n,1)T ,代入矩阵变换:
M ⋅ p = [ n x n y ( n + f ) n − n f n ] = [ n x n y n 2 + n f − n f n ] = [ n x n y n 2 n ] M \cdot p = \begin{bmatrix} n x \\ n y \\ (n+f)n - n f \\ n \end{bmatrix} = \begin{bmatrix} n x \\ n y \\ n^2 +n f - n f \\ n \end{bmatrix} = \begin{bmatrix} n x \\ n y \\ n^2 \\ n \end{bmatrix} M⋅p= nxny(n+f)n−nfn = nxnyn2+nf−nfn = nxnyn2n
透视除法后,得到 ( x , y , n , 1 ) (x, y, n, 1) (x,y,n,1) ,和原始点的坐标完全一致!完美满足「近平面不变形」的约束1。
验证2:远平面任意点的变换
取远平面上的任意点 p = ( x , y , f , 1 ) T p=(x,y,f,1)^T p=(x,y,f,1)T ,代入矩阵变换:
M ⋅ p = [ n x n y ( n + f ) f − n f f ] = [ n x n y n f + f 2 − n f f ] = [ n x n y f 2 f ] M \cdot p = \begin{bmatrix} n x \\ n y \\ (n+f)f - n f \\ f \end{bmatrix} = \begin{bmatrix} n x \\ n y \\ n f +f^2 - n f \\ f \end{bmatrix} = \begin{bmatrix} n x \\ n y \\ f^2 \\ f \end{bmatrix} M⋅p= nxny(n+f)f−nff = nxnynf+f2−nff = nxnyf2f
透视除法后,得到 ( n x f , n y f , f , 1 ) (\frac{n x}{f}, \frac{n y}{f}, f, 1) (fnx,fny,f,1) :
-
z z z 坐标保持 f f f 不变,满足约束2;
-
x 、 y x、y x、y 坐标缩放了 n f \frac{n}{f} fn 倍,而截头锥的远平面 x 、 y x、y x、y 范围恰好是近平面的 f n \frac{f}{n} nf 倍,缩放后远平面的 x 、 y x、y x、y 范围和近平面完全一致,截头锥完美变成了长方体,实现了 Persp→Ortho 的核心目标。
3.4 完整的透视投影矩阵
把 Persp→Ortho 矩阵和正交投影矩阵相乘,就得到了完整的透视投影矩阵:
M p e r s p = M o r t h o ⋅ M p e r s p → o r t h o = [ 2 n r − l 0 l + r l − r 0 0 2 n t − b b + t b − t 0 0 0 − n + f n − f 2 n f f − n 0 0 1 0 ] M_{persp} = M_{ortho} \cdot M_{persp→ortho} = \begin{bmatrix} \frac{2n}{r-l} & 0 & \frac{l+r}{l-r} & 0 \\ 0 & \frac{2n}{t-b} & \frac{b+t}{b-t} & 0 \\ 0 & 0 & -\frac{n+f}{n-f} & \frac{2nf}{f-n} \\ 0 & 0 & 1 & 0 \end{bmatrix} Mpersp=Mortho⋅Mpersp→ortho= r−l2n0000t−b2n00l−rl+rb−tb+t−n−fn+f100f−n2nf0
常用简化形式:当视锥体对称时( l = − r l=-r l=−r , b = − t b=-t b=−t ),矩阵会大幅简化,这也是实际开发中大多数场景会用到的形式。同时,我们通常会用垂直视场角 f o v Y fovY fovY 和宽高比 a s p e c t aspect aspect 来替代 l / r / b / t l/r/b/t l/r/b/t,转换关系为:
t = − n ⋅ tan ( f o v Y 2 ) , b = − t , r = t ⋅ a s p e c t , l = − r t = -n \cdot \tan(\frac{fovY}{2}), \quad b = -t, \quad r = t \cdot aspect, \quad l = -r t=−n⋅tan(2fovY),b=−t,r=t⋅aspect,l=−r


代入后,得到基于 f o v Y fovY fovY 和 a s p e c t aspect aspect 的对称透视投影矩阵:
M p e r s p = [ − 1 a s p e c t ⋅ tan ( f o v Y 2 ) 0 0 0 0 − 1 tan ( f o v Y 2 ) 0 0 0 0 − n + f n − f 2 n f f − n 0 0 1 0 ] M_{persp} = \begin{bmatrix} -\frac{1}{aspect \cdot \tan(\frac{fovY}{2})} & 0 & 0 & 0 \\ 0 & -\frac{1}{\tan(\frac{fovY}{2})} & 0 & 0 \\ 0 & 0 & -\frac{n+f}{n-f} & \frac{2nf}{f-n} \\ 0 & 0 & 1 & 0 \end{bmatrix} Mpersp= −aspect⋅tan(2fovY)10000−tan(2fovY)10000−n−fn+f100f−n2nf0
3.5 关键区别:正交 vs 透视的非对称窗口,为什么不能等价互换
很多人会发现,正交投影里的非对称窗口,完全可以等价为「平移相机 + 对称正交窗口」,但透视投影里,这个等价关系完全不成立,这是为什么?
核心原因在于两者的成像原理完全不同:
-
正交投影:成像射线是平行的, x 、 y x、y x、y 的成像结果和深度 z z z 完全无关。非对称窗口只是平移了可见的长方体,只要平移相机,就能让对称窗口的可见区域和非对称窗口完全一致。
-
透视投影:成像射线是从相机原点出发的汇聚射线, x 、 y x、y x、y 的成像结果和深度 z z z 强相关。非对称窗口只是改变了近平面窗口相对主视线的位置,投影中心(相机原点)没有变;而平移相机,改变的是所有成像射线的起点,两者的几何意义完全不同,最终的成像结果也天差地别。
这个区别不是数学上的边角料,而是有极强的工程价值:VR 双目渲染、多屏拼接、Portal 传送门、平面反射渲染等场景,都必须用到非对称透视投影,无法用简单的相机平移替代。
四、 Viewport 变换:从 NDC 到屏幕像素的最终映射
经过 Projection 矩阵变换和透视除法之后,顶点已经进入 NDC(Normalized Device Coordinates,标准化设备坐标)空间。此时坐标范围已经被统一成 x n d c ∈ [ − 1 , 1 ] x_{ndc} \in [-1,1] xndc∈[−1,1] , y n d c ∈ [ − 1 , 1 ] y_{ndc} \in [-1,1] yndc∈[−1,1] , z n d c ∈ [ − 1 , 1 ] z_{ndc} \in [-1,1] zndc∈[−1,1] 。但这还不是最终可用于光栅化和深度测试的屏幕坐标。Viewport 变换要做的,就是把这一套标准化区间映射到一个具体的窗口范围里。
这一节最容易乱的地方有两个。第一,到底要不要在公式里写 w i d t h − 1 width - 1 width−1 和 h e i g h t − 1 height - 1 height−1;第二,屏幕原点到底取左下角还是左上角。如果把连续窗口坐标和离散像素索引混在一起讨论,最后公式往往会前后不一致。这里统一采用:左下角为原点的窗口坐标系(因此 y y y 方向不需要翻转,如果改成左上角原点,应该将 y y y 轴做额外变换,这里不做讨论);先把 NDC 映射到连续窗口坐标;深度范围默认映射到 [ 0 , 1 ] [0,1] [0,1]。
4.1 Viewport 变换的公式
Viewport 变换的输入是 NDC 坐标 p n d c = ( x n d c , y n d c , z n d c ) p_{ndc}=(x_{ndc}, y_{ndc}, z_{ndc}) pndc=(xndc,yndc,zndc) ,输出是屏幕坐标 p s c r e e n = ( x s c r e e n , y s c r e e n , z d e p t h ) p_{screen}=(x_{screen}, y_{screen}, z_{depth}) pscreen=(xscreen,yscreen,zdepth) 。
x s c r e e n = ( x n d c + 1 ) ⋅ w i d t h 2 x_{screen}=(x_{ndc}+1) \cdot \frac{width}{2} xscreen=(xndc+1)⋅2width
y s c r e e n = ( y n d c + 1 ) ⋅ h e i g h t 2 y_{screen}=(y_{ndc}+1) \cdot \frac{height}{2} yscreen=(yndc+1)⋅2height
z d e p t h = z n d c + 1 2 z_{depth}=\frac{z_{ndc}+1}{2} zdepth=2zndc+1
写成矩阵的形式就是:
p s c r e e n = M v i e w p o r t ⋅ p n d c , M v i e w p o r t = [ w i d t h 2 0 0 w i d t h 2 0 h e i g h t 2 0 h e i g h t 2 0 0 1 2 1 2 0 0 0 1 ] p_{screen}=M_{viewport} \cdot p_{ndc} , M_{viewport}=\begin{bmatrix} \frac{width}{2} & 0 & 0 & \frac{width}{2} \\ 0 & \frac{height}{2} & 0 & \frac{height}{2} \\ 0 & 0 & \frac{1}{2} & \frac{1}{2} \\ 0 & 0 & 0 & 1 \end{bmatrix} pscreen=Mviewport⋅pndc,Mviewport= 2width00002height00002102width2height211
补充说明:透视投影的 NDC 深度是非线性的,因此映射后的z也保持非线性,这能保证近平面的深度精度更高,避免远平面深度冲突(Z-Fighting)。
4.2 为什么这里不用 w i d t h − 1 width - 1 width−1 和 h e i g h t − 1 height - 1 height−1
很多资料会把公式写成 x s c r e e n = ( x n d c + 1 ) ⋅ w i d t h − 1 2 x_{screen}=(x_{ndc}+1) \cdot \frac{width-1}{2} xscreen=(xndc+1)⋅2width−1 、 y s c r e e n = ( x n d c + 1 ) ⋅ h e i g h t − 1 2 y_{screen}=(x_{ndc}+1) \cdot \frac{height-1}{2} yscreen=(xndc+1)⋅2height−1 。这种写法并不是绝对错误,但它对应的语义已经发生了变化:它更像是在把 NDC 直接映射到离散像素索引,也就是把最左边像素当作 0,把最右边像素当作 w i d t h − 1 width - 1 width−1。
而这一节讨论的是图形学管线里的 viewport 变换,本质上是在做连续空间到连续空间的映射。因此,这里更自然、更统一的写法应该使用 w i d t h width width 和 h e i g h t height height,而不是提前把像素下标那一层离散化细节混进来。如果后续还要进一步讨论像素中心、取整和栅格覆盖,那是光栅化阶段的问题,不属于 viewport 变换本身。
附录:补充内容
A. 左右手坐标系的矩阵差异
本文所有推导都基于 GAMES101 / OpenGL 的右手系约定(相机看向 − z -z −z 轴),而 DirectX / Unity 采用左手系约定(相机看向 + z +z +z 轴),两者的核心差异在于:
-
旋转正方向的定义不同;
-
投影矩阵的 z z z 轴符号、深度范围不同(OpenGL深度范围 [ − 1 , 1 ] [-1,1] [−1,1],DirectX深度范围 [ 0 , 1 ] [0,1] [0,1]);
-
相机 z z z 轴的方向相反,View 矩阵和投影矩阵的 z z z 轴相关项需要做符号调整。
B. 透视除法与深度缓冲
透视投影矩阵变换后,我们得到的是裁剪空间坐标,必须经过透视除法(即 x 、 y 、 z x、y、z x、y、z 分别除以 w w w 分量),才能得到 NDC 坐标。这一步是 GPU 自动完成的,也是透视投影「近大远小」效果的最终实现环节。
同时,透视投影的深度值在 NDC 空间中是非线性的,离相机越近,深度精度越高;离相机越远,深度精度越低。这也是为什么远平面不能设置得过大,否则会出现严重的深度冲突(Z-Fighting)问题。
C. 罗德里格斯公式与四元数的转换
罗德里格斯公式和单位四元数,是绕任意轴旋转的两种等价表达方式,两者可以互相转换。对于绕单位轴 n ⃗ \vec{n} n 旋转 θ θ θ 角,对应的单位四元数为:
q = ( cos θ 2 , n x sin θ 2 , n y sin θ 2 , n z sin θ 2 ) q = (\cos\frac{\theta}{2}, n_x \sin\frac{\theta}{2}, n_y \sin\frac{\theta}{2}, n_z \sin\frac{\theta}{2}) q=(cos2θ,nxsin2θ,nysin2θ,nzsin2θ)
四元数相比旋转矩阵,有存储量小、插值平滑、无万向锁问题的优势,是游戏引擎中旋转的主流表达方式。
D. 非对称投影的工程应用
非对称透视投影不是理论上的边角料,在很多工业级场景中有不可替代的作用:
-
VR 双目渲染:左右眼的视锥体需要做轻微的偏移,用非对称投影实现,而非平移相机;
-
Portal 传送门渲染:传送门的成像需要根据相机和传送门的相对位置,动态计算非对称投影矩阵;
-
多屏拼接渲染:多块屏幕组成的环形幕、折幕,需要用非对称投影保证画面的无缝拼接;
-
平面反射渲染:镜子、水面的反射效果,需要用非对称投影矩阵修正反射相机的视锥体。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)