本节摘要:片元着色器是一段被百万次并行执行的孤立函数:输入是插值后的属性与屏幕坐标,输出是颜色(以及可选的深度),且必须假装世界上只有自己这一个片元。本节建立这套执行模型,演示屏幕坐标驱动的程序化图案,并交代 discard 与深度写入的代价账。
读完本节,你应当能够:区分片元与像素两个概念;列出片元着色器可用的一切输入来源;写出基于屏幕坐标的程序化图案;判断 discard 的使用是否恰当。
光栅化把三角形切碎后,每个候选像素——片元——都要独立执行一遍你写的片元程序。注意"片元"与"像素"不是同义词:像素是屏幕上真实的显示单元,片元是"可能成为像素的候选"。候选可能被深度测试淘汰(被更近的几何体挡住)、被 discard 主动丢弃,只有幸存者才最终写进帧缓冲。一个像素被多个三角形覆盖时,会先后产生多个片元。
片元程序能拿到的输入,清点下来就四类:
out、本阶段 in 的所有变量,已按片元位置插值(第 3.2 节的机制);gl_FragCoord,xy 是片元中心在视口内的像素坐标,z 是深度值;gl_FrontFacing,布尔值,标明当前片元属于正面还是背面三角形;输出侧,至少写一个 out vec4 颜色;需要时还可输出自定义深度。整套模型归纳成一句话:孤立求色——不知道兄弟片元算了什么,不保存任何跨片元状态。

gl_FragCoord 是片元程序感知"自己是谁"的唯一途径。用它画棋盘,只需理解取整与奇偶:
#version 330 core out vec4 FragColor; uniform vec2 uResolution; // 视口宽高,CPU 传入 void main() { vec2 px = gl_FragCoord.xy; // 片元中心的像素坐标 vec2 cell = floor(px / 40.0); // 每 40 像素一格 float checker = mod(cell.x + cell.y, 2.0); // 行列和的奇偶:0 或 1 vec3 col = mix(vec3(0.92), vec3(0.25), checker); FragColor = vec4(col, 1.0); }
这段代码值得逐行咀嚼,因为它的套路可以无限复用:坐标 → 归一 → 取整 → 周期判断 → 混合。把 floor 换成 fract 得到条纹;把 mod 的周期参数换成 uTime 让棋盘滚动;把 px 先减去屏幕中心再算 length,棋盘就变成了同心圆环。程序化图案的全部秘密都藏在这条流水账里。
UV 驱动的版本更常用(跟着模型走而不是跟着屏幕走):把 px 换成插值来的 vUv * uTiling 即可。两种坐标源的选择标准——效果贴世界还是贴屏幕:水面波纹贴世界用 UV,镜面脏迹与扫描线贴屏幕用 gl_FragCoord。
⚠️ 常见坑:
gl_FragCoord.y的原点在左下角,而多数图像与 UI 库的坐标原点在左上角。混合使用两套坐标时上下颠倒是最常见的初学者症状,一次1.0 - y就能救回来。
片元程序有权拒绝输出——discard 让当前片元直接蒸发,输出合并站不再见到它:
// 镂空效果:alpha 低于阈值的部分当它不存在 vec4 texColor = texture(uTex, vUv); if (texColor.a < 0.1) { discard; // 栅栏植被、铁丝网的经典做法 } FragColor = texColor;
镂空 foliage 这类用法确实离不开 discard,但要知道代价:discard 的存在会削弱驱动的 early-z 优化。现代 GPU 本可以在片元程序执行之前就用深度缓冲淘汰被遮挡的片元(早期深度测试,省下的可是整段着色计算);一旦声明了 discard 或输出深度,驱动必须假定"这片元可能翻盘",把深度比较推迟到程序跑完之后——被遮挡区域的着色计算全额付费。经验法则:整片区域都透明的材质才用 discard;只是半透明则应走混合路线,别动 discard。
主动输出深度的场景更少(深度剥离、特殊遮挡算法),同样触发 early-z 削弱,非必要不碰。
把棋盘的套路换个输出,做一个左红右蓝的分屏,体会"坐标判断"的最小完整闭环:
void main() { float side = step(uResolution.x * 0.5, gl_FragCoord.x); // 左半 0,右半 1 vec3 col = mix(vec3(0.85, 0.2, 0.2), vec3(0.2, 0.35, 0.85), side); FragColor = vec4(col, 1.0); }
一行 step 完成分界,一行 mix 完成选色。变式练习按难度排:分界线改成斜线——把 x 与 y 组合进判断式;分界线随时间扫过——把阈值换成 uTime 的函数;分界处加过渡带——把 step 换成 smoothstep。四个变式做完,"坐标驱动的颜色"这门手艺就真正长在手上了,第 7 章的扫描线与擦除转场都是这套动作的放大版。
顺带补一个工程视角:这类全屏效果在引擎里通常挂在一个覆盖全屏的面板或后处理 pass 上,而不是游戏物体上。效果逻辑与承载介质分离——同样的代码,换个承载就换了个舞台。
gl_FragCoord、gl_FrontFacing、全局资源;执行模型是孤立求色;gl_FragCoord 原点在左下,与图像库坐标相反时记得翻转。下一节把片元数学的武器库摆开:不依赖任何贴图,纯函数能画到什么程度。