Skip to main content

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 的控制流和函数定义,理解性能优化的方法。

具体要求

  1. 自定义函数:创建一个函数,计算两个颜色的混合
  2. If-Else 版本:使用 if-else 创建分区域的颜色效果
  3. 无分支版本:将 if-else 改写为使用 step()mix() 的数学函数版本
  4. 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 循环创建更复杂的图案(如网格、圆点阵列)

参考资源