项目地址OpenGL_OIT_Stochastic_Transparency关键词Stochastic Transparency、OIT、MSAA、gl_SampleMask、深度测试摘要传统半透明需要按深度排序再 Alpha 混合。本文说明一种无需排序的近似做法Stochastic Transparency——在 MSAA 的每个子采样上按纹理 Alpha「掷骰子」用gl_SampleMask决定写入哪些子采样再配合子采样级深度测试最后由硬件Resolve平均得到屏幕像素。下文聚焦原理、关键着色器实现以及本项目中StochasticTransparencyApp::init()里五行 OpenGL 状态在何时、何阶段起作用。1. 背景为什么需要 OIT画家算法透明物体从远到近绘制每片元做C final C src ⋅ α C dst ⋅ ( 1 − α ) C_{\text{final}} C_{\text{src}} \cdot \alpha C_{\text{dst}} \cdot (1 - \alpha)Cfinal​Csrc​⋅αCdst​⋅(1−α)痛点排序贵、无法处理循环重叠、与深度缓冲难协作。OITOrder-Independent Transparency不依赖绘制顺序。本项目采用Stochastic Transparency把 Alpha 当作「每个 MSAA 子采样被保留的概率」而非混合权重。方法排序本项Alpha 混合需要—Stochastic Transparency不需要采用2. 原理子采样掷骰子核心思想McGuire Bavoil, HPG 2013Alpha coverage 子采样保留概率。2.1 三步直觉① 每个子采样掷骰子片元着色器coverage 0.6纹理 Alpha示意 8 个子采样 子采样: s0 s1 s2 s3 s4 s5 s6 s7 随机数 r: 0.23 0.71 0.45 0.88 0.12 0.55 0.39 0.94 r 0.6? ✓ ✗ ✓ ✗ ✓ ✓ ✓ ✗ gl_SampleMask: 1 0 1 0 1 1 1 0 → 约 60% 位为 1② 深度测试同一子采样上近的赢子采样 s2远片元 A 先写 → 近片元 B 后写 B 深度更近 → 在 s2 上覆盖 A无需对物体排序③ MSAA Resolve对「亮着」的子采样求平均 → 近似半透明[R][--][R][--][R][R][R][--] → Resolve → 约 0.5×红色2.2 一帧内数据流与本项目对应initWindow: GLFW_SAMPLES16 → 创建多采样帧缓冲前置条件 ↓ init(): 五行 GL 状态 → 整段渲染过程的全局规则见 §4 ↓ beginFrame: glClear 颜色深度 → 每帧清空 MSAA FBO ↓ renderScene: 4 次 Draw无序 → 片元写 gl_SampleMask 颜色 │ 深度测试/写入在固定管线阶段执行 ↓ SwapBuffers → MSAA Resolve → 显示器3. 关键实现片元着色器resources/quad.frag是算法核心vec4 color texture(texture_diffuse, textureCoord); float coverage color.w; uint randMask 0u; for (int i 0; i sampleCnt; i) { vec2 seed vec2(i, frameID); float r fract(sin(dot(seed, vec2(12.9898, 78.233))) * 43758.5453); if (r coverage) randMask | (1u i); } gl_SampleMask[0] int(randMask); FragColor color;符号来源作用coverage纹理 Alpha伯努利试验成功概率sampleCntCPU 传GL_MAX_SAMPLES掷骰子次数 MSAA 采样数frameID每物体递增% 4随机种子避免重叠面 mask 完全相同gl_SampleMask片元输出位为 1 的子采样才允许写入颜色/深度CPU 侧每帧对 Spot、蓝/绿/红窗各Draw一次不排序beginFrame只清一次屏物体之间不清深度clearColorDepth {false,false}深度在四次绘制间累积。4. 五行 GL 状态在流程中何时、扮演什么角色以下代码位于StochasticTransparencyApp::init()在首帧绘制之前调用一次之后每帧、每个片元的固定管线都受这些状态约束直到被glDisable改掉本项目不会关掉。glEnable(GL_MULTISAMPLE);glEnable(GL_SAMPLE_MASK);glEnable(GL_DEPTH_TEST);glDepthFunc(GL_LEQUAL);glDepthMask(GL_TRUE);另有一处必须先于上述状态生效的配置initWindowglfwWindowHint(GLFW_SAMPLES,16);// 创建 16× MSAA 默认帧缓冲没有多采样缓冲后面五行中的「子采样」概念不存在。下文按OpenGL 管线时间顺序说明每一项。4.1 总览状态 × 管线阶段状态 / 配置主要生效阶段一句话GLFW_SAMPLES16上下文/ FBO 创建提供 N 个子采样槽位GL_MULTISAMPLE光栅化 → Resolve打开多采样路径GL_SAMPLE_MASK片元后、写入前允许片元用 mask 筛子采样GL_DEPTH_TEST片元后、写入前按深度决定能否写入某 sampleglDepthFunc(GL_LEQUAL)深度测试瞬间通过条件新深度 ≤ 旧深度glDepthMask(GL_TRUE)深度测试通过后允许更新深度缓冲4.2glEnable(GL_MULTISAMPLE)—— 多采样路径的总开关何时设置init()窗口已带GLFW_SAMPLES创建完毕之后。在哪些阶段起作用光栅化三角形覆盖一个像素时不是只影响 1 个点而是影响该像素的N 个子采样本项 N≤16。片元着色在 MSAA 模式下片元与子采样关联具体是否「每子采样跑一次片元」取决于驱动与是否开启 sample shading本 Demo 未开GL_SAMPLE_SHADING但 mask/深度仍按子采样语义工作。写入颜色、深度写入多采样颜色/深度缓冲每个像素 N 份。SwapBuffersResolve硬件把 N 个子采样平均成 1 个显示像素——Stochastic Transparency 的「混合」 largely 发生在这里。若关闭退化为单采样无法「按子采样保留/丢弃」本算法失效。4.3glEnable(GL_SAMPLE_MASK)—— 允许片元改写「写入资格」何时设置init()且必须在片元里写gl_SampleMask之前启用。在哪些阶段起作用发生在片元着色器执行完毕之后、颜色/深度实际写入 framebuffer 之前的「样本遮罩」阶段。片元里gl_SampleMask[0] randMask只有 mask 中为 1 的 bit该子采样才允许接收本片元的FragColor和深度。与coverage掷骰子直接对应先用随机决定哪些 sample「有资格写」再对这些 sample 做深度测试。若关闭gl_SampleMask写入被忽略所有子采样都会尝试写入 → 半透明变成「全不透明片元」失去随机透明度。依赖关系依赖GL_MULTISAMPLE单采样下无意义。4.4glEnable(GL_DEPTH_TEST)—— 子采样上的前后关系何时设置init()每帧beginFrame里不清深度开关只glClear(DEPTH)。在哪些阶段起作用每个片元、每个通过 Sample Mask 的子采样将该子采样上的片元深度与多采样深度缓冲中对应 sample 的已存深度比较。本 Demo 连续画 4 个物体同一子采样上后绘制且更近的片元可以赢远的被挡——这是在子采样粒度实现「谁在前」从而无需对网格排序。与 Stochastic 的配合片元到达 → Sample Mask 筛 sample → 深度测试筛 sample → 通过的 sample 写颜色深度若关闭所有片元都写入远近错乱重叠透明完全错误。4.5glDepthFunc(GL_LEQUAL)—— 深度比较规则何时设置init()与GL_DEPTH_TEST同时生效。在哪些阶段起作用仅在深度测试执行的那一瞬间。GL_LEQUAL新片元深度≤缓冲中深度 →通过。相等深度可通过对共面或同一几何重复绘制更宽容。本 Demo 每帧从glClearDepth(1.0)开始近处深度小远处大。角色定义「什么叫 nearer」。Stochastic 只决定哪些 sample 参与竞争谁赢由深度测试决定。4.6glDepthMask(GL_TRUE)—— 是否写入深度缓冲何时设置init()。在哪些阶段起作用深度测试通过之后的写入阶段。GL_TRUE通过的子采样更新多采样深度缓冲。之后同一子采样上更远的片元会因深度测试失败而无法写入颜色。在本项目中的角色使「近处透明片元占住该 sample」在后续 Draw中仍成立四物体共用同一深度缓冲、中间不清深度。这是无序绘制仍能近似正确的前后关系的关键之一。若改为GL_FALSE只测不写后续片元无法被挡住多层透明叠加会乱传统透明常在对透明 pass 关深度写本算法路径不同。4.7 单行代码在「一帧四物体」中的时间线帧开始 glClear 颜色深度 ← 深度缓冲置远平面 ───────────────────────────────────────────────────────── Draw Spot 片元: 掷骰子 → gl_SampleMask ← GL_SAMPLE_MASK 片元 shader 深度测试 LEQUAL ← GL_DEPTH_TEST glDepthFunc 通过则写色写深 ← GL_MULTISAMPLE 缓冲 glDepthMask TRUE ───────────────────────────────────────────────────────── Draw 蓝窗 (frameID1, 新随机 mask) 同上与 Spot 在重叠像素的同一 sample 上比深度 ───────────────────────────────────────────────────────── Draw 绿窗、红窗 … ───────────────────────────────────────────────────────── SwapBuffers → MSAA Resolve ← GL_MULTISAMPLE 解析到屏幕 帧结束4.8 五行与着色器分工对照表层次谁负责做什么窗口GLFW_SAMPLES创建 N 子采样缓冲全局状态GL_MULTISAMPLE走多采样 Resolve全局状态GL_SAMPLE_MASK允许片元筛 sample片元 shadergl_SampleMask randMask按 Alpha 随机保留 sample全局状态GL_DEPTH_TESTLEQUAL近的赢全局状态glDepthMask(TRUE)赢的 sample 写下深度挡住远的硬件Resolve子采样平均 ≈ 透明感5. 设计取舍简短噪声采样数有限16会有颗粒可时间累积或 TAA。近似非物理精确混合要精确需 Linked List / Depth Peeling。Per-sample shading未显式glMinSampleShading(1.0)极端情况下驱动行为需实机验证。6. 总结问题答案原理是什么Alpha 子采样保留概率mask 深度 Resolve关键代码在哪quad.frag中gl_SampleMask循环五行 GL 状态何时设init()一次作用于之后每帧整条管线各自管什么MSAA 提供 sampleMASK 筛 sample深度测/写决定远近为何能不排序前后关系在每个子采样上由深度解决透明度由随机 mask Resolve 平均近似参考McGuire Bavoil,Stochastic Transparency, HPG 2013本项目src/StochasticTransparencyApp.cppinit 53–57 行、resources/quad.frag