渲染管线-计算得到gl_FragColor(片元着色器)之后续GPU流程

核心结论

  • 计算出 gl_FragColor 之后,并不代表这个颜色一定会显示到屏幕上。
  • GPU 后面还会经过一串逐片元操作Per-Fragment Operations,只有通过这些步骤,这个片元才会真正写入帧缓冲
  • 你可以先记住主线:
text 复制代码
gl_FragColor
-> 片元丢弃判断
-> 裁剪/裁切测试
-> 模板测试
-> 深度测试
-> 混合
-> 写入颜色/深度/模板缓冲
-> 最终显示到屏幕

先纠正一个关键认知

  • gl_FragColor 是"片元着色器给出的候选颜色"。
  • 它不是"最终屏幕颜色"。
  • 真正最终显示出来的结果,还要看:
    • 这个片元有没有被丢弃
    • 有没有被深度测试挡住
    • 有没有被模板测试拦住
    • 是否要和已有颜色混合
    • 当前写入掩码是否允许写入

所以:

  • 片元着色器负责算颜色
  • 后续固定管线负责决定这个颜色能不能落盘

1. 片元可能先被丢弃

即使片元着色器已经执行完,片元仍然可能被直接丢掉。

常见有两种情况:

  • 你在片元着色器里手动 discard
  • 后续测试不通过

例如透明裁剪里常见:

glsl 复制代码
if (alpha < 0.1) {
    discard;
}
gl_FragColor = vec4(color, alpha);

这表示:

  • 即使本来算出了颜色
  • 只要执行了 discard
  • 这个片元就不会继续后续流程

所以第一个重点是:

  • 算出 gl_FragColor 不等于一定进入帧缓冲

2. 裁切测试,Scissor Test

如果启用了裁切测试:

js 复制代码
gl.enable(gl.SCISSOR_TEST);
gl.scissor(x, y, width, height);

GPU 会检查:

  • 当前片元是否落在指定矩形区域内

如果不在这个矩形里:

  • 直接丢弃
  • 后面深度测试、混合都不会走

它的作用是:

  • 限制绘制只发生在屏幕某个局部区域
  • 常用于分屏、局部刷新、UI 子区域绘制

所以这一步的本质是:

  • 先看这个片元是否在允许绘制的区域里

3. 模板测试,Stencil Test

如果启用了模板测试:

js 复制代码
gl.enable(gl.STENCIL_TEST);

GPU 会拿当前片元对应位置的模板缓冲值,和你的模板规则比较。

比如你可以设置:

  • 只有模板值等于某个数字的地方才允许画
  • 或者只在某些区域禁止画

如果模板测试失败:

  • 片元被丢弃
  • 颜色不会写入

模板测试常见用途:

  • 描边
  • 遮罩
  • 镜面/传送门效果
  • 限制某区域可见

所以这一步可以理解成:

  • 这个片元所在的位置,逻辑上允许绘制吗

4. 深度测试,Depth Test

这是最关键的一步之一。

如果启用了深度测试:

js 复制代码
gl.enable(gl.DEPTH_TEST);

GPU 会比较:

  • 当前片元的深度值
  • 和深度缓冲里该位置已有的深度值

如果当前片元更靠前,通常测试通过; 如果它被前面的物体挡住,通常测试失败。

例如默认常见规则是:

js 复制代码
gl.depthFunc(gl.LESS);

意思是:

  • 新片元深度更小,才通过

如果深度测试失败:

  • 这个片元即使颜色算好了,也不会显示

所以深度测试的本质是:

  • 这个片元是不是当前看到的最前面那个

这就是为什么 3D 场景里,后面的物体不会盖到前面的物体上。

5. 深度缓冲可能会更新

如果片元通过了深度测试,而且深度写入是开启的:

js 复制代码
gl.depthMask(true);

那么 GPU 不只是写颜色,还会把这个片元的深度值写入深度缓冲

这样后面再来的片元就能拿它做比较。

所以深度相关其实有两件事:

  • 深度测试:决定这个片元能不能画
  • 深度写入:决定这个片元会不会更新深度缓冲

这两个概念要分清。

例如做透明物体时,经常会:

  • 开启深度测试
  • 但关闭深度写入

6. 颜色不会立刻直接覆盖,可能先混合

如果启用了混合:

js 复制代码
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

GPU 不会简单把 gl_FragColor 直接写进去,而是会把:

  • 新片元颜色,source
  • 帧缓冲中原有颜色,destination

按混合公式合成一个新颜色。

最典型的透明混合就是:

text 复制代码
finalColor = srcColor * srcAlpha + dstColor * (1 - srcAlpha)

所以对于半透明物体来说:

  • gl_FragColor 只是输入颜色
  • 最终屏幕颜色是"新旧颜色混合后的结果"

这就是透明效果的根本来源。

7. 颜色写入还受写掩码控制

即使通过了所有测试,也不一定所有通道都能写。

例如:

js 复制代码
gl.colorMask(true, false, false, true);

表示:

  • 允许写入 R
  • 不允许写入 G
  • 不允许写入 B
  • 允许写入 A

类似地也有:

js 复制代码
gl.depthMask(...)
gl.stencilMask(...)

所以 GPU 在真正写缓冲时,还会看:

  • 哪些颜色通道能写
  • 深度能不能写
  • 模板能不能写

