← ALL ARTICLES
数学与采样7 MIN READ

空间变换

全库目录221
本文目录15

图形学中的数学问题,难点很少在公式本身,多在数据所在的空间不清楚。同一组三个浮点数可以表示位置、方向、法线或颜色;数值一样,语义未必一样。

向量与几何关系

点和向量

点表示一个位置。向量表示方向和长度,也可以表示两个点之间的位移。

  • 点减点得到向量。
  • 点加向量得到新点。
  • 两个点直接相加通常没有清楚的几何意义。

使用齐次坐标时,位置常写成 (x,y,z,1)(x,y,z,1),方向写成 (x,y,z,0)(x,y,z,0)。因此平移矩阵会影响点,不会影响方向。

齐次坐标

纯三维矩阵乘法只能表达线性变换——旋转、缩放、剪切都保持原点不动,而平移不是线性变换,无法写成 3×33\times3 矩阵与向量的乘积。

把点嵌入四维、令 w=1w=1,平移就变成了四维空间中的剪切,从而可以用一次矩阵乘法表达:

[x′y′z′1]=[100tx010ty001tz0001][xyz1]\begin{bmatrix} x' \\ y' \\ z' \\ 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix}

方向写 w=0w=0 后,平移列与方向相乘恰好为零——“平移不影响方向”不是人为规定,而是这个设计的直接推论。

多出的 ww 还给了透视投影一个安放之处:投影矩阵把视空间深度写进 ww,透视除法 p/w\mathbf p/w 完成近大远小。没有齐次分量,这两件事都要靠矩阵外的特殊步骤处理,管线就无法保持“逐级矩阵乘法”的统一形态。

长度和归一化

向量 v\mathbf v 的长度是:

∥v∥=vx2+vy2+vz2\lVert \mathbf v \rVert = \sqrt{v_x^2+v_y^2+v_z^2}

归一化得到长度为 1 的方向:

v^=v∥v∥\hat{\mathbf v}=\frac{\mathbf v}{\lVert \mathbf v \rVert}

归一化会丢掉原长度。距离、速度等数据不能随便归一化。零向量也不能直接除以长度,代码中需要处理接近零的情况。

点积(Dot Product)

a⋅b=axbx+ayby+azbz\mathbf a\cdot\mathbf b=a_xb_x+a_yb_y+a_zb_z

另一种写法是:

a⋅b=∥a∥∥b∥cos⁡θ\mathbf a\cdot\mathbf b=\lVert\mathbf a\rVert\lVert\mathbf b\rVert\cos\theta

当两个输入都是单位向量时,点积直接表示夹角余弦:

  • 接近 1:方向相近;
  • 接近 0:接近垂直;
  • 接近 -1:方向相反。

Lambert 光照中的 max⁡(0,N⋅L)\max(0,\mathbf N\cdot\mathbf L) 就是在问表面朝向光源的程度。

向量 a\mathbf a 投影到单位方向 b^\hat{\mathbf b} 上的标量长度是 a⋅b^\mathbf a\cdot\hat{\mathbf b}。对应的投影向量是:

proj⁡b(a)=(a⋅b^)b^\operatorname{proj}_{\mathbf b}(\mathbf a) =(\mathbf a\cdot\hat{\mathbf b})\hat{\mathbf b}

[!warning] 易错点 未归一化的点积同时包含角度和长度。不同坐标空间中的方向也不能直接点积。

叉积(Cross Product)

a×b\mathbf a\times\mathbf b 得到一个垂直于两者所在平面的向量。长度是:

∥a×b∥=∥a∥∥b∥sin⁡θ\lVert\mathbf a\times\mathbf b\rVert =\lVert\mathbf a\rVert\lVert\mathbf b\rVert\sin\theta

它可以用来:

  • 根据三角形边构造法线;
  • 构造相机或切线空间基底;
  • 判断二维转向;
  • 计算平行四边形面积。

