Chapter 6: Linear Algebra Essentials
线性代数在图形学中承担的是数据约定:顶点位置、法线、相机、光照方向、材质参数和屏幕坐标都要通过向量、矩阵或更高维数组进入 GPU 管线。读这一章的目标是能在一个 draw call 里定位每个量属于哪类数据、处在哪个空间、被哪段 shader 改写、最终影响哪种画面结果。
本章用一个贯穿例子展开:CPU 提交一个带漫反射光照的立方体 draw call,vertex buffer 提供 position、normal、UV,uniform buffer 提供 model、view、projection 和 normal matrix,fragment shader 使用 normal、light direction 与材质纹理生成颜色。这个例子足够小,但已经覆盖图形数学最常见的数据路径:顶点从模型空间进入屏幕,法线进入光照计算,纹理和材质以数组形态参与采样,深度缓冲记录投影后的结果。
读完本章后,读者应能追踪一次坐标变换,判断矩阵乘法顺序是否与 shader 约定一致,区分 position、direction、normal、UV 和材质张量的角色,解释 dot、cross、normalize、mul 等向量运算在 shader 中的边界,并把抖动、Z-fighting、法线错误、NaN 亮斑这类视觉现象放回精度和矩阵条件问题中排查。
6.1 Vector Matrix Tensor Roles in Graphics Data
向量是图形数据的最小空间载体。一个 position 通常写成三维向量,因为它表示模型空间中的点;一个 normal 也是三维向量,但它表示表面方向;一个 UV 是二维向量,因为它定位纹理参数域;一个 color 可以是三维或四维向量,因为它表达颜色通道和 alpha。向量的组件数量来自被表达对象的自由度,语义来自它在管线中的位置。W3C 的 WGSL Vector Types 把向量定义为 2、3 或 4 个标量组件的组合,这个语言定义解释了 shader 类型系统的表面形态;图形工程还要继续追问这个向量表示点、方向、纹理坐标、颜色还是中间计算量。
在贯穿例子中,立方体的每个顶点至少包含 position: vec3<f32>、normal: vec3<f32> 和 uv: vec2<f32>。这三类数据同在 vertex buffer 中,含义完全不同。position 会经过 model、view、projection 进入 clip space;normal 会经过 normal matrix 进入世界或视图空间参与光照;uv 通常沿着光栅化插值进入 fragment shader,再用于纹理采样。调试一个画面错误时,第一步要把“同样是向量”的数据拆回它的空间和用途。
矩阵是成批变换向量的规则。一个 model matrix 把模型空间坐标变到世界空间;一个 view matrix 把世界空间坐标变到相机空间;一个 projection matrix 把相机空间坐标变到 clip space。W3C 的 WGSL Matrix Types 把矩阵描述为浮点向量的组合,并指出矩阵的关键用途是表达线性变换。图形管线中的 4×4 矩阵还会利用齐次坐标容纳平移和投影,所以工程上看到 mat4x4<f32> 时,应同时检查它表达的空间转换、乘法方向、内存布局和 CPU / GPU 两端约定。
张量在本章采用工程语义:它是多维数据块,可以表示图像、材质表、体数据、G-buffer、神经渲染特征或批量几何属性。二维纹理可以看作高为 H、宽为 W、通道数为 C 的数据块;一组 PBR 材质参数可以看作按材质、参数、贴图通道排列的数据集合;ML 图形管线中的 feature map 也以多维数组形态流动。shader 语言通常通过 texture、buffer、array 或结构体访问这些数据,图形工程的判断重点是它们如何被采样、过滤、压缩、缓存和绑定。
这三类对象在一个 frame 中承担不同责任:向量携带单个几何或像素相关量,矩阵描述空间之间的变换,张量承载可被索引、采样或批处理的大块数据。把它们混成“数学对象”会丢失管线位置。更稳定的检查顺序是:先问数据来自 vertex buffer、uniform buffer、texture 还是 storage buffer,再问它处在哪个坐标空间或资源布局中,接着问它在 shader 的哪个阶段被消费,最后观察它改变了位置、光照、采样还是颜色输出。
6.2 线性变换与几何意义
线性变换在渲染中的作用是把几何从一个可编辑空间送到一个可绘制空间。建模软件导出的顶点通常位于模型空间,场景系统用 model matrix 给它位置、朝向和缩放,相机系统用 view matrix 把世界转到相机视角,投影矩阵把相机空间点转成 clip space。GPU 后续执行 perspective divide,把 clip coordinate 的 x、y、z 除以 w,再进入 NDC、viewport 和 depth buffer。
下面的路径只描述本章贯穿例子的主线:一个立方体 draw call 如何把顶点和法线送入屏幕与光照。
这张图的关键点是位置和法线从 model space 出发后会分流。位置继续经过 projection 生成屏幕覆盖和深度,法线进入光照计算并影响颜色。排查“模型位置正确但明暗异常”时,应重点检查 normal path;排查“光照正确但模型飞出屏幕”时,应重点检查 position path。两条路径共享部分矩阵,却有不同的输出语义。
平移、旋转、缩放和投影分别改变几何的不同属性。平移改变点的位置,方向向量通常使用齐次分量 w = 0,因此不受平移影响;旋转改变点和方向的朝向;缩放改变长度,非均匀缩放还会扭曲法线;投影改变 w 并为透视除法准备数据。把点写成 (x, y, z, 1),把方向写成 (x, y, z, 0),可以解释为什么同一个 4×4 矩阵对 position 和 direction 的效果不同。
vertex shader 中的最小变换可以写成下面这种形式。示例使用 WGSL 风格,只展示关键数据路径,具体 binding 号和结构体布局由工程决定。
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) normal: vec3<f32>,
@location(2) uv: vec2<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) world_normal: vec3<f32>,
@location(1) uv: vec2<f32>,
@location(2) world_position: vec3<f32>,
};
@vertex
fn vertex_main(input: VertexInput) -> VertexOutput {
let local_position = vec4<f32>(input.position, 1.0);
let world_position = uniforms.model * local_position;
var output: VertexOutput;
output.clip_position = uniforms.projection * uniforms.view * world_position;
output.world_normal = normalize(uniforms.normal_matrix * input.normal);
output.uv = input.uv;
output.world_position = world_position.xyz;
return output;
}
这段代码验证了三个判断。第一,position 需要使用 w = 1,因为它要接受平移。第二,normal 使用单独的 normal_matrix,因为非均匀缩放后直接使用 model matrix 的 3×3 部分会让光照方向偏离真实表面方向。第三,projection * view * model * position 的书写只在当前矩阵约定下成立;如果引擎使用行向量、转置存储或 HLSL 的另一套调用习惯,乘法方向和 CPU 上传布局必须一起检查。
几何意义比公式更适合作为调试入口。模型在屏幕中左右翻转,通常先检查坐标系 handedness、负 scale 和矩阵乘法顺序;模型深度异常,先检查 projection、near/far、depth range 和 clip-space 约定;法线贴图明暗反转,先检查 tangent space、normal matrix 和通道方向。每个现象都要回到“哪个向量经过哪个矩阵进入哪个空间”这个问题。
6.3 矩阵分解与图形学常用操作
矩阵分解的工程价值是把一个合成变换拆回可检查量。渲染引擎里常见的 model matrix 来自 translation、rotation、scale 的组合,场景层级还会把父节点和子节点矩阵继续相乘。最终传入 shader 的矩阵已经是一组浮点数,调试时需要把它拆回位置、朝向、缩放、镜像、剪切或投影成分,才能判断错误来自动画、相机、导入、骨骼还是 shader 约定。
TRS 分解最常用于对象变换和动画姿态。Translation 可以从 4×4 矩阵的平移列或平移行中读取,具体位置取决于矩阵约定;scale 可以从基向量长度估计;rotation 可以在移除 scale 后转换为四元数或旋转矩阵。这个过程要求矩阵接近“平移 × 旋转 × 缩放”的结构。导入资产中如果存在 shear、负 scale、父子节点叠加的非均匀缩放,普通 TRS 分解会产生跳变或符号歧义,动画曲线和包围盒计算都会受到影响。
相机控制也依赖矩阵分解。一个 view matrix 本质上描述“世界如何进入相机空间”,工程调试时常把它反解为相机世界位置、forward、right、up 三个方向。相机飞行、orbit、billboard、screen-space effect 都需要这些方向向量。相机抖动时,先检查世界位置数值范围,再检查 view matrix 的基向量是否仍接近单位长度和互相垂直;如果基向量长度漂移,后续投影和屏幕空间计算会持续放大误差。
法线校正依赖矩阵的逆和转置。对一个表面来说,法线需要保持与切平面垂直。非均匀缩放会改变切向量长度和夹角,直接用 model matrix 变换 normal 会破坏这种垂直关系。常用做法是使用 transpose(inverse(mat3(model))) 生成 normal matrix,再在 shader 中归一化结果。实际工程会在 CPU 侧预计算它,或在对象变换更新时缓存它,减少 vertex shader 重复求逆的成本。
矩阵分解还服务数值诊断。基向量点积接近 0,说明它们接近正交;基向量长度接近 1,说明旋转部分接近纯旋转;行列式接近 0,说明矩阵接近不可逆;行列式为负,通常表示包含镜像或奇数次负 scale。一个模型突然消失时,检查 model、view、projection 的行列式、平移量级、scale 量级和 w 分布,往往比直接盯着最终屏幕结果更快。
在贯穿例子中,如果立方体使用 scale(2, 1, 1) 后明暗像被拉歪,排查顺序是:确认 position path 使用完整 model matrix,确认 normal path 使用 normal matrix,确认 normal matrix 与 model 同步更新,确认 shader 对变换后的 normal 执行归一化。这个顺序把矩阵分解、法线变换和 shader 运算连接到同一个画面症状。
6.4 在 Shader 代码中高效使用向量运算
shader 中的向量运算应围绕输入语义和执行粒度组织。一次 vertex invocation 处理一个顶点,一次 fragment invocation 处理一个片元,dot、cross、normalize 和矩阵乘法会在大量 invocation 上重复执行。内建函数通常由编译器和 GPU 后端优化,但它们仍然会产生 ALU 指令、特殊函数单元开销、寄存器压力或精度边界。写 shader 时要同时回答两个问题:这个运算在几何上表达什么,这个运算在 GPU 上重复了多少次。
dot 的几何意义是投影和夹角度量。在 Lambert 漫反射中,dot(N, L) 表示光线方向在表面法线方向上的投影;在视锥裁剪和半空间测试中,点积可以判断点位于平面哪一侧;在切线空间中,点积可以把向量投到某个基方向上。WGSL Numeric Built-in Functions 中的 dot 返回两个同长度向量的点积,工程上应保证两个输入处在同一空间,并且法线和光照方向已经按预期归一化。
cross 的几何意义是构造垂直方向。它常用于从两个边向量生成几何法线,从 tangent 和 normal 生成 bitangent,或根据相机方向构建 billboard 基向量。cross(a, b) 的方向受坐标系 handedness 和参数顺序影响。参数交换会让结果反向,负 scale 或镜像 UV 也可能让 tangent space 的符号需要额外处理。它只适合三维向量语义,拿二维 UV 或四维颜色做叉积没有图形意义。
normalize 的几何意义是保留方向、丢弃长度。法线、光照方向、视线方向和反射方向经常需要单位长度,因为后续 dot、反射和 BRDF 计算默认方向向量长度稳定。WGSL 对 normalize 的定义指出它返回同方向单位向量,并且零向量不在定义域内。工程中对可能为零的输入应先设置最小长度、默认方向或上游约束,否则 NaN 会通过颜色、深度或后处理扩散成闪烁点、整块黑色或平台相关结果。
下面的 fragment shader 片段展示了本章贯穿例子的最小光照计算。它只表达法线、光照方向、材质颜色和输出颜色的关系。
@fragment
fn fragment_main(input: VertexOutput) -> @location(0) vec4<f32> {
let normal = normalize(input.world_normal);
let light_direction = normalize(scene.light_position - input.world_position);
let diffuse = max(dot(normal, light_direction), 0.0);
let albedo = textureSample(material.base_color, material_sampler, input.uv).rgb;
return vec4<f32>(albedo * diffuse, 1.0);
}
这段代码的检查点很集中。input.world_normal 和 light_direction 都要处在世界空间;dot 的结果需要限制到非负区间,因为背光面不贡献 Lambert 漫反射;textureSample 的结果是材质张量在 UV 位置的采样;最终颜色把几何方向和材质颜色合成。若模型旋转后明暗没有跟着变,优先检查 normal matrix 和 normal 所在空间;若光源移动后高亮跳变,优先检查 light direction 的长度、空间和零长度输入。
矩阵乘法的高效使用重点在约定统一和阶段选择。对象级矩阵、view-projection 矩阵、normal matrix 可以在 CPU 或每对象更新阶段预计算,再通过 uniform buffer 传入 shader;每顶点重复计算 projection * view * model 会增加 ALU 和寄存器开销。对 skinned mesh,骨骼矩阵确实要在 vertex shader 中按权重组合,但骨骼数量、权重数量和矩阵读取方式会影响 bandwidth 与寄存器压力。把“能在对象级固定的量”提前算好,把“必须随顶点变化的量”留给 shader,是处理向量运算成本的基本边界。
精度选择也属于向量运算边界。f32 是通用 shader 浮点类型,f16 可以降低带宽和寄存器压力,但需要 API feature、硬件支持和误差预算。颜色、某些法线编码、屏幕空间特效可能适合较低精度;世界坐标、相机相对位置、深度相关计算和矩阵累积通常需要更高精度或更严格的范围控制。判断标准应落到误差传播:这个量的误差是否会进入几何位置、深度比较、法线方向或时间累积。
6.5 数值稳定性及精度误差分析
图形中的数值问题通常先表现为画面症状,再回到浮点、矩阵和深度路径定位。浮点数用有限位数表示连续实数,数值越大,相邻可表示值之间的间隔越大。WGSL 的 Floating Point Evaluation 明确讨论了舍入、溢出、ULP、重关联和函数精度边界;图形工程中的直接结论是:同一段数学公式在 GPU 上得到的是有限精度结果,矩阵连乘、深度投影、归一化和三角函数都会把误差带入画面。
大世界坐标是最常见的精度来源之一。假设场景坐标已经达到十万量级,而角色或相机还要表现厘米级运动,f32 的有效精度会让小位移和大平移混在同一个数中。画面上会出现相机附近物体抖动、阴影边缘跳动、粒子相对位置不稳。稳定做法是采用 camera-relative rendering、origin rebasing 或分块坐标,把 GPU shader 中参与矩阵计算的位置量级压回相机附近的小范围。
深度精度来自 projection 和 depth buffer 的组合。透视投影下,深度分布通常在 near plane 附近更密,far plane 附近更稀。near 设置得过小、far 设置得过大,会让大量可见表面争用有限深度精度,表现为 Z-fighting、阴影 acne、远处模型闪烁。排查顺序是先记录 near/far、depth format 和 depth range,再缩小可见距离范围或提高 near,然后观察同一 frame 中重叠表面的深度稳定性。高级方案如 reversed-Z 也服务同一个目标:把有限深度值分配给更需要精度的距离范围。
矩阵病态会把输入误差放大。一个 scale 接近 0 的矩阵会让 inverse 和 normal matrix 对微小误差极端敏感;大量父子节点矩阵连续相乘会让旋转基向量逐渐偏离正交;骨骼动画中权重异常会让顶点位置被拉到远处。排查时不要只看最终矩阵是否存在,还要检查基向量长度、基向量之间的点积、行列式、平移量级和是否出现 NaN / Infinity。能被反复复现的画面错误,通常可以通过这些标量诊断量定位到具体变换阶段。
累计误差来自时间更新和重复变换。每帧把当前矩阵继续乘一个小旋转,长时间运行后旋转部分会产生漂移;每帧把位置加上速度,长时间运行后误差会累积到碰撞和相机;粒子模拟、骨骼姿态和相机轨道也会出现类似问题。更稳定的做法是保存规范化状态,例如位置、四元数、scale 和时间参数,每帧从状态重建矩阵;对旋转四元数定期归一化;对基向量做正交化;对模拟使用固定时间步长或明确的误差预算。
把这些问题放回贯穿例子,可以得到一套可复用排查顺序。立方体抖动时,先看 world position 量级和 camera-relative 处理;立方体明暗异常时,看 normal matrix、normalize 输入和空间一致性;立方体与地面闪烁时,看 projection near/far、depth format 和重叠面距离;颜色中出现闪点时,看除法、normalize 零向量、纹理采样输入和 NaN 传播。每一步都把视觉症状连接到一个数学量、一个 shader 输入或一个 GPU 资源状态。
最小自检任务
给定一个简单渲染场景:一个立方体使用 model = translate(100000.0, 0.0, 0.0) * rotateY(t) * scale(2.0, 1.0, 1.0),相机使用透视投影,near = 0.01,far = 100000.0。vertex shader 使用 projection * view * model * vec4(position, 1.0) 输出位置,但使用 mat3(model) * normal 输出法线。fragment shader 中计算 normalize(light_position - world_position),光源有时与片元世界位置非常接近。画面现象包括:立方体边缘随相机轻微抖动,地面接触处闪烁,旋转后明暗不自然,偶尔出现白色闪点。
请按本章的判断顺序说明:哪些数据是向量、矩阵或张量;哪些问题来自变换路径;哪些问题来自 shader 向量运算;哪些问题来自数值精度;每个问题应优先检查什么证据。
答案要点
position、normal、uv、light direction 和 color 都是向量,但它们分别表达点、方向、纹理坐标、光照方向和颜色通道,检查时要先标注空间和用途。model、view、projection和 normal matrix 是矩阵,负责把顶点从模型空间送到屏幕路径,或把法线送到光照路径。材质纹理属于多维采样数据,可以按张量型资源理解。- 立方体边缘抖动优先指向大世界坐标和
f32精度范围,应检查 world position 量级、camera-relative 处理和 view matrix 中的大平移。 - 地面接触处闪烁优先指向深度精度,应检查
near = 0.01与far = 100000.0的比例、depth format、重叠面距离和 projection 设置。 - 旋转后明暗不自然优先指向法线路径。
scale(2.0, 1.0, 1.0)是非均匀缩放,normal 应使用transpose(inverse(mat3(model)))生成的 normal matrix,并在 shader 中归一化。 - 白色闪点优先检查
normalize(light_position - world_position)的输入长度。光源与片元位置接近时可能产生零向量或极小向量,应设置最小长度、默认方向或上游约束。 - 最终排查顺序是:标注数据类型和空间,分开 position path 与 normal path,检查矩阵约定和分解诊断量,检查 shader 内建函数输入域,再检查深度范围与大坐标精度。
本章知识点总结
- 向量角色:向量在图形管线中表达点、方向、UV、颜色或中间量,判断时必须同时标注空间和用途。
- 矩阵角色:矩阵表达空间变换规则,model、view、projection 和 normal matrix 分别服务不同管线路径。
- 张量角色:纹理、材质表、体数据和图形特征可按多维数据块理解,shader 通过资源绑定、索引或采样访问它们。
- 贯穿路径:位置从模型空间经过世界、视图、裁剪、NDC 和屏幕,法线在世界或视图空间参与光照。
- 齐次坐标:
w = 1的点接受平移,w = 0的方向保留方向语义,投影矩阵会改变后续透视除法。 - 法线矩阵:非均匀缩放下应使用 normal matrix 变换法线,并在 shader 中重新归一化。
- 矩阵分解:TRS、行列式、基向量长度和正交性可以把黑箱矩阵拆成可诊断的工程量。
- 点积用途:
dot用于投影、夹角、光照和半空间测试,输入向量需要处在同一空间。 - 叉积用途:
cross用于构造三维垂直方向,结果方向受参数顺序和坐标系 handedness 影响。 - 归一化边界:
normalize要求输入长度稳定,零向量或极小向量会把 NaN 或平台相关结果带入画面。 - 运算阶段:对象级固定量适合提前计算,随顶点或片元变化的量留在对应 shader 阶段处理。
- 浮点精度:大坐标、小位移、矩阵连乘和函数近似都会把有限精度误差转化成可见画面症状。
- 深度精度:near/far、depth format 和投影方式共同决定 Z-fighting、远处闪烁和阴影稳定性。
- 排查顺序:先标注数据来源、空间和用途,再分离 position path、normal path、采样路径和深度路径。