PHASE 0 · 心智模型

0.3GLSL 语言核心

向量、Swizzle、Uniform 与新手坑

GLSL 语法长得像 C,但它是为「同时操纵几百万个像素」设计的语言:向量是一等公民,类型检查严格到苛刻,数据来源被 uniform / in / out 三个修饰符管得死死的。这一章把语言层面的东西一次讲完,以后所有章节都不再解释语法。

1类型系统一张表

GLSL 的类型不多,常用的就这几个:

类型含义示例
float浮点数,GLSL 的基本粮食float a = 1.0;
int / uint / bool整数 / 无符号 / 布尔int i = 3; bool ok = true;
vec2 / vec3 / vec42/3/4 维浮点向量:坐标、颜色都是它vec3 c = vec3(1.0, 0.5, 0.0);
mat2 / mat3 / mat4方阵,主要用于旋转缩放(1.3 章)mat2 r = mat2(c, -s, s, c);
sampler2D2D 纹理采样器(2.1 章)uniform sampler2D uTex;

构造函数很灵活,这几种写法都合法且常用:

GLSL · 向量构造
vec3 a = vec3(1.0, 0.5, 0.2);   // 逐个给
vec3 b = vec3(0.7);             // 一个值广播到所有分量 → (0.7, 0.7, 0.7)
vec4 c = vec4(a, 1.0);          // 用低维向量拼高维:颜色补 alpha 的标准写法
vec2 d = vec2(c);               // 高维截断成低维(取前两个分量)

2向量:GLSL 的灵魂

最重要的规则一句话:向量之间的算术运算是逐分量(component-wise)的,标量和向量运算时标量自动广播。这让「同时操纵 RGB 三个通道」「同时变换 xy 两个坐标」变成一行代码:

GLSL · 逐分量运算
vec3 a = vec3(1.0, 2.0, 3.0);
vec3 b = vec3(4.0, 5.0, 6.0);

a + b     // (5.0, 7.0, 9.0)
a * b     // (4.0, 10.0, 18.0) ← 逐分量乘,不是点积!
a * 2.0   // (2.0, 4.0, 6.0)   ← 标量广播

// 真正的几何运算要用内置函数:
dot(a, b)       // 32.0,点积 → 求夹角、算光照的核心
length(a)       // 3.74…,向量长度 → 画圆就靠它
distance(a, b)  // 两点距离 = length(a - b)
normalize(a)    // 化为单位长度 → 只要方向不要大小时用
cross(a, b)     // 叉积(仅 vec3)→ 求垂直向量,3D 光照用

几乎所有内置数学函数(sinabspowmixstep……)都同时接受标量和向量,向量版就是对每个分量各算一遍。sin(vec3(1.0, 2.0, 3.0)) 完全合法——1.4 章的调色板公式全靠这一特性才能写得那么短。

3Swizzle:分量重排魔法

向量的分量可以用 .x .y .z .w 访问,也可以任意组合、重复、打乱——这个语法叫 Swizzle,是 GLSL 手感最爽的部分:

GLSL · Swizzle
vec4 color = vec4(1.0, 0.5, 0.2, 1.0);

color.rgb    // vec3(1.0, 0.5, 0.2)  取前三个
color.bgr    // vec3(0.2, 0.5, 1.0)  倒序重排 → 红蓝互换一行搞定
color.rrr    // vec3(1.0, 1.0, 1.0)  重复同一分量 → 快速灰度图
color.xy     // vec2(1.0, 0.5)       xyzw 与 rgba 是同一套东西的两组别名

vec2 p = vec2(0.3, 0.8);
p.yx         // vec2(0.8, 0.3)       交换 xy → 画面沿对角线镜像
p = p.yx;    // swizzle 也能出现在赋值左边

.xyzw.rgba.stpq 三套别名完全等价,选哪套纯看语义:坐标用 xy,颜色用 rgb,纹理坐标传统上用 st。亲眼看一下 swizzle 对画面的影响:

4数据从哪来:uniform / in / out

Shader 是纯函数,它的所有输入都必须显式声明来源。三个修饰符划分了三条数据通道:

