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=PVMplocal

本文会把这个公式的每一步拆解得明明白白,从几何意义到公式推导,从易混点辨析到引擎实践,带你理解图形学变换的核心逻辑。

一、Model 矩阵:把模型从局部空间「搬进」世界空间

Model 矩阵(简称 M 矩阵)的核心任务:将模型局部坐标系下的顶点坐标,转换为世界坐标系下的坐标,公式表达为:

p w o r l d = M ⋅ p l o c a l p_{world} = M \cdot p_{local} pworld=Mplocal

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 xyz 三个轴分别对坐标进行缩放,缩放因子为 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} Splocal= 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θ00sinθcosθ00001

  • 绕y轴旋转θ角的旋转矩阵(注意符号与 x 、 z x、z xz 轴相反,源于右手系坐标轴叉乘顺序):

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θ0sinθ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θ​0​0sinθcosθ0000100001​

核心性质:旋转矩阵是正交矩阵,满足 R T = R − 1 R^T = R^{-1} RT=R1 ,即旋转矩阵的转置等于其逆矩阵。这个性质在后续 View 矩阵的推导中至关重要。

(3)平移矩阵 T

平移变换的核心,是给 x 、 y 、 z x、y、z xyz 三个轴分别加上偏移量 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)= ​1​0​0​001000010txtytz1​

推导验证:对点 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} Tp= ​1​0​0​001000010txtytz1​ 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=TRS

