GLSL 语法长得像 C,但它是为「同时操纵几百万个像素」设计的语言:向量是一等公民,类型检查严格到苛刻,数据来源被 uniform / in / out 三个修饰符管得死死的。这一章把语言层面的东西一次讲完,以后所有章节都不再解释语法。
1类型系统一张表
GLSL 的类型不多,常用的就这几个:
| 类型 | 含义 | 示例 |
|---|---|---|
| float | 浮点数,GLSL 的基本粮食 | float a = 1.0; |
| int / uint / bool | 整数 / 无符号 / 布尔 | int i = 3; bool ok = true; |
| vec2 / vec3 / vec4 | 2/3/4 维浮点向量:坐标、颜色都是它 | vec3 c = vec3(1.0, 0.5, 0.0); |
| mat2 / mat3 / mat4 | 方阵,主要用于旋转缩放(1.3 章) | mat2 r = mat2(c, -s, s, c); |
| sampler2D | 2D 纹理采样器(2.1 章) | uniform sampler2D uTex; |
构造函数很灵活,这几种写法都合法且常用:
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 两个坐标」变成一行代码:
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 光照用
几乎所有内置数学函数(sin、abs、pow、mix、step……)都同时接受标量和向量,向量版就是对每个分量各算一遍。sin(vec3(1.0, 2.0, 3.0)) 完全合法——1.4 章的调色板公式全靠这一特性才能写得那么短。
3Swizzle:分量重排魔法
向量的分量可以用 .x .y .z .w 访问,也可以任意组合、重复、打乱——这个语法叫 Swizzle,是 GLSL 手感最爽的部分:
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 是纯函数,它的所有输入都必须显式声明来源。三个修饰符划分了三条数据通道:
| 修饰符 | 数据从哪来 | 谁设置它 | 每个像素看到的值 |
|---|---|---|---|
| uniform | CPU 端(Kotlin / JS) | 你,每帧或按需 | 全体相同(全厂广播) |
| in | 上一阶段(Vertex Shader 的 out) | 光栅化插值后送达 | 各不相同(逐像素插值) |
| out | ——本阶段的输出 | 你在 main 里赋值 | 本像素的最终颜色 |
本书 Demo 固定使用四个 uniform,Android 端(3.3 章)会亲手把它们喂进去:
uniform vec2 uResolution; // 画布分辨率(像素)
uniform float uTime; // 启动以来的秒数 → 一切动画之源
uniform vec2 uMouse; // 鼠标/触摸位置(像素,y 已翻转为 GL 方向)
uniform sampler2D uTex; // 输入图像(Phase 2 使用)
把 uniform 理解成 Compose 里传给 Composable 的参数/State:外部单向传入、内部只读、变化驱动重绘。in/out 则像管线里的数据流。Kotlin 端一行 glUniform1f(loc, time) 就是在「更新 State」。
5函数与流程控制
自定义函数和 C 一样,但有两点差异值得知道:参数默认按值传递,想要「传出」用 out/inout 参数修饰;没有递归(GPU 没有调用栈)。
// 普通函数:必须写在 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 specified | Fragment 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;真用到了它就会回来 |
本章小结
- 常用类型五件套:
float、vec2/3/4、mat2/3/4、sampler2D;构造函数支持广播、拼接、截断。 - 向量算术逐分量,标量自动广播;几何运算(点积、长度、归一化)用内置函数。
- Swizzle 可任意取、重排、重复分量,赋值左右两边都能用。
- 数据三通道:
uniform全厂广播、in逐像素插值、out交卷。 - 分支基于 uniform 免费、基于坐标要谨慎;循环次数需编译期确定;没有递归。
动手练习
- 在 Swizzle 实验台里,把三栏分别改成:原图、灰度(提示:
.xx之类不够,试试vec2((base.x+base.y)*0.5))、上下翻转。 - 写一个函数
vec3 flag(vec2 uv),返回横向三等分的三色旗颜色,在 main 里调用它(练习函数定义 + step 组合)。 - 故意制造上面坑清单里的前三个编译错误,读一遍你的浏览器 Demo 报出的错误信息——认识报错长相,比避免报错更重要。