叉积不满足交换律:a×b=−(b×a)\mathbf a\times\mathbf b=-(\mathbf b\times\mathbf a)。输入顺序和坐标系手性都会改变结果方向。

坐标变换链

矩阵表示变换

4x4 矩阵可以统一表示缩放、旋转和平移。组合变换时,矩阵乘法不满足交换律。代码中的实际顺序还受行向量/列向量约定影响,不能只看“行主序或列主序”猜乘法方向。

通常会看到这些变换:

text
Local/Object Space
  -- Model Matrix --> World Space
  -- View Matrix  --> View Space
  -- Projection   --> Clip Space
  -- Perspective Divide --> NDC
  -- Viewport Transform  --> Screen Space

Model Matrix

把模型局部坐标变到世界坐标。它通常由平移、旋转和缩放组合。

View Matrix

把世界变到相机空间。可以理解为相机世界变换的逆,而不是把相机本身继续往前移动。

Projection Matrix

把视空间的可见体映射到裁剪空间。透视投影会让距离影响屏幕大小,正交投影不会。

Clip Space、NDC 和透视除法

顶点乘投影矩阵后得到齐次裁剪坐标 (xc,yc,zc,wc)(x_c,y_c,z_c,w_c)。GPU 在裁剪后做透视除法:

(xndc,yndc,zndc)=(xcwc,ycwc,zcwc)(x_{ndc},y_{ndc},z_{ndc})= \left(\frac{x_c}{w_c},\frac{y_c}{w_c},\frac{z_c}{w_c}\right)

这一步才得到 NDC。Clip Space 和 NDC 不是同一个空间。

不同 API 的 NDC 深度范围和屏幕 Y 方向可能不同。写跨平台 Shader 时应使用引擎提供的变换函数,或者明确当前 API 约定。

顶点阶段的 MVP

下面的 HLSL 顶点阶段采用列向量约定,把对象空间位置依次变换到世界、观察和裁剪空间。分开保存中间量有助于检查矩阵顺序和空间归属:

hlsl
VSOutput VSMain(VSInput input)
{
    VSOutput output;
    float4 positionWS = mul(Model, float4(input.positionOS, 1.0));
    float4 positionVS = mul(View, positionWS);
    output.positionCS = mul(Projection, positionVS);
    output.positionWS = positionWS.xyz;
    return output;
}

矩阵放在左侧还是右侧取决于向量约定和编译设置。代码、CPU 数学库和上传转置必须使用同一套规则,不能只凭表达式外观判断。

存储布局与乘法约定

这里有两个独立约定:矩阵按行还是按列存放,以及向量写成行向量右乘还是列向量左乘。上传接口和编译器再决定怎样解释内存。只改存储方式、却没有同步解释方式,就可能把矩阵当成它的转置,物体的位置和旋转因此出错。

对应关系:行向量结果的转置满足(vTM)T=MTv(\mathbf v^TM)^T=M^T\mathbf v,其中右侧的v\mathbf v为列向量。也就是说,同一份矩阵数据,把乘法写法换边的同时把矩阵转置,结果不变;只换一边,结果就是错的。

跨层排查的顺序是:先确认 CPU 数学库的存储约定,再确认上传到 GPU 时是否转置,最后确认 Shader 里 mul 的参数顺序。三层各核对一次,比盯着表达式猜快得多。

单点的完整投影

用列向量、相机朝+Z、零到一深度与左下UV作为这一算例的约定。对象点(1,0,0)先沿X放大2倍,再平移(0,0,5),得到世界点(2,0,5)。相机世界位置为(1,0,0)、旋转为单位,逆相机变换得到视空间点(1,0,5)。这一步若误用相机正变换,会得到(3,0,5)。

