Skip to main content

03. 向量与矩阵操作

在 GLSL 中,向量不仅仅是数据容器,它们有一套极其强大的操作语法。

1. 分量访问 (Accessing Components)

你可以用三种方式访问向量的分量,它们在功能上是完全一样的,只是为了语义清晰:

  • x, y, z, w: 用于位置坐标
  • r, g, b, a: 用于颜色
  • s, t, p, q: 用于纹理坐标
vec4 v = vec4(10.0, 20.0, 30.0, 40.0);

float x = v.x; // 10.0
float r = v.r; // 10.0 (完全等价,只是语义不同)

2. 魔法特性:Swizzling (重组)

这是 GLSL 最酷的特性。你可以随意组合分量来创建新的向量。

vec3 color = vec3(1.0, 0.0, 0.5); // 红色略带蓝

// 提取前两个分量
vec2 rg = color.rg; // (1.0, 0.0)
vec2 xy = color.xy; // 同上

// 交换顺序
vec3 bgr = color.bgr; // (0.5, 0.0, 1.0)

// 重复分量
vec3 rrr = color.rrr; // (1.0, 1.0, 1.0)

// 甚至可以用于赋值(掩码)
vec4 dest = vec4(0.0);
dest.xy = vec2(1.0, 2.0); // dest 变成 (1.0, 2.0, 0.0, 0.0)

3. 向量运算

GLSL 自动支持向量的逐分量运算。这意味着你不需要写循环。

逐分量数学运算

vec3 a = vec3(1.0, 2.0, 3.0);
vec3 b = vec3(2.0, 2.0, 2.0);

vec3 c = a + b; // (3.0, 4.0, 5.0)
vec3 d = a * b; // (2.0, 4.0, 6.0) - 注意:这不是点积,是逐项相乘!
vec3 e = a * 2.0; // (2.0, 4.0, 6.0) - 向量乘标量

几何运算 (内置函数)

这些函数是图形学的核心:

  • dot(v1, v2) (点积):

    • 结果是 float
    • 公式:|v1| * |v2| * cos(θ)
    • 几何意义
      • 当向量归一化时:纯粹衡量两个向量方向的相似度(等于夹角的余弦值)。
        • 1.0: 方向相同
        • 0.0: 垂直
        • -1.0: 方向相反
      • 当向量未归一化时:包含长度信息,可用于计算投影长度。
    • 常用于光照计算(详见第 5 章)。
  • cross(v1, v2) (叉积):

    • 结果是 vec3
    • 仅用于 vec3
    • 几何意义:得到一个垂直于 v1 和 v2 所在平面的新向量。常用于计算法线
  • length(v) (长度):

    • 结果是 float
    • 计算向量的模长(勾股定理)。
  • distance(p1, p2) (距离):

    • 结果是 float
    • 等同于 length(p1 - p2)
  • normalize(v) (归一化):

    • 结果是同维度的向量。
    • 保持方向不变,将长度变为 1.0。
    • ⚠️ 重要:涉及方向计算(如光照)时,几乎总是需要先归一化。
    • 💡 直观理解
      • 想象成调节音量:把所有向量的长度(音量)都调到标准的 1.0,但保留方向(歌曲内容)不变。
      • 在光照计算中,我们只关心方向是否一致,如果不归一化,较长的向量会导致光照计算结果偏大(像音量过大一样失真)。

数值限制函数

这些函数用于限制数值范围,防止超出有效区间:

  • clamp(x, min, max) (限制范围):

    • 功能:将值限制在 [min, max] 范围内。
    • 等价于:min(max(x, min), max)
    • 常见用途:限制颜色值在 0.0~1.0 之间。
  • min(a, b) / max(a, b): 返回较小/较大的值。

4. 矩阵运算

矩阵 = 一组基向量(Basis Vectors)

请把矩阵看作三个“新坐标轴”的组合。一个 mat3 矩阵实际上就是 3 个 vec3 列向量 并排站在一起:

mat3 m = mat3(
1.0, 0.0, 0.0, // 第一列:新的 X 轴 (Right)
0.0, 1.0, 0.0, // 第二列:新的 Y 轴 (Up)
0.0, 0.0, 1.0 // 第三列:新的 Z 轴 (Forward)
);

矩阵乘法 = 坐标系变换

当你写 m * v 时,GPU 实际上在做线性组合: $$ \text{结果} = (\text{新 X 轴} \times v.x) + (\text{新 Y 轴} \times v.y) + (\text{新 Z 轴} \times v.z) $$