这一步可以理解成:

  • 即使片元合法,最终写多少内容还受权限控制

8. 写入帧缓冲

通过所有测试、混合和掩码处理后,GPU 才会把结果写入帧缓冲Framebuffer

帧缓冲里通常有几类数据:

  • 颜色缓冲:最终颜色
  • 深度缓冲:深度值
  • 模板缓冲:模板值

如果你在默认帧缓冲上绘制:

  • 最终会显示到屏幕

如果你绑定的是离屏帧缓冲,FBO

  • 结果可能写入纹理或渲染缓冲
  • 后续再作为贴图继续使用

所以"写入帧缓冲"不一定等于"马上显示在显示器上"。

9. 如果启用了多重采样,还会有采样解析

如果使用了抗锯齿,尤其是多重采样,MSAA,那么情况会更细一点:

  • 一个像素内部可能有多个采样点
  • 片元结果可能会对多个采样进行测试和记录
  • 最后还会有一次resolve,把多个采样合成一个最终像素颜色

你可以先简单理解为:

  • 光栅化和逐片元测试可能不是"每像素一个样本"那么简单
  • 抗锯齿时,一个像素内部可能有更细粒度的数据

不过在初学阶段,记住主线就够了:

  • 最终仍然要合成成帧缓冲里的像素颜色

10. 最后浏览器把帧缓冲内容显示出来

如果你渲染的是默认帧缓冲,那么这一帧绘制完成后:

  • 浏览器会在合适时机把颜色缓冲内容提交到屏幕
  • 用户最终看到的,就是这一帧的结果

所以从逻辑上可以分两层:

  • WebGL 负责把结果写进当前帧缓冲
  • 浏览器 / 显示系统负责把帧缓冲内容显示到屏幕

用一个最直观的例子串起来

假设你画了一个半透明红色三角形,片元着色器输出:

glsl 复制代码
gl_FragColor = vec4(1.0, 0.0, 0.0, 0.5);

GPU 后面会做的事大致是:

  1. 看这个片元有没有被 discard
  2. 看它是否落在 scissor 允许区域里
  3. 看模板测试是否通过
  4. 看深度测试是否通过,是否被前面的物体挡住
  5. 如果启用了混合,就把这个半透明红色和背景颜色混合
  6. 按颜色掩码决定哪些通道真正写入
  7. 把结果写入颜色缓冲,必要时也更新深度/模板缓冲
  8. 最终显示到屏幕

所以你屏幕上看到的那个红色,往往不是 vec4(1,0,0,0.5) 原样显示,而是和背景合成后的颜色。

要特别区分的 3 件事

  • gl_FragColor:片元着色器算出来的候选输出
  • 最终片元结果:经过深度、模板、混合后的结果
  • 最终像素显示:写入帧缓冲后,由屏幕显示出来的结果

这三者不是一回事。

最常见的几个误区

  • 误区1:gl_FragColor 一算出来就上屏:不是,后面还有测试、混合、写缓冲。
  • 误区2:片元着色器决定一切:不是,固定管线后处理同样关键,尤其是深度测试和混合。
  • 误区3:透明只靠 alpha 就能显示正确:不是,还必须正确开启 BLEND,通常还要注意绘制顺序。
  • 误区4:片元通过深度测试后一定会完全覆盖旧颜色:不一定,如果开启混合,结果可能只是部分叠加。
  • 误区5:写入颜色和写入深度是同一件事:不是,它们是两套独立控制。

总结

  • 计算出 gl_FragColor 之后,GPU 还会对这个片元做一系列逐片元操作,例如裁切、模板测试、深度测试、混合和写掩码控制,只有通过这些步骤后,结果才会真正写入帧缓冲并最终显示到屏幕上。

最短记忆版

  • gl_FragColor 之后主线就是:
text 复制代码
discard/裁切 -> 模板测试 -> 深度测试 -> 混合 -> 写入帧缓冲 -> 显示

计算得到gl_Position(顶点着色器)之后续GPU流程,请看之前的这篇文章

相关推荐
Monki1 小时前
AI 规范式 UI 设计,一键批量出页,高效落地不翻车
前端
不想说话的麋鹿1 小时前
「项目实战」我用 Codex + GPT-5.5 + Trellis "氛围编程",独立做了一个情侣厨房小程序
前端·agent·全栈
TMAH1 小时前
OpenLayers 实现网格色斑图:WebGL 叠加层方案
webgl
接着奏乐接着舞。2 小时前
【2026年7月最新】69道RAG面试题
前端·人工智能·后端·aigc·embedding·rag
以和为贵2 小时前
前端也能搞懂 Agent:从 Function Calling 到自主编排
前端·人工智能·架构
风止何安啊3 小时前
🚦 前端并发请求 “交通管制”:别让你的接口堵成早高峰
前端·javascript·面试
J船长3 小时前
扫盲烂笔头:A 记录、CNAME 的作用, Nginx的用途,为啥叫反向代理
前端
妙码生花3 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(二十六):icon 组件封装
前端·vue.js·typescript
李明卫杭州3 小时前
Vue2 & Vue3 中 Router `props` 配置详解:彻底摆脱 `$route` 依赖
前端·vue.js·前端框架