取90°垂直视场、宽高比1、近面1、远面11。投影令clip.x=x、clip.y=y、clip.z=1.1*z−1.1、clip.w=z,所以裁剪坐标为(1,0,4.4,5),透视除法得到NDC(0.2,0,0.88)。UV为(0.6,0.5),100×100目标上的连续边界坐标为(60,50)。这是投影位置,不声明该点恰好命中某个像素中心;覆盖仍由光栅器判断。

沿这条链保存中间量,可以区分对象变换错、相机逆变换错和透视除法错。模型点若变成方向(1,0,0,0),相同模型矩阵只留下X缩放,不会被平移到z=5。

法线的逆转置变换

法线表示一个平面的垂直约束。非均匀缩放会改变平面的形状。如果直接用模型矩阵变换法线,变换后的法线可能不再垂直于表面切线。

法线应使用模型矩阵线性部分的逆转置矩阵:

n′=(M−1)Tn\mathbf n'=(M^{-1})^T\mathbf n

如果线性部分是旋转乘非零统一缩放,两种变换在归一化后给出相同方向。非均匀缩放会破坏这个关系;缩放为零时矩阵不可逆,应当由资产或变换系统拒绝这类输入。

垂直关系的保持

设切向量tt与法线nn满足nTt=0n^Tt=0。几何变换后切线成为MtMt,要让新法线继续垂直,取n′=M−Tnn'=M^{-T}n,则(n′)TMt=nTt=0(n')^TMt=n^Tt=0。逆转置来自这条垂直约束。

例如t=(1,1,0)t=(1,1,0)、n=(1,−1,0)n=(1,-1,0),模型沿X放大两倍。新切线为(2,1,0),直接变换法线得到(2,-1,0),点积为3;逆转置得到(0.5,-1,0),点积为0。最后归一化只改变长度,保留零点积。这个例子可在CPU逐项计算,无需依赖引擎的矩阵布局。

本页HLSL为顶点阶段片段,VSInput、VSOutput、常量缓冲与顶点绑定由宿主提供,尚未独立编译或执行。这里的空间推导不覆盖奇异矩阵;透视除法也要求对裁剪结果按图形管线约定处理。

二维精灵的枢轴与投影

屏幕空间 2D 场景常使用正交投影,把左上或左下角坐标直接映射到裁剪空间。一个精灵的对象矩阵通常按“平移到目标位置、移动到枢轴、旋转、缩放单位 Quad、移回枢轴”的矩阵组合构造,列向量实际按右侧到左侧应用:

cpp
Mat4 model = Translate(position)
           * Translate(pivot * size)
           * RotateZ(rotationRadians)
           * Translate(-pivot * size)
           * Scale(size);
Mat4 clipFromObject = Orthographic(0, width, height, 0, -1, 1) * model;

这里的单位 Quad 位于 [0,1]2[0,1]^2,position、size、正交投影边界都使用像素单位,pivot 位于 [0,1]2[0,1]^2。矩阵乘法顺序需要按数学库约定调整。批处理时每个精灵只上传变换、UV Rect、颜色和层级,所有实例共享 Quad 顶点和材质。验证可使用非正方形图片、中心与角落枢轴、90 度旋转和窗口缩放;Y 轴约定错误会表现为上下翻转,枢轴顺序错误会表现为绕世界原点旋转。

验证方法

  • 在场景中画出位置、法线、切线和光照方向的 Gizmo。
  • 把方向映射到 RGB,检查轴向是否符合预期。
  • 暂时把模型设置为非均匀缩放,观察法线变换是否仍正确。
  • 在 RenderDoc 中检查 Vertex Shader 输入、输出和常量矩阵。
  • 明确矩阵是在 CPU 端转置,还是 Shader 编译器按布局解释。

相关主题

参考资料

  • PBRT第四版3.9节,齐次坐标与变换表示;2026-10-22核读。
  • Eric Lengyel, Foundations of Game Engine Development, Volume 1: Mathematics.
  • Unity Manual, Built-in shader variables.
  • LearnOpenGL, src/1.getting_started/5.1.transformations and 6.1.coordinate_systems.