核心规则:矩阵乘法的执行顺序,是从右到左。也就是说,顶点会先执行缩放(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°,结果天差地别。

  1. 先旋转后平移(T·R):
  2. 旋转后点变为 ( 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
  3. 先平移后旋转(R·T):
  4. 平移后点变为 ( 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 矩阵的哪一侧,在列向量右乘的约定下:

  1. 在 M 矩阵右侧乘旋转矩阵 M n e w = M o l d ⋅ R n e w M_{new} = M_{old} \cdot R_{new} Mnew=MoldRnew

几何意义:绕模型当前的局部轴旋转。因为旋转先于M矩阵中已有的平移和旋转执行,是在模型局部空间中完成的。

  1. 在 M 矩阵左侧乘旋转矩阵 M n e w = R n e w ⋅ M o l d M_{new} = R_{new} \cdot M_{old} Mnew=RnewMold

几何意义:绕世界坐标系的固定轴旋转。因为旋转后于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=parentWorldMatrixlocalMatrix

这种做法的优势非常明显:

  1. 避免矩阵连乘带来的浮点误差积累;
  2. 方便编辑器直接修改位置、旋转、缩放参数,无需做矩阵逆运算;
  3. 完美适配场景的父子层级系统,层级关系清晰可控。

二、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=Vpworld

经过View变换后,坐标空间从世界空间切换到观察空间(相机空间),这个空间的原点不再是世界原点,而是相机的位置 x 、 y 、 z x、y、z xyz 轴也不再是世界坐标轴,而是相机自身的右、上、后方向轴。

2.2 核心推导:View 矩阵 = 相机位姿矩阵的逆

我们先定义:相机在世界坐标系中的位姿矩阵为 C C C ,这个 C C C 和模型的 M 矩阵完全等价——它描述了相机局部坐标系如何嵌入到世界坐标系中,同样可以拆分为平移和旋转:

C = T c ⋅ R c C = T_c \cdot R_c C=TcRc

其中 T c T_c Tc 是相机在世界中的平移矩阵, R c R_c Rc 是相机在世界中的旋转矩阵。

现在,我们要把世界坐标系下的点,转换为相机坐标系下的点,本质上就是做一次「坐标系的逆变换」。如果相机的位姿是「先旋转,再平移到世界中的某个位置」,那么要把世界点转换到相机空间,就需要做完全相反的操作:先把世界整体平移,让相机回到世界原点;再把世界整体旋转,让相机的坐标轴和世界坐标轴对齐

用矩阵的语言表达,就是 View 矩阵等于相机位姿矩阵的逆:

V = C − 1 V = C^{-1} V=C1

根据矩阵逆的运算性质 ( A B ) − 1 = B − 1 A − 1 (AB)^{-1}=B^{-1}A^{-1} (AB)1=B1A1 ,我们可以把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=(TcRc)1=Rc1Tc1

这里就用到了旋转矩阵的核心性质:旋转矩阵是正交矩阵, R c − 1 = R c T R_c^{-1}=R_c^T Rc1=RcT 。所以最终 V 矩阵可以写成:

V = R c T ⋅ T c − 1 V = R_c^T \cdot T_c^{-1} V=RcTTc1

这也是所有教程里 View 矩阵的标准形式,它的几何意义无比清晰:先对世界做逆平移( T c − 1 T_c^{-1} Tc1 ),把相机挪到原点;再对世界做逆旋转( R c T R_c^T RcT ),让相机的坐标轴与世界坐标轴对齐。

2.3 相机旋转矩阵的线性代数本质与完整构造

这是绝大多数教程的跳步重灾区,我们从线性代数「基变换」的根源出发,推导相机旋转矩阵的完整构造逻辑,讲清「为什么旋转矩阵的行是 u 、 v 、 w u、v、w uvw」「为什么逆旋转是转置」。

前置核心定理:坐标系变换 = 基变换

线性代数中,同一个向量,在不同坐标系下的坐标转换,本质是基向量的变换。我们先明确两个核心坐标系的基向量:

  • 世界坐标系的标准正交基(世界基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 ,都可以用任意一组基的线性组合来表示。比如:

  1. 用世界基表示: 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

  1. 用相机基表示: p ⃗ = x ′ u + y ′ v + z ′ w \vec{p} = x' u + y' v + z' w p =xu+yv+zw ,其中 ( 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] xyz

因为是同一个向量,两个表达式完全相等:

[ 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] xyz

由于世界基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} p world

我们把右边的 [ u v w ] \begin{bmatrix} u & v & w \end{bmatrix} [uvw] 定义为相机基变换矩阵 C C C ,它的列向量,就是相机的三个基向量 u 、 v 、 w u、v、w uvw 在世界坐标系下的坐标。此时公式简化为:

p ⃗ ∗ w o r l d = C ⋅ p ⃗ v i e w \vec{p}*{world} = C \cdot \vec{p}_{view} p world=Cp view

关键结论1:相机旋转矩阵 R c R_c Rc 就是基变换矩阵 C C C

这个公式的几何意义非常明确:已知一个向量在相机坐标系下的坐标 p ⃗ v i e w \vec{p}_{view} p view ,乘上基变换矩阵 C C C ,就能得到它在世界坐标系下的坐标 p ⃗ w o r l d \vec{p}_{world} p world 。而这,正是相机旋转矩阵 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 uvw

关键结论2:逆旋转矩阵 R v i e w = R c − 1 = R c T R_{view}=R_c^{-1}=R_c^T Rview=Rc1=RcT

我们的核心需求是反过来:已知世界坐标 p ⃗ w o r l d \vec{p}_{world} p world ,求它在相机坐标系下的坐标 p ⃗ v i e w \vec{p}_{view} p view 。因此我们对上面的公式两边同时左乘 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} C1p world=p view

这里就用到了正交矩阵的核心性质:由单位正交基构成的基变换矩阵,一定是正交矩阵,而正交矩阵的逆矩阵,等于它的转置矩阵,即 C − 1 = C T C^{-1}=C^T C1=CT

C C C 的列是 u 、 v 、 w u、v、w uvw ,转置之后,行就变成了 u 、 v 、 w u、v、w uvw ,因此我们得到了逆旋转矩阵(也就是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 p worldeye

这个「相对位置」,就是我们上面推导的、需要做旋转变换的向量!因此,完整的View变换分为两步,逻辑顺序绝对不能颠倒:

  1. 平移变换:把世界坐标系的原点,平移到相机的位置,也就是给所有世界点减去 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 p translated=p worldeye

  2. 旋转变换:把这个相对位置,从世界基变换到相机基,也就是乘上我们推导的逆旋转矩阵 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} p view=Rviewp translated

    用矩阵乘法的形式表达,平移变换对应的逆平移矩阵 T v i e w = T c − 1 T_{view}=T_c^{-1} Tview=Tc1 ,它的作用是给所有点平移 − 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= ​1​0​0​001000010exeyez1​

由于矩阵乘法的执行顺序是「从右到左」,我们需要先执行平移,再执行旋转,因此完整的View矩阵必须写成:

V = R v i e w ⋅ T v i e w V = R_{view} \cdot T_{view} V=RviewTview

2.4 相机基底的正交归一化完整步骤

到这里,我们只解决了旋转矩阵的形式问题,还有一个最核心的问题没解决:怎么根据相机的参数,构造出合法的正交归一基 u 、 v 、 w u、v、w uvw

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)

    这里有一个巨大的坑:很多人直接拿gazeup作为相机的z轴和y轴,这是完全错误的。因为gazeup往往不是正交的,直接用的话,会导致相机基底倾斜,旋转矩阵不再是正交矩阵,最终渲染出来的画面会出现拉伸、扭曲。

    正确的做法,是先对这三个向量做归一化 + 正交化,构造出一组正交归一的相机基底(右手系),这一步是View变换的核心,也是最容易被省略的关键步骤。

    相机基底的完整构造步骤

    我们约定:相机的局部坐标系, x x x 轴为右方向, y y y 轴为上方向, z z z 轴为后方向(相机看向 − z -z z 轴,符合 GAMES101 / OpenGL 的右手系约定)。

  1. 构造相机 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 轴,保证基底的归一性。

  2. 构造相机 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 向量的倾斜问题。

  3. 构造相机 y y y 轴( v v v

    现在我们已经有了正交的 x x x u u u z z z w w w,只需要再做一次叉乘,就能得到真正正交于 x 、 z x、z xz 轴的相机上方向 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 uvw 构成一组完美的右手正交归一基底

    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=RviewTview= uxvxwx​0uyvywy0uzvzwz0ueyeveyeweye1​

    其中 u ⋅ e y e u \cdot eye ueye 是向量 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= 1000010000100051 ,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=RviewTview= 1000010000100051

    第三步:计算世界原点的观察空间坐标

    世界原点 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=Vpworld= 0051

    结果完全符合预期:世界原点在相机的正前方5个单位处,对应相机空间的 z = − 5 z=-5 z=5(相机看向 − z -z z 轴),完美验证了 View 矩阵的正确性。

    2.6 最常见的认知误区纠正

  1. 误区1:View 变换后,原点还是世界原点

    正确理解:View 变换后,观察空间的原点是相机的位置,所有坐标都是相对相机描述的。

  2. 误区2: u p up up 向量不正交会导致画面中心偏移

    正确理解:只要通过正交化步骤构造了正确的相机基底,View 变换会完全修正 u p up up 向量的偏差;画面是否偏移,是后续投影矩阵的参数决定的,和 View 变换无关。

  3. 误区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] xyz[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= ​1​0​0​0010000102l+r2b+t2n+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}] [2rl,2rl] ,y轴范围是 [ − t − b 2 , t − b 2 ] [-\frac{t-b}{2}, \frac{t-b}{2}] [2tb,2tb] ,z轴范围是 [ − n − f 2 , n − f 2 ] [-\frac{n-f}{2}, \frac{n-f}{2}] [2nf,2nf] 。我们需要把这个范围缩放到[-1,1],所以三个轴的缩放因子分别为 2 r − l \frac{2}{r-l} rl2 2 t − b \frac{2}{t-b} tb2 2 n − f \frac{2}{n-f} nf2 ,对应的缩放矩阵为:

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= rl2​0​0​00tb20000nf200001​

