2.2 控制流、函数与存储限定符


2.2 控制流、函数与存储限定符

本节摘要:GLSL 的函数语法接近 C,但控制流的代价模型完全不同——分支的开销取决于"组内线程是否走同一条路",而不是判断本身。本节讲清分支与循环的真实成本、函数的参数限定符,以及 in/out/uniform 这组决定数据通道的关键字——它们是读懂一切着色器模板的钥匙。

分支不是免费的

CPU 程序员的直觉是"if 判断很便宜"。GPU 上这个直觉必须修正。着色器线程是成组调度的:一批线程锁步执行同一指令序列。遇到分支时,硬件把组内两条路径都走一遍,每个线程按自己的条件"假装"执行不属于它的那条路——术语叫分支发散。组内条件越杂,浪费越大。

// 反例:按片元条件分支,同一批线程条件各异,两条路都要走 if (texture(uTex, vUv).r > 0.5) { color = color * 1.5; } else { color = doExpensiveCalc(color); }

上面这段代码在最坏情况下等于把便宜路径和昂贵路径都执行一遍。改写思路有两种:用混合函数把分支抹平,或者确认条件在整组线程上一致。

// 改法一:无分支化,用 step 得到 0 或 1 的权重做插值 float k = step(0.5, texture(uTex, vUv).r); color = mix(doExpensiveCalc(color), color * 1.5, k); // 改法二:uniform 条件分支是安全的——所有线程看到同一个值,方向必然一致 if (uEnableVignette) { color *= vignetteFactor(vUv); }

stepmix 的无分支写法是片元着色器的日常武器,第 4 章会大量使用。判断分支是否值得写,只需要问一句:条件对所有线程是否同值?同值(uniform、常量)放心写;异值(依赖纹理、依赖插值属性)尽量无分支化。循环的规矩类似:循环边界是常量或 uniform 时,驱动能展开优化;边界依赖逐线程数据的循环则要慎用,且 GLSL 不保证动态循环的高效实现。

函数写法与参数限定符

GLSL 函数与 C 函数神似,但有三条硬约束:不支持递归(调用栈在 GPU 上是奢侈品)、所有函数必须在使用前声明或定义、返回值不能是数组。日常写法如下:

// 工具函数:求亮度(人眼对各通道敏感度不同,权重不相等) float luminance(vec3 c) { return dot(c, vec3(0.2126, 0.7152, 0.0722)); } // 参数限定符:in(默认,值传入)、out(结果传出)、inout(双向) void expandUV(inout vec2 uv, float scale) { uv = (uv - 0.5) * scale + 0.5; // 以中心为基准缩放 UV }

inout 参数是 GLSL 里少数能"修改调用者变量"的手段,相当于 C 里的传指针但语法更干净。工具函数的积累非常重要:成熟的着色器工程师都有一份自己的小函数库(亮度、哈希噪声、旋转矩阵、雾混合),第 4 章的数学工具箱会帮你攒起第一批。

💡 关键直觉:把着色器函数当"纯数学函数"设计——不改全局状态、不依赖执行历史,同输入必同输出。这个习惯能避开绝大多数着色器调试噩梦,也为将来迁移到 compute 场景打好底子。

存储限定符:给数据声明来路

一个变量前的限定符,宣告的是它站在哪条数据通道上。这是 GLSL 与普通语言差异最大的地方,用一张表对齐:

限定符 数据方向 变化粒度 典型内容
in 上一阶段流入本阶段 每顶点 / 每片元各异 顶点坐标、插值后的 UV 与法线
out 本阶段流向下一阶段 每顶点 / 每片元各异 变换后的位置、传给片元的属性
uniform CPU 流入 GPU 一次绘制内全体相同 矩阵、时间、材质参数、灯光位置
const 编译期确定 永不变化 数学常量、权重表

把两个着色器的 in/out 想成插头与插座:顶点着色器 out vec2 vUv; 声明了一个插座,片元着色器 in vec2 vUv; 必须提供匹配的插头——名字相同、类型相同,缺一不可。链接器会做匹配检查,对不上直接报链接错误。数据在两站之间经历插值:顶点处的值在三角形表面上按重心坐标线性混合后交给每个片元,这一机制是第 4 章的主角。

uniform 则是全体线程共享的只读常量,CPU 在绘制前上传,绘制期间不变。它是 CPU 与 GPU 之间最重要的参数通道:时间、相机矩阵、光源参数全走这里。误用 uniform 当可写变量是新手高频错误——尝试在着色器里给 uniform 赋值,编译器会直接拒绝,这是好事,说明通道语义被语言强制了。

// 顶点着色器:完整的通道视角 #version 330 core layout (location = 0) in vec3 aPos; // in:顶点缓冲流入,location 对应 CPU 侧绑定 layout (location = 1) in vec2 aUv; uniform mat4 uMVP; // uniform:CPU 上传,全体顶点共用 out vec2 vUv; // out:流向片元着色器的插座 void main() { vUv = aUv; gl_Position = uMVP * vec4(aPos, 1.0); }

顺带交代历史包袱:老教程里的 attributevarying 分别是 in(顶点阶段)与 out/in(跨阶段)的旧名,GLSL 3.30 起全面换成现代写法。看老代码时在脑子里做个替换映射即可,新代码一律用现代限定符。

三问三答

问:GLSL 有标准库吗?想排序、查字符串怎么办? 没有也不需要。着色器函数库是数学向的(三角、指数、向量、矩阵),字符串与容器这类 CPU 概念在 GPU 上没有立足点。真需要复杂数据处理,那是 CPU 的活,处理好再把结果喂进来——着色器只做它擅长的数值变换。

问:两个着色器阶段能用同一个函数吗? 可以,把工具函数放进公共源码字符串,编译顶点与片元着色器时各自拼接进去。工程里常见的组织方式是"公共头"——噪声函数、颜色空间转换、旋转矩阵这类通用件集中维护,各阶段按需引入。注意GLSL 没有真正的链接器,重复引入同名函数会直接编译报错。

问:uniform 数量有上限吗? 有,且各平台不一:移动端常见 256 个 vec4 的量级,桌面宽裕得多。超限的症状是编译或链接报错,也可能静默失败。参数多的材质应该优先改用纹理存参数(查表思路)或常量块分池——这也是引擎里"材质参数面板"背后做结构设计的原因。

本节要点回顾

  • 分支看一致性:uniform 条件放心分支,逐线程条件优先 stepmix 无分支化;
  • 循环看边界:常量或 uniform 边界可被优化,逐线程动态边界慎用;
  • 函数三约束:无递归、先声明后使用、返回值不能是数组;inout 参数是修改调用者变量的合法通道;
  • 限定符即通道in 收上游、out 交下游、uniform 接 CPU、const 编译期定死;
  • 跨阶段配对:顶点 out 与片元 in 必须名字类型双匹配,中间自动发生插值。

语法工具齐了。下一章进入顶点着色器的正面战场:坐标变换与 MVP 矩阵。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U