向量运算 — 着色器的几何基础
向量是着色器中最核心的数据类型。从光照计算到法线贴图,从顶点变换到 TBN 矩阵,几乎所有图形学运算都建立在向量操作之上。本课将深入理解点积与叉积这两个最关键的向量运算。
本课目标
- 理解向量在着色器中的角色
- 学会配置法线作为输入输出(语义绑定)
- 掌握向量的几何意义:方向、长度、归一化
- 理解点积 (Dot Product) 及其在光照中的应用
- 理解叉积 (Cross Product) 及其在 TBN 构建中的应用
1. 向量在着色器中的角色
在图形学中,向量无处不在:
| 向量 | 维度 | 作用 |
|---|---|---|
| 顶点位置 (Position) | float3/float4 | 定义物体的几何形状 |
| 法线 (Normal) | float3 | 垂直于表面,决定光照方向 |
| 切线 (Tangent) | float4 | 沿纹理 U 方向,用于法线贴图 |
| UV 坐标 | float2 | 纹理映射坐标 |
| 光照方向 | float3 | 从片元指向光源 |
| 视线方向 | float3 | 从片元指向摄像机 |
标量 vs. 向量
- 标量 (Scalar):只有一个值,如
float、half。表示”多少”(如温度、质量)。- 向量 (Vector):有多个值,如
float2、float3、float4。表示”方向和大小”(如速度、力)。 法线是向量——它有方向(朝哪)和大小(长度通常为 1)。顶点位置是标量的组合——它只是空间中的一个点。
2. 配置输入输出(语义绑定)
要在着色器中使用法线,必须在 struct 中通过语义 (Semantics) 声明它们。
2.1 顶点输入结构 (appdata)
2.2 顶点输出 / 片元输入结构 (v2f)
为什么 v2f 中不用 NORMAL 语义? 根据 HLSL 规范,片元着色器阶段没有 NORMAL 语义 。因此我们需要使用
TEXCOORD[n]这样的通用语义来传递法线数据。TEXCOORD1 有四个分量 (XYZW),足够存储法线的三个分量。
2.3 在顶点着色器中连接
2.4 变换到世界空间
光照计算通常需要在世界空间 (World Space) 中进行。因为光源的方向、位置都是相对于世界原点定义的。因此我们需要将法线从对象空间变换到世界空间:
为什么 W 分量设为 0? 法线是方向 而不是位置 。将 W 设为 0 会使矩阵的平移部分失效(任何数 × 0 = 0),只保留旋转和缩放效果。这是将"方向向量"与"位置向量"区分的关键。
3. 向量的几何意义
向量是一条有长度 (Magnitude) 和方向 (Direction) 的箭头。
沿 X 轴指向右方
长度 = 1
沿 Y 轴指向上方
长度 = 1
指向右上方 45°
长度 = √2 ≈ 1.414
3.1 向量长度 (Magnitude)
也叫”模”。计算公式:
向量长度公式
||v|| = √(Vx² + Vy² + Vz²)
在 HLSL 中直接用 length(v) 函数。内部实现为 sqrt(dot(v, v))。
3.2 归一化 (Normalize)
将向量的长度变为 1,保持方向不变。归一化后的向量称为单位向量。
为什么法线总是被 normalize? 法线最重要的属性是它的方向 (朝哪),而不是长度。经过插值和矩阵变换后,法线的长度会发生改变,所以在使用前必须重新归一化,确保所有法线长度一致,否则光照计算会出现误差。
4. 点积 (Dot Product) — 光照计算的基础
4.1 数学定义
点积公式
a · b = ||a|| × ||b|| × cos(θ)
其中 θ 是向量 a 和 b 之间的夹角。
在代码中:点积返回一个标量(单一数值)。
a · b = 1
两向量同向 → 满光照
a · b = 0
两向量垂直 → 零光照
a · b = -1
两向量反向 → 背光面
4.2 手算示例
设 a = (1, 1), b = (1, 1):
4.3 HLSL 中的点积
4.4 图形学应用:漫反射光照
点积在图形学中最重要的应用就是漫反射 (Diffuse) 光照计算:
NdotL 的含义
NdotL(Normal dot Light)是图形学中最常见的缩写之一。它代表"法线和光照方向的点积",值越大说明表面越正对光源,接收的光照越多。这是兰伯特 (Lambertian) 漫反射模型 的核心。
4.5 投影 (Projection)
点积的另一个重要几何意义是投影:a · b 表示向量 a 在向量 b 方向上的投影长度(乘以 b 的长度)。这在计算镜面反射和阴影时非常有用。
5. 叉积 (Cross Product) — 法线和 TBN 构建的基础
5.1 数学定义
叉积公式
a × b = ||a|| × ||b|| × sin(θ) × n
其中 n 是垂直于 a 和 b 的单位向量(遵循右手定则)。
在代码中:叉积返回一个向量,这个向量垂直于两个输入向量。
5.2 手算示例
设 a = (1, 0, 0), b = (0, 1, 0),求 a × b:
c.x = (ay × bz) - (az × by)
c.y = (az × bx) - (ax × bz)
c.z = (ax × by) - (ay × bx)
c.x = (0 × 0) - (0 × 1) =
c.y = (0 × 0) - (1 × 0) =
c.z = (1 × 1) - (0 × 0) =
结果 (0, 0, 1) 同时垂直于 a (X 轴) 和 b (Y 轴)——即 Z 轴方向。
5.3 HLSL 中的叉积
5.4 图形学应用:构建 TBN 矩阵
叉积最重要的应用之一是构建 TBN 矩阵(Tangent-Bitangent-Normal 矩阵),用于法线贴图 (Normal Mapping)。
叉积的不可交换性
a × b = -(b × a)。交换两个向量的顺序,结果的方向会反转。在使用叉积时一定要确认方向是正确的,否则法线贴图会"凹变凸、凸变凹"。
6. 点积 vs. 叉积 对比
| 对比维度 | 点积 (Dot) | 叉积 (Cross) |
|---|---|---|
| 输入 | 两个向量 | 两个向量 |
| 输出 | 标量(一个数) | 向量(垂直于输入) |
| 数学符号 | a · b | a × b |
| 几何意义 | 夹角余弦 × 长度积 | 垂直向量 × 长度积 × sin(θ) |
| HLSL 函数 | dot(a, b) | cross(a, b) |
| 主要用途 | 光照计算、投影 | TBN 矩阵、法线计算 |
| 运算代价 | 低(乘加) | 中(多次乘减) |
7. 综合示例:简单光照着色器
将点积、叉积和向量变换综合起来,创建一个简单的”自定义漫反射 + 边缘光”着色器:
8. 本课总结
| 概念 | 要点 |
|---|---|
| 向量 vs. 标量 | 向量有方向和大小,标量只有一个数值 |
| 法线配置 | appdata 中用 NORMAL 语义,v2f 中用 TEXCOORD1 传递 |
| 归一化 | 将向量长度变为 1,保持方向。法线使用前必须 normalize |
| 点积 (dot) | 返回标量,衡量两向量”对齐程度”,光照计算核心 |
| 叉积 (cross) | 返回垂直向量,TBN 矩阵构建核心 |
| 世界空间变换 | 法线需从对象空间变换到世界空间,W 分量设为 0 |
9. 练习与回顾
思考题
- 方向 vs. 位置:为什么法线的 W 分量设为 0,而顶点位置的 W 分量设为 1?
- 点积的值域:NdotL 在什么时候等于 1?什么时候等于 0?什么时候为负?负值代表什么物理含义?
- 叉积方向:
cross(tangent, normal)和cross(normal, tangent)的结果有什么区别? - 投影应用:点积可以用来计算”一个向量在另一个向量上的投影”。你能想出一个图形学中的具体应用场景吗?
- 动手实验:创建一个 Unlit Shader,在片元着色器中计算 NdotL(模拟光照方向为 (1,1,0)),并用
max(0, NdotL)乘以纹理颜色显示结果。
10. 教材对照
教材对照
本课对应《The Unity Shaders Bible》第 II 章 (p.177-189):
- 5.0.1 配置输入输出 (p.177-182)
- 5.0.2 向量 (p.182-184)
- 5.0.3 点积 (p.184-187)
- 5.0.4 叉积 (p.187-189)