完整的正交投影矩阵

和 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=SorthoTortho= rl2​0​0​00tb20000nf20rll+rtbb+tnfn+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= r1​0​0​00t10000nf2000nfn+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= 42​0​0​0022000099.9200099.9100.11​ = ​0.5​0​0​00100000.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=Morthopview=(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课程里给出了一个天才的拆解思路:

  1. 第一步:Persp→Ortho:把透视的截头锥,「压成」一个和正交投影一样的长方体,保留透视的「近大远小」特性;

  2. 第二步: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=MorthoMpersportho

在这里插入图片描述

3.3 Persp→Ortho 矩阵的完整构造

这是透视投影的核心难点,我们从几何意义、约束条件、公式联立到结果验证,理清 Persp→Ortho 矩阵的每一个元素的由来。

前置核心目标与约束条件

Persp→Ortho 变换的核心目标:把透视投影的截头锥(Frustum),变换成一个轴对齐的长方体(正交投影的视锥体),同时必须满足以下5个不可违背的约束

  1. 近平面上的所有点,经过变换后, x 、 y 、 z x、y、z xyz 坐标完全不变(近平面是成像平面,不能有任何变形);

  2. 远平面上的所有点,经过变换后, z z z 坐标完全不变, x 、 y x、y xy 坐标按比例缩放,保证远平面边界与近平面对齐;

  3. 变换必须可以用 4 × 4 4×4 4×4 齐次矩阵表示(仿射变换),能和其他变换矩阵组合;

  4. 变换后,点的深度顺序保持不变(离相机近的点, z z z 值更小,保证深度缓冲正确);

  5. 变换后的 x 、 y x、y xy 坐标必须满足透视的「近大远小」特性,即与深度 − z -z z 成反比(相似三角形的核心结论)。

    步骤1:从相似三角形推导透视核心公式

    透视投影的「近大远小」,本质上是相似三角形的几何规律,我们用二维截面做推导:

    二维截面( y − z y - z yz 平面):相机在相机空间的原点 O ( 0 , 0 ) O(0, 0) O(0,0),看向 − z -z z 轴;近平面是直线 z = n ( n < 0 ) z = n(n < 0) z=nn<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|} OAON=APNP