线性代数视角:列向量派

在 GLSL 中,我们坚定地做“列向量派”!

  • 向量是竖着的列向量。
  • 矩阵在左,向量在右:matrix * vector

5. 实战详解:光照与法线

这是本章最核心的应用场景,将向量运算(点积、归一化)结合起来实现 3D 光照效果。

5.1 光照计算核心公式

Lambert 光照模型的核心公式非常简单:

float brightness = dot(normal, lightDir);
  • normal (法线):表面朝向哪里?
  • lightDir (光线方向):光从哪里来?
  • dot (点积):计算夹角余弦值。
    • 夹角 0° (正对) → 1.0 (最亮)
    • 夹角 90° (垂直) → 0.0 (无光)
    • 夹角 > 90° (背光) → < 0.0 (黑)

📝 编程作业

难度:⭐⭐ 中等 | 预计时间:45-60 分钟

任务要求

  1. 绘制圆形:使用 distance()step()
  2. 径向渐变:使用 length()normalize()
  3. 方向性光照:使用 dot() 创建 Lambert 光照效果

代码框架

// 片段着色器
precision mediump float;

varying vec2 vUv;

void main() {
vec2 center = vec2(0.5, 0.5);

// 任务 1:绘制圆形
float d = distance(vUv, center);
float circle = step(d, 0.3);
vec3 circleColor = vec3(circle);

// 任务 2:径向渐变
float dist = length(vUv - center);
float gradient = 1.0 - dist * 2.0;
gradient = clamp(gradient, 0.0, 1.0); // 限制在 0-1 范围
vec3 gradientColor = vec3(gradient);

// 任务 3:简单光照(Lambert 模型)

// 1. 构造法线 (详见第 5 章)
// 建议 Z 分量设为 0.5 以避免暗部过黑
vec3 normal = normalize(vec3(vUv - center, 0.5));

// 2. 定义光线方向 (从右上方照射)
vec3 lightDir = normalize(vec3(0.5, 0.5, 1.0));

// 3. 计算光照强度 (点积)
float light = dot(normal, lightDir);
light = max(light, 0.0); // 避免负值
vec3 lightColor = vec3(light);

// TODO: 组合这些效果
gl_FragColor = vec4(lightColor, 1.0);
}

normal z 分量通俗理解:从“陡峭山峰”到“平缓土包”

这一段可能有点抽象,我们用一个生活中的例子来理解。

想象你在平地上堆了一个土堆(模拟凸起效果):

  1. 法线 Z 分量很小 (0.1) = 陡峭的山峰

    • 这个土堆非常陡峭,像一面墙。
    • 左边的坡面几乎是垂直的,完全朝向左边。
    • 光线从右边照过来,完全照不到左边的坡面(背光)。
    • 结果:左边一片漆黑。
  2. 法线 Z 分量变大 (0.5) = 平缓的小土包

    • 我们把山峰拍扁,变成一个平缓的土包。
    • 左边的坡面现在只是微微向左倾斜,大部分还是朝上的。
    • 光线从右边照过来,虽然不是直射,但能照到这个缓坡。
    • 结果:左边不再是全黑,而是有淡淡的光照。

代码中的操作:

// 0.1 -> 陡峭 -> 容易出现全黑死角
// 0.5 -> 平缓 -> 光照过渡更柔和
vec3 normal = normalize(vec3(vUv - center, 0.5));

这行代码本质上就是决定你是要造一座“险峰”还是一个“缓坡”。

为什么左下方会全黑?(原理分析)

如果你还是想知道数学原理,看这里:

  1. 左下方区域vUv - center 是负值(比如指向左下)。
  2. Z 分量太小:导致法线主要指向左下方(几乎平行于屏幕)。
  3. 光线方向:从右上方射来。
  4. 冲突:法线指左下,光线指左下(光线的去向),两者几乎相反。
    • 或者说:法线指左下,光线从右上。法线和光线来源方向夹角 > 90°。
  5. 结果:点积为负,光照强度被截断为 0(全黑)。

解决方案:增加 Z 分量(如改为 0.5)。让法线更“竖直”(指向观察者),避免与光线夹角过大。

检查清单

  • 圆形边缘清晰(使用 step
  • 径向渐变平滑过渡
  • 光照效果有明显的明暗变化(调整 Z 分量避免全黑)
  • 所有向量在使用前都进行了归一化 (normalize)

参考资源