向量运算 — 着色器的几何基础

向量是着色器中最核心的数据类型。从光照计算到法线贴图,从顶点变换到 TBN 矩阵,几乎所有图形学运算都建立在向量操作之上。本课将深入理解点积与叉积这两个最关键的向量运算。

本课目标

  • 理解向量在着色器中的角色
  • 学会配置法线作为输入输出(语义绑定)
  • 掌握向量的几何意义:方向、长度、归一化
  • 理解点积 (Dot Product) 及其在光照中的应用
  • 理解叉积 (Cross Product) 及其在 TBN 构建中的应用

1. 向量在着色器中的角色

在图形学中,向量无处不在:

向量维度作用
顶点位置 (Position)float3/float4定义物体的几何形状
法线 (Normal)float3垂直于表面,决定光照方向
切线 (Tangent)float4沿纹理 U 方向,用于法线贴图
UV 坐标float2纹理映射坐标
光照方向float3从片元指向光源
视线方向float3从片元指向摄像机

标量 vs. 向量

  • 标量 (Scalar):只有一个值,如 floathalf。表示”多少”(如温度、质量)。
  • 向量 (Vector):有多个值,如 float2float3float4。表示”方向和大小”(如速度、力)。 法线是向量——它有方向(朝哪)和大小(长度通常为 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 · ba × 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. 练习与回顾

思考题

  1. 方向 vs. 位置:为什么法线的 W 分量设为 0,而顶点位置的 W 分量设为 1?
  2. 点积的值域:NdotL 在什么时候等于 1?什么时候等于 0?什么时候为负?负值代表什么物理含义?
  3. 叉积方向cross(tangent, normal)cross(normal, tangent) 的结果有什么区别?
  4. 投影应用:点积可以用来计算”一个向量在另一个向量上的投影”。你能想出一个图形学中的具体应用场景吗?
  5. 动手实验:创建一个 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)