4.1 片元着色器剖析:每个片元孤立求色


4.1 片元着色器剖析:每个片元孤立求色

本节摘要:片元着色器是一段被百万次并行执行的孤立函数:输入是插值后的属性与屏幕坐标,输出是颜色(以及可选的深度),且必须假装世界上只有自己这一个片元。本节建立这套执行模型,演示屏幕坐标驱动的程序化图案,并交代 discard 与深度写入的代价账。

学习目标

读完本节,你应当能够:区分片元与像素两个概念;列出片元着色器可用的一切输入来源;写出基于屏幕坐标的程序化图案;判断 discard 的使用是否恰当。

每个像素面前都站着一段程序

光栅化把三角形切碎后,每个候选像素——片元——都要独立执行一遍你写的片元程序。注意"片元"与"像素"不是同义词:像素是屏幕上真实的显示单元,片元是"可能成为像素的候选"。候选可能被深度测试淘汰(被更近的几何体挡住)、被 discard 主动丢弃,只有幸存者才最终写进帧缓冲。一个像素被多个三角形覆盖时,会先后产生多个片元。

片元程序能拿到的输入,清点下来就四类:

  • 插值属性:顶点阶段 out、本阶段 in 的所有变量,已按片元位置插值(第 3.2 节的机制);
  • 屏幕坐标:内置变量 gl_FragCoord,xy 是片元中心在视口内的像素坐标,z 是深度值;
  • 朝向信息gl_FrontFacing,布尔值,标明当前片元属于正面还是背面三角形;
  • 各路资源:纹理、uniform 常量,与顶点阶段共享同一套绑定。

输出侧,至少写一个 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 与深度写入:生杀权的代价

片元程序有权拒绝输出——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 上,而不是游戏物体上。效果逻辑与承载介质分离——同样的代码,换个承载就换了个舞台。

本节要点回顾

  • 片元不等于像素:片元是候选,可能被深度淘汰或 discard 蒸发,幸存者才落进帧缓冲;
  • 输入四路:插值属性、gl_FragCoordgl_FrontFacing、全局资源;执行模型是孤立求色;
  • 程序化图案套路:坐标归一、取整或取余、周期判断、混合输出,UV 贴世界、屏幕坐标贴屏幕;
  • discard 是双刃剑:换镂空的便利,付 early-z 失效的代价,半透明请走混合;
  • 坐标原点差异gl_FragCoord 原点在左下,与图像库坐标相反时记得翻转。

下一节把片元数学的武器库摆开:不依赖任何贴图,纯函数能画到什么程度。


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