空间变换
全库目录221
本文目录15
图形学中的数学问题,难点很少在公式本身,多在数据所在的空间不清楚。同一组三个浮点数可以表示位置、方向、法线或颜色;数值一样,语义未必一样。
向量与几何关系
点和向量
点表示一个位置。向量表示方向和长度,也可以表示两个点之间的位移。
- 点减点得到向量。
- 点加向量得到新点。
- 两个点直接相加通常没有清楚的几何意义。
使用齐次坐标时,位置常写成 ,方向写成 。因此平移矩阵会影响点,不会影响方向。
齐次坐标
纯三维矩阵乘法只能表达线性变换——旋转、缩放、剪切都保持原点不动,而平移不是线性变换,无法写成 矩阵与向量的乘积。
把点嵌入四维、令 ,平移就变成了四维空间中的剪切,从而可以用一次矩阵乘法表达:
方向写 后,平移列与方向相乘恰好为零——“平移不影响方向”不是人为规定,而是这个设计的直接推论。
多出的 还给了透视投影一个安放之处:投影矩阵把视空间深度写进 ,透视除法 完成近大远小。没有齐次分量,这两件事都要靠矩阵外的特殊步骤处理,管线就无法保持“逐级矩阵乘法”的统一形态。
长度和归一化
向量 的长度是:
归一化得到长度为 1 的方向:
归一化会丢掉原长度。距离、速度等数据不能随便归一化。零向量也不能直接除以长度,代码中需要处理接近零的情况。
点积(Dot Product)
另一种写法是:
当两个输入都是单位向量时,点积直接表示夹角余弦:
- 接近 1:方向相近;
- 接近 0:接近垂直;
- 接近 -1:方向相反。
Lambert 光照中的 就是在问表面朝向光源的程度。
向量 投影到单位方向 上的标量长度是 。对应的投影向量是:
[!warning] 易错点 未归一化的点积同时包含角度和长度。不同坐标空间中的方向也不能直接点积。
叉积(Cross Product)
得到一个垂直于两者所在平面的向量。长度是:
它可以用来:
- 根据三角形边构造法线;
- 构造相机或切线空间基底;
- 判断二维转向;
- 计算平行四边形面积。
叉积不满足交换律:。输入顺序和坐标系手性都会改变结果方向。
坐标变换链
矩阵表示变换
4x4 矩阵可以统一表示缩放、旋转和平移。组合变换时,矩阵乘法不满足交换律。代码中的实际顺序还受行向量/列向量约定影响,不能只看“行主序或列主序”猜乘法方向。
通常会看到这些变换:
Local/Object Space
-- Model Matrix --> World Space
-- View Matrix --> View Space
-- Projection --> Clip Space
-- Perspective Divide --> NDC
-- Viewport Transform --> Screen SpaceModel Matrix
把模型局部坐标变到世界坐标。它通常由平移、旋转和缩放组合。
View Matrix
把世界变到相机空间。可以理解为相机世界变换的逆,而不是把相机本身继续往前移动。
Projection Matrix
把视空间的可见体映射到裁剪空间。透视投影会让距离影响屏幕大小,正交投影不会。
Clip Space、NDC 和透视除法
顶点乘投影矩阵后得到齐次裁剪坐标 。GPU 在裁剪后做透视除法:
这一步才得到 NDC。Clip Space 和 NDC 不是同一个空间。
不同 API 的 NDC 深度范围和屏幕 Y 方向可能不同。写跨平台 Shader 时应使用引擎提供的变换函数,或者明确当前 API 约定。
顶点阶段的 MVP
下面的 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 数学库和上传转置必须使用同一套规则,不能只凭表达式外观判断。
存储布局与乘法约定
这里有两个独立约定:矩阵按行还是按列存放,以及向量写成行向量右乘还是列向量左乘。上传接口和编译器再决定怎样解释内存。只改存储方式、却没有同步解释方式,就可能把矩阵当成它的转置,物体的位置和旋转因此出错。
对应关系:行向量结果的转置满足,其中右侧的为列向量。也就是说,同一份矩阵数据,把乘法写法换边的同时把矩阵转置,结果不变;只换一边,结果就是错的。
跨层排查的顺序是:先确认 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。
法线的逆转置变换
法线表示一个平面的垂直约束。非均匀缩放会改变平面的形状。如果直接用模型矩阵变换法线,变换后的法线可能不再垂直于表面切线。
法线应使用模型矩阵线性部分的逆转置矩阵:
如果线性部分是旋转乘非零统一缩放,两种变换在归一化后给出相同方向。非均匀缩放会破坏这个关系;缩放为零时矩阵不可逆,应当由资产或变换系统拒绝这类输入。
垂直关系的保持
设切向量与法线满足。几何变换后切线成为,要让新法线继续垂直,取,则。逆转置来自这条垂直约束。
例如、,模型沿X放大两倍。新切线为(2,1,0),直接变换法线得到(2,-1,0),点积为3;逆转置得到(0.5,-1,0),点积为0。最后归一化只改变长度,保留零点积。这个例子可在CPU逐项计算,无需依赖引擎的矩阵布局。
本页HLSL为顶点阶段片段,VSInput、VSOutput、常量缓冲与顶点绑定由宿主提供,尚未独立编译或执行。这里的空间推导不覆盖奇异矩阵;透视除法也要求对裁剪结果按图形管线约定处理。
二维精灵的枢轴与投影
屏幕空间 2D 场景常使用正交投影,把左上或左下角坐标直接映射到裁剪空间。一个精灵的对象矩阵通常按“平移到目标位置、移动到枢轴、旋转、缩放单位 Quad、移回枢轴”的矩阵组合构造,列向量实际按右侧到左侧应用:
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 位于 ,position、size、正交投影边界都使用像素单位,pivot 位于 。矩阵乘法顺序需要按数学库约定调整。批处理时每个精灵只上传变换、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.transformationsand6.1.coordinate_systems.