修饰符数据从哪来谁设置它每个像素看到的值
uniformCPU 端(Kotlin / JS)你,每帧或按需全体相同(全厂广播)
in上一阶段(Vertex Shader 的 out)光栅化插值后送达各不相同(逐像素插值)
out——本阶段的输出你在 main 里赋值本像素的最终颜色

本书 Demo 固定使用四个 uniform,Android 端(3.3 章)会亲手把它们喂进去:

GLSL · 本书的标准 uniform
uniform vec2  uResolution;  // 画布分辨率(像素)
uniform float uTime;        // 启动以来的秒数 → 一切动画之源
uniform vec2  uMouse;       // 鼠标/触摸位置(像素,y 已翻转为 GL 方向)
uniform sampler2D uTex;     // 输入图像(Phase 2 使用)
Android 视角 类比

uniform 理解成 Compose 里传给 Composable 的参数/State:外部单向传入、内部只读、变化驱动重绘。in/out 则像管线里的数据流。Kotlin 端一行 glUniform1f(loc, time) 就是在「更新 State」。

5函数与流程控制

自定义函数和 C 一样,但有两点差异值得知道:参数默认按值传递,想要「传出」用 out/inout 参数修饰;没有递归(GPU 没有调用栈)。

GLSL · 函数定义
// 普通函数:必须写在 main 之前(或先声明原型)
float circleMask(vec2 p, float radius) {
    return step(length(p), radius);
}

// out 参数:一次算出多个结果
void polarCoords(vec2 p, out float r, out float angle) {
    r = length(p);
    angle = atan(p.y, p.x);
}

流程控制(if / for / while)语法都在,但回想 0.1 的锁步规则:相邻像素走不同分支时,两个分支都会被执行。所以经验法则是:

  • 基于 uniform 的分支随便用(所有像素走同一边,零代价);
  • 基于坐标的「二选一取值」优先用 mix / step 改写;
  • for 循环要用编译期能确定的次数(如 for (int i = 0; i < 8; i++)),噪声、模糊、Ray Marching 都靠它,合法且常用。

6新手坑清单

这张表是给「从别处抄了代码但编译不过 / 效果不对」的你准备的,建议收藏:

症状原因解法
编译错:cannot convert int to float浮点字面量裸奔:写了 1全部带小数点:1.0
编译错:No precision specifiedFragment Shader 缺精度声明precision mediump float;
编译错:'texture2D' no matching function抄了旧版 GLSL 100 的代码300 es 里统一叫 texture()
画面全黑输出忘了 alpha,或颜色算出了负数/NaN先输出纯色排查;检查除零、pow 负底数
画面颠倒 / 效果偏上偏下GL 与 Android 的 y 轴方向相反翻转:uv.y = 1.0 - uv.y(0.2 章)
圆变椭圆坐标各除各的,比例被拉伸统一除以 uResolution.y(0.2 章)
真机上有色带 / 抖动,模拟器正常mediump 精度不够(尤其 uTime 累积大了)关键量升 highp;时间取模循环(1.4 章)
uniform 传了值但没效果变量在代码里没被用到,被编译器优化删除,location 返回 -1不是 bug;真用到了它就会回来

本章小结

  • 常用类型五件套:floatvec2/3/4mat2/3/4sampler2D;构造函数支持广播、拼接、截断。
  • 向量算术逐分量,标量自动广播;几何运算(点积、长度、归一化)用内置函数。
  • Swizzle 可任意取、重排、重复分量,赋值左右两边都能用。
  • 数据三通道:uniform 全厂广播、in 逐像素插值、out 交卷。
  • 分支基于 uniform 免费、基于坐标要谨慎;循环次数需编译期确定;没有递归。

动手练习

  1. 在 Swizzle 实验台里,把三栏分别改成:原图、灰度(提示:.xx 之类不够,试试 vec2((base.x+base.y)*0.5))、上下翻转。
  2. 写一个函数 vec3 flag(vec2 uv),返回横向三等分的三色旗颜色,在 main 里调用它(练习函数定义 + step 组合)。
  3. 故意制造上面坑清单里的前三个编译错误,读一遍你的浏览器 Demo 报出的错误信息——认识报错长相,比避免报错更重要。