3.2 颜色:覆盖写 vs 混合写 本节摘要:片元着色器算出的颜色并不自动“变成屏幕上的颜色”。默认路径是覆盖:新颜色替换帧缓冲里的旧颜色。打开混合后,源颜色与目标颜色按 合成。半透明、粒子、UI 叠加走混合写;不透明物体应走覆盖写并让深度测试生效。只在着色器里把 alpha 写成 0.3,却不启用混合,画面仍是一块不透明的淡色,因为覆盖不读旧像素。 本节目标 阅读完本节,你应当能够: 说明覆盖写与混合写在是否读取目标颜色上的差别 读懂常用 与预乘的 解释绘制顺序对混合结果的影响,以及对覆盖写几乎无影响的原因 对照关闭混合的不透明 pass 与开启混合的透明 pass 指出清屏是一种特殊的覆盖,发生在绘制之前 着色器只提出颜色,状态机决定怎么落盘 FS 输出 vec4。
本节摘要:片元着色器算出的颜色并不自动“变成屏幕上的颜色”。默认路径是覆盖:新颜色替换帧缓冲里的旧颜色。打开混合后,源颜色与目标颜色按
blendFunc合成。半透明、粒子、UI 叠加走混合写;不透明物体应走覆盖写并让深度测试生效。只在着色器里把 alpha 写成 0.3,却不启用混合,画面仍是一块不透明的淡色,因为覆盖不读旧像素。
阅读完本节,你应当能够:
SRC_ALPHA, ONE_MINUS_SRC_ALPHA 与预乘的 ONE, ONE_MINUS_SRC_ALPHAFS 输出 vec4。若混合关闭,RGB 直接写入,alpha 按颜色掩码决定是否写入。旧值被扔掉。这叫覆盖,便宜,可与早期深度优化合作。若混合打开,硬件取源(FS 输出)与目标(帧缓冲旧值),按函数做乘加。这叫混合写,必须读旧值,顺序敏感。
固定管线记忆里“打开 ALPHA”就变透明,WebGL 里对应的是 enable(BLEND) 加函数,而不是某个灯光式开关自动读 alpha。可编程只负责把 alpha 算对;读不读目标,是固定阶段的事。第 1.2 节那条边界在这里落地。
覆盖: 新颜色 → 直接进附件 混合: 新颜色 + 旧颜色 + 因子 → 进附件 清屏: 用 clearColor 覆盖整附件
| 配置 | 公式直觉 | 适合 | 风险 |
|---|---|---|---|
| 混合关 | 覆盖 | 不透明世界 | 半透明物体像实心淡色 |
| SRC_ALPHA 与 ONE_MINUS_SRC_ALPHA | 普通插值 | 非预乘贴图 | 与预乘图一起用会黑边 |
| ONE 与 ONE_MINUS_SRC_ALPHA | 预乘插值 | 预乘 PNG、部分引擎默认 | 非预乘图会过亮 |
| ONE 与 ONE | 加法 | 火焰、光晕 | 饱和爆白,顺序仍有影响 |
| 关闭颜色写入只写深度 | 深度预 pass | 复杂不透明场景 | 忘开回来导致后面全黑 |
预乘:RGB 已经乘过 alpha。CPU 上传时可以预乘,也可以让 GPU 当非预乘处理。两端合同必须一致。图集工具、canvas 2D 导出、视频帧,默认不一定相同。黑边与白边是合同裂缝的典型视觉。
gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); // 透明物体:通常先画不透明,再按从远到近画透明 gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_SHORT, 0);
顺序:覆盖写下,近处后画会通过深度测试盖住远处,结果与画家算法相反但正确。混合写下,深度测试若仍丢弃更远的片元,而你又先画了近处透明,远处透明就被永远丢掉。所以透明 pass 常关深度写入、按距离排序。引擎的透明队列就是 CPU 在履行混合对顺序的合同。裸写忘了排序,玻璃会时对时错。
清屏不是混合。clear 把颜色附件设成 clearColor,把深度设成 clearDepth。每帧通常先 clear 再覆盖画不透明。用全屏黑四边形代替 clear 是把覆盖做成一次 draw,通常更慢。canvas 的 alpha 与页面合成还有一层浏览器混合,和 GL 内部混合不是同一开关——页面能透过 canvas 看见后面的 DOM,要在上下文属性 alpha 与 CSS 上对照,不要只改 FS。
默认路径应是:关混合,开深度,画全部不透明。然后开混合,关深度写入,画透明。粒子若是加法混合,可再分一队。CPU 多几次状态切换,换来 GPU 少读目标、早深度。把所有物体都开混合“以免漏掉透明”,会让整帧变成最贵路径。
| 物体类型 | 混合 | 深度测试 | 深度写入 | 排序 |
|---|---|---|---|---|
| 墙壁、角色不透明 | 关 | 开 | 开 | 可前到后也可随便 |
| 玻璃 | 开 | 开 | 关 | 远到近 |
| 加法火焰 | 开加法 | 开或关视情况 | 关 | 常不严格 |
| UI | 开 | 常关 | 关 | 按图层 |
颜色掩码 colorMask 能禁止写 RGB 或 A。做特殊遮罩时有用。忘了设回 true,后续物体“画了没看见”,像着色器死了。状态机不会在下一帧自动复位,除非你或引擎每帧重置。裸写要有一份默认状态表,每 pass 进出时恢复。引擎通常每对象设置;混用裸调用后必须自己复位,第 1.3 节的合同撕裂在混合上特别常见。
⚠️ 常见坑:FS 写了
gl_FragColor.a = 0.2,混合仍关着。你看见的是 20% 亮度的实心块,不是 20% 透明。先问状态,再问公式。💡 关键直觉:透明是合成问题,不是“颜色淡一点”。合成依赖旧像素,所以依赖顺序与是否预乘。
sRGB 与混合:若颜色附件是 sRGB,混合可能在线性空间进行,视实现与格式。WebGL2 对 sRGB 纹理更明确。工作流要全项目统一:贴图怎么解码、光照在哪层空间、最后怎么进显示。混合函数假定的 RGB 含义一错,玻璃颜色会脏。这不是调 blendFunc 能救的,是颜色空间合同。
当前默认帧缓冲一般不能当纹理读。要读已画内容,必须上一趟画到离屏纹理,这一趟再采。那是 3.4 的 FBO,不是把混合函数用 GLSL 重写一遍就能代替。硬读屏幕属于把固定阶段的目标颜色通道,改造成可编程采样,必须换输出目标。
第一个半透明三角形:先画一个不透明背景,再 enable 混合画三角。对比关混合的同款三角。这一对照比背函数表更牢。再换预乘函数,看黑边是否出现。把实验留在最小几何上,不要在复杂场景里调混合。
完全正确的透明是顺序无关或 OIT,WebGL 里贵且吵。产品常用减法:不透明先画;透明按重心距离排序;强加法粒子不排序;屏幕空间软粒子用深度纹理比较。排序在 CPU,比较在 GPU。物体很少时,每帧 sort 一次可接受;物体很多时,用桶排序或只排序可见透明。不要为两扇门写 OIT。
UI 与世界的混合合同经常冲突。世界已预乘,UI 从 canvas 2D 来的图非预乘。分开两个 pass,各配函数,比统一函数再在 FS 里纠正更干净。UI 通常关深度,按图层。世界透明开深度测试关写入。把 UI 网格丢进世界透明队列,按钮会被模型挡住或反过来。分队列是 CPU 组织问题。
颜色附件若带 alpha 且页面要透视 canvas,浏览器会再混一次。clearColor 的 alpha 与页面背景有关。要画布不透,请求上下文时 alpha 设 false,少一层意外混合。这是创建合同时的选择,运行时改 FS 救不回来。查看器嵌在白底营销页里出现脏边,优先查这一层,再查预乘。
调试混合:先关混合看不透明是否正确,再开混合只画一个透明物体,再加第二个看顺序。一次加齐所有玻璃,你分不清是函数错、预乘错还是顺序错。状态机调试靠加减 pass,不靠同时拧五个旋钮。
加法混合在 LDR 8 位附件上很容易饱和成白斑。火焰看起来像洞。HDR 附件加色调映射是正路,贵。减法是限制粒子亮度、减少重叠层数。层数是 CPU 生成或 GPU 粒子寿命问题。混合函数救不了 200 层加法。overdraw 是 4.7 的 GPU 税,根源在 3.2 选了加法还无限层。
MSAA 默认 FBO 与混合一起时,边缘半透明可能与预期不同。后处理关掉默认 MSAA、在离屏自己 resolve,边缘更可控。查看器若只画不透明模型,默认 antialias true 最省事。一旦上透明玻璃加后处理,默认 MSAA 反而碍事。创建属性与混合路线要一起选,不要上线后才发现玻璃描边脏。
预乘合同裂了。图是直通 alpha,函数按预乘在乘,暗色渗边。或相反。统一资产预乘,函数用 ONE 与 ONE_MINUS_SRC_ALPHA;或统一非预乘用 SRC_ALPHA。不要同一帧两种图混一个函数。黑边优先查合同,不要加描边着色器遮丑。
透明未排序或重心排序在交叉时不稳定。减法:合并网格、减少层、用 dither 近似、接受偶尔错。OIT 贵。两扇窗不值得。排序在 CPU 每帧做,物体少时稳定。交叉的叶子用双面加 alpha test,走覆盖加 discard,牺牲早深度,换可排序性。
创建时 alpha 为 true,clear 的 alpha 不足 1,页面再混一次。要实心底,alpha 设 false 或 clear alpha 为 1。这与 GL 内部混合无关。营销页白底脏边先查创建合同。FS 输出 alpha 1 仍透,更是创建属性问题。
把不透明、透明、加法粒子、UI 四行写成 pass 表:混合函数、深度测、深度写、排序。每行进出调用 restoreDefaults。表改一处,代码跟一处。不要在物体循环里临时 enable。临时 enable 必漏 restore。预乘检查放在贴图导入:发现直通 PNG 进预乘管线就告警。告警比上线黑边便宜。canvas 创建属性写进启动日志,和版本一起打。透明验收用两层已知顺序的格子,顺序反了立刻能看。不要用复杂场景验收混合。复杂场景变量太多。
混合验收用两张已知 PNG:一张预乘圆、一张直通圆,叠在棋盘上。边应干净。脏了就停业务开发。pass 表打印到调试层,能看见当前函数与掩码。幽灵透明常是上一 pass 没关混合。UI 与世界分相机分队列,禁止 UI 网格进世界透明排序。页面 alpha 合同写进 README 式的启动注释:本应用 canvas 不透明。后人改 getContext 时能看见。加法粒子层数上限写进生成器,超过则拒绝诞生。拒绝比饱和白洞体面。HDR 档才允许更高层数。层数是 3.2 送给 4.7 的开关,不开关就会在节日活动爆填充率。
混合问题禁止在复杂场景调。用棋盘加两圆的最小场景。最小场景过了再回业务。业务里灯、雾、后处理会一起改颜色,归因不可能。pass 表打印当前函数。函数与资产预乘不一致就告警。告警在导入时响,比上线黑边响得早。早响是工程,晚响是客服。
现场对照时只改一个变量。同时改版本、封装、通道,归因会消失。消失之后只能靠感觉,感觉正是本书要拆掉的东西。把改动写成一条,测完再写下一条。一条一条,四组对照才用得上。用不上的对照表只是装饰。装饰填不满格子,格子空着就不要标完工。混合这一格改完,用棋盘两圆验收,不要直接上玻璃大楼。
下一节对照深度测试与面剔除:谁在光栅之后扔片元,谁在装配之后扔三角形。