05. 着色器结构与控制流
GLSL 的代码结构很像 C 语言,但为了性能,它有一些特殊的限制。
1. 程序入口:main()
每个着色器(无论是顶点还是片段)都必须有一个 main 函数。这是程序执行的起点。它没有返回值 (void)。
void main() {
// 你的逻辑代码
}
2. 函数定义
你可以定义自己的函数来组织代码。
- 必须在调用之前定义(或声明原型)。
- 支持函数重载(同名但参数不同)。
// 定义一个计算亮度的函数
float getBrightness(vec3 color) {
return dot(color, vec3(0.299, 0.587, 0.114));
}
void main() {
vec3 col = vec3(1.0, 0.5, 0.0);
float b = getBrightness(col);
}
输入输出参数修饰符:
in: 输入参数(默认,传值)。out: 输出参数(函数内修改会影响外部)。inout: 既输入又输出。
void doubleValue(inout float x) {
x = x * 2.0;
}
3. 控制流:If-Else
支持标准的 if-else,但在早期的 GPU 或特定的优化场景下,过多的分支会导致性能下降(Warp Divergence)。但在现代 WebGL 中,简单的分支完全没问题。
if (distance(vUv, vec2(0.5)) < 0.2) {
gl_FragColor = vec4(1.0); // 白色圆形
} else {
gl_FragColor = vec4(0.0); // 黑色背景
}
⚠️ 性能提示:
- 尽量避免导致不同像素执行不同代码路径的复杂分支。
- 如果是简单的 0/1 切换,尝试使用
step()或mix()等数学函数代替if。
替代 If 的数学方法(无分支编程):
假设我们想画一个圆,圆内白色(1.0),圆外黑色(0.0)。
// ❌ 方式 A:使用 if (产生分支)
// 可能导致同一组 warp 内的线程等待,效率较低
float c = 0.0;
float dist = distance(vUv, vec2(0.5));
if (dist < 0.2) {
c = 1.0;
} else {
c = 0.0;
}
// ✅ 方式 B:使用 step (数学运算,无分支)
// 所有的线程都执行相同的指令,效率极高
// step(edge, x): 如果 x < edge 返回 0.0,否则返回 1.0
// step(dist, 0.2): 如果 dist < 0.2 返回 1.0 (注意参数顺序)
float c = step(dist, 0.2);
// ✅ 方式 C:使用 mix (线性混合)
// 适合需要渐变的情况
float c = mix(0.0, 1.0, step(dist, 0.2));
4. 循环:For Loop
GLSL 支持 for 循环,但有严格限制:
循环次数必须是编译时常量。
这意味着编译器必须在编译代码时就知道循环会跑多少次。你不能用一个 uniform 变量作为循环终止条件(在旧版 GLSL ES 1.0 / WebGL 1.0 中尤其如此)。
// ✅ 正确:固定次数
for (int i = 0; i < 5; i++) {
color += texture2D(uTex, uv + offset[i]);
}
// ✅ 正确:常量定义
const int MAX_STEPS = 100;
for (int i = 0; i < MAX_STEPS; i++) {
// ...
}
// ❌ 错误(在某些 WebGL 环境):依赖变量
uniform int uCount;
for (int i = 0; i < uCount; i++) { // 循环次数不确定
// ...
}
5. 精度限定符 (Precision Qualifiers)
在 WebGL 的片段着色器顶部,你通常会看到这样一行:
precision mediump float;
这是告诉 GPU 使用什么精度来存储浮点数。
highp: 高精度(开销大,手机可能不支持)。顶点着色器默认是这个。mediump: 中精度(最常用,够用且快)。片段着色器通常设为这个。lowp: 低精度(颜色计算够用,位置计算不够)。
📝 编程作业
难度:⭐ 简单 | 预计时间:40 分钟
任务要求
练习 GLSL 的控制流和函数定义,理解性能优化的方法。
具体要求
- 自定义函数:创建一个函数,计算两个颜色的混合
- If-Else 版本:使用
if-else创建分区域的颜色效果 - 无分支版本:将
if-else改写为使用step()或mix()的数学函数版本 - For 循环:使用
for循环创建重复图案(如条纹)
代码框架
// 片段着色器
precision mediump float;
varying vec2 vUv;
// 任务 1:自定义函数
vec3 mixColors(vec3 color1, vec3 color2, float t) {
return mix(color1, color2, t);
}
void main() {
// 任务 2:if-else 版本
vec3 color;
if (vUv.x < 0.5) {
color = vec3(1.0, 0.0, 0.0); // 红色
} else {
color = vec3(0.0, 0.0, 1.0); // 蓝色
}
// 任务 3:无分支版本(使用 step)
vec3 colorNoBranch = mix(
vec3(1.0, 0.0, 0.0),
vec3(0.0, 0.0, 1.0),
step(0.5, vUv.x)
);
// 任务 4:for 循环创建条纹
//
// 💡 简单版本(推荐初学者):
// 创建简单的垂直条纹:每隔一段距离切换一次颜色
//
// 原理拆解:
// 1. vUv.x * 5.0:将 UV 的 x 坐标放大 5 倍(0-1 变成 0-5)
// 2. fract(...):取小数部分(0-5 变成重复的 0-1)
// 3. step(0.5, ...):如果 > 0.5 返回 1,否则返回 0(创建黑白条纹)
// 结果:5 条垂直条纹(黑白相间)
float stripes = step(0.5, fract(vUv.x * 5.0));
// 💡 进阶版本(使用循环):
// 用循环创建多个条纹,每个条纹有不同的偏移
// float stripes = 0.0;
// const int numStripes = 5;
// for (int i = 0; i < numStripes; i++) {
// float stripe = step(0.5, fract(vUv.x * 10.0 + float(i) * 0.2));
// stripes += stripe;
// }
// stripes = stripes / float(numStripes);
// 组合效果
vec3 finalColor = mixColors(colorNoBranch, vec3(stripes), 0.5);
gl_FragColor = vec4(finalColor, 1.0);
}
检查清单
- 自定义函数正确定义和使用
-
if-else版本可以正常工作 - 无分支版本效果相同
-
for循环创建了重复图案 - 循环次数是编译时常量
性能对比
尝试对比 if-else 版本和无分支版本的性能(虽然在这个简单例子中差异可能不明显,但理解原理很重要)。
扩展挑战
- 🌟 创建一个更复杂的分支逻辑,然后改写为无分支版本
- 🌟 使用
for循环创建更复杂的图案(如网格、圆点阵列)
参考资源
- 完整作业清单:编程作业清单.md