其中:

  • ∣ 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} zn=yy

    交叉相乘后,得到近平面上的投影 x x x 坐标:

    y ′ = n ⋅ y z y' = \frac{n \cdot y}{z} y=zny

    x x x 方向与 y y y 方向完全对称,同理可得:

    x ′ = n ⋅ x z x' = \frac{n \cdot x}{z} x=znx

    这就是透视投影最核心的公式:投影后的 x 、 y x、y xy 坐标,与深度 z z z 成反比 z z z 越小(离相机越远), x 、 y x、y xy 越小,物体看起来就越小,完美实现了「近大远小」的效果。

    步骤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=nx+0y+0z+01=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=0x+ny+0z+01=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=0x+0y+1z+01=z

    步骤3:确定第三行( Z Z Z 分量)的形式

    现在只剩下矩阵的第三行,也就是 Z Z Z 分量的变换规则还没确定。这里有两个核心约束,决定了第三行的形式:

  1. 深度值只能和 z z z 有关,不能和 x 、 y x、y xy 有关:如果 Z Z Z 分量和 x 、 y x、y xy 相关,那么同一个深度 z z z 的点,会因为 x 、 y x、 y xy 不同得到不同的 Z Z Z 值,深度缓冲会完全失效,这是绝对不允许的。因此第三行的 x 、 y x、y xy 对应的系数必须为0。

  2. 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=Az+B1

    至此,我们得到了 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} Mpersportho= n​0​0​00n0000A100B0​

    步骤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} Mpersporthopnear= n0+00+0n+01​00+n0+0n+01​00+00+An+B1​00+00+1n+01​ = 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} Mpersporthopfar= n0+00+0f+01​00+n0+0f+01​00+00+Af+B1​00+00+1f+01​ = 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} Mpersportho= n​0​0​00n0000n+f100nf0​

    步骤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} Mp= nxny(n+f)nnfn = nxnyn2+nfnfn = 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} Mp= nxny(n+f)fnff = nxnynf+f2nff = 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 xy 坐标缩放了 n f \frac{n}{f} fn 倍,而截头锥的远平面 x 、 y x、y xy 范围恰好是近平面的 f n \frac{f}{n} nf 倍,缩放后远平面的 x 、 y x、y xy 范围和近平面完全一致,截头锥完美变成了长方体,实现了 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=MorthoMpersportho= rl2n​0​0​00tb2n00lrl+rbtb+tnfn+f100fn2nf0​

    常用简化形式:当视锥体对称时( 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=ntan(2fovY),b=t,r=taspect,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= aspecttan(2fovY)1​0​0​00tan(2fovY)10000nfn+f100fn2nf0​

3.5 关键区别:正交 vs 透视的非对称窗口,为什么不能等价互换

很多人会发现,正交投影里的非对称窗口,完全可以等价为「平移相机 + 对称正交窗口」,但透视投影里,这个等价关系完全不成立,这是为什么?

核心原因在于两者的成像原理完全不同:

  • 正交投影:成像射线是平行的, x 、 y x、y xy 的成像结果和深度 z z z 完全无关。非对称窗口只是平移了可见的长方体,只要平移相机,就能让对称窗口的可见区域和非对称窗口完全一致。

  • 透视投影:成像射线是从相机原点出发的汇聚射线, x 、 y x、y xy 的成像结果和深度 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 width1 h e i g h t − 1 height - 1 height1;第二,屏幕原点到底取左下角还是左上角。如果把连续窗口坐标和离散像素索引混在一起讨论,最后公式往往会前后不一致。这里统一采用:左下角为原点的窗口坐标系(因此 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=MviewportpndcMviewport= 2width​0​0​002height00002102width2height211​

    补充说明:透视投影的 NDC 深度是非线性的,因此映射后的z也保持非线性,这能保证近平面的深度精度更高,避免远平面深度冲突(Z-Fighting)。

    4.2 为什么这里不用 w i d t h − 1 width - 1 width1 h e i g h t − 1 height - 1 height1

    很多资料会把公式写成 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)2width1 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)2height1 。这种写法并不是绝对错误,但它对应的语义已经发生了变化:它更像是在把 NDC 直接映射到离散像素索引,也就是把最左边像素当作 0,把最右边像素当作 w i d t h − 1 width - 1 width1

    而这一节讨论的是图形学管线里的 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 轴),两者的核心差异在于:

  1. 旋转正方向的定义不同;

  2. 投影矩阵的 z z z 轴符号、深度范围不同(OpenGL深度范围 [ − 1 , 1 ] [-1,1] [1,1],DirectX深度范围 [ 0 , 1 ] [0,1] [0,1]);

  3. 相机 z z z 轴的方向相反,View 矩阵和投影矩阵的 z z z 轴相关项需要做符号调整。

    B. 透视除法与深度缓冲

    透视投影矩阵变换后,我们得到的是裁剪空间坐标,必须经过透视除法(即 x 、 y 、 z x、y、z xyz 分别除以 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. 非对称投影的工程应用

    非对称透视投影不是理论上的边角料,在很多工业级场景中有不可替代的作用:

  4. VR 双目渲染:左右眼的视锥体需要做轻微的偏移,用非对称投影实现,而非平移相机;

  5. Portal 传送门渲染:传送门的成像需要根据相机和传送门的相对位置,动态计算非对称投影矩阵;

  6. 多屏拼接渲染:多块屏幕组成的环形幕、折幕,需要用非对称投影保证画面的无缝拼接;

  7. 平面反射渲染:镜子、水面的反射效果,需要用非对称投影矩阵修正反射相机的视锥体。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