OpenGL OIT 之 Stochastic Transparency 实现(上篇):原理与架构 源码地址GitHub 仓库0. 前言在前两篇中我们分别介绍了 Linked List OIT通过 GPU 端链表收集排序和 Depth Peeling OIT通过多次 Pass 逐层剥离。这两种方案都是确定性的——每个像素的最终颜色是精确计算的。Stochastic Transparency随机/各向同性透明度走了一条完全不同的路用随机化替代排序。它不精确但实现极其简单只需要一个 Pass 和一个 Shader。1. 核心思想1.1 问题回顾回顾 alpha 混合的 over 算子C_result C1 * α1 (1 - α1) * C2 * α2 (1 - α1)(1 - α2) * C3 * α3 ...这个公式要求片段按深度排序Back-to-Front。如果排序错误结果就不对。1.2 Stochastic Transparency 的直觉与其精确排序不如随机抽样。假设一个像素有 3 个重叠的透明片段片段颜色透明度A红色0.5B绿色0.3C蓝色0.2如果我们有 16 个样本MSAA 16x按概率分配样本 1-8: 红色 (0.5 × 16 8 个样本) 样本 9-12: 绿色 (0.3 × 16 ≈ 4.8 → 4 个样本) 样本 13-16: 蓝色 (0.2 × 16 ≈ 3.2 → 3 个样本)硬件 MSAA 自动将这 16 个样本混合成最终颜色近似得到正确结果。不需要排序因为每个样本只会被一个片段覆盖——这就是随机化解决的问题。2. 关键技术MSAA gl_SampleMask2.1 MSAA 基础MSAAMulti-Sample Anti-Aliasing是 GPU 硬件支持的抗锯齿技术。对于每个像素GPU 维护多个子样本通常 4x、8x、16x每个子样本独立存储颜色和深度。正常 MSAA 流程光栅化产生多个覆盖样本每个样本独立进行深度测试解析resolve时所有样本的颜色平均得到最终像素颜色2.2 gl_SampleMask —— 核心武器gl_SampleMask是 GLSL 4.0 引入的内建输出变量允许 Fragment Shader逐样本控制哪些样本被写入。out int gl_SampleMask[]; // 每个 bit 控制一个样本gl_SampleMask[0]是一个 32-bit 整数bit 0 控制样本 0bit 1 控制样本 1以此类推某 bit 为 1 → 该样本被当前片段覆盖某 bit 为 0 → 该样本不被当前片段覆盖保留原有值或不变这正是 Stochastic Transparency 的核心用 alpha 值作为概率决定每个样本是否被覆盖。2.3 为什么顺序不重要关键洞察当每个样本最多被一个透明片段覆盖时顺序就不重要了。传统 Alpha 混合需要排序 后景 → 中景 → 前景 Back-to-Front Stochastic Transparency 不需要排序 每个样本随机选一个片段覆盖硬件 MSAA 平均得到结果3. 算法流程3.1 整体流程┌─────────────────────────────────────────────────────────────────────┐ │ 每帧渲染循环 │ │ │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ Step 1: 初始化 MSAA 状态 │ │ │ │ - glEnable(GL_MULTISAMPLE) ← 启用 MSAA │ │ │ │ - glEnable(GL_SAMPLE_MASK) ← 允许 Shader 写 sample mask │ │ │ │ - glEnable(GL_DEPTH_TEST) ← 启用深度测试 │ │ │ │ - glDepthFunc(GL_LEQUAL) ← 深度比较函数 │ │ │ │ - glDepthMask(GL_TRUE) ← 允许深度写入 │ │ │ │ - GLFW_SAMPLES 16 ← 16x MSAA │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ Step 2: 逐个对象渲染同一 Pass │ │ │ │ │ │ │ │ for each object: │ │ │ │ ┌────────────────────────────────────────────────┐ │ │ │ │ │ 2a. 设置 model/view/projection 矩阵 │ │ │ │ │ │ 2b. 传入 sampleCntMSAA 样本数 │ │ │ │ │ │ 2c. 传入 frameID随机种子每个对象不同 │ │ │ │ │ │ 2d. Fragment Shader: │ │ │ │ │ │ - 采样纹理获取颜色和 alpha │ │ │ │ │ │ - alpha → 覆盖率 │ │ │ │ │ │ - 对每个样本 i 生成随机数 r[i] │ │ │ │ │ │ - if r[i] coverage → randMask | (1 i) │ │ │ │ │ │ - gl_SampleMask[0] randMask │ │ │ │ │ └────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ Step 3: 硬件 MSAA 自动解析 │ │ │ │ - glfwSwapBuffers 时 GPU 自动将样本平均为最终像素颜色 │ │ │ │ - 无需额外 Shader 或 FBO │ │ │ └──────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘3.2 随机数生成Shader 中使用经典的伪随机数生成器vec2 seed vec2(sampleIdx, frameID); float r fract(sin(dot(seed, vec2(12.9898, 78.233))) * 43758.5453);sampleIdx样本索引 0…15保证每个样本的随机数不同frameID每个对象的唯一 ID作为不同对象之间的种子区分结果r在 [0, 1) 范围内均匀分布3.3 覆盖率决策float coverage color.w; // alpha 值作为覆盖率 if (r coverage) { randMask | (1u i); // 样本 i 被当前片段覆盖 }例如 alpha 0.516 个样本中大约 8 个会被覆盖因为 r 0.5 的概率是 50%。4. 深度测试与遮挡处理4.1 为什么需要深度测试Stochastic Transparency 不排序但深度测试仍然是必要的。原因是如果一个不透明的片段遮挡了透明的片段不透明的片段应该覆盖所有样本深度测试的GL_LEQUAL确保如果当前片段在已有的片段前面则覆盖如果在后面则不覆盖4.2 深度写入glDepthMask(GL_TRUE);// 允许深度写入每个片段通过深度测试后会更新深度缓冲。这确保了后续更远的片段不会覆盖更近的片段。4.3 完整 GL 状态glEnable(GL_MULTISAMPLE);// 启用 MSAA 多重采样glEnable(GL_SAMPLE_MASK);// 启用 sample mask 输出glEnable(GL_DEPTH_TEST);// 启用深度测试glDepthFunc(GL_LEQUAL);// 通过深度测试 当前深度glDepthMask(GL_TRUE);// 写入深度缓冲GLFW 窗口配置glfwWindowHint(GLFW_SAMPLES,16);// 请求 16x MSAA5. 随机种子frameID的作用5.1 为什么需要不同的种子如果所有对象用相同的种子每个样本的随机数序列就完全一样——这意味着所有对象会在完全相同的样本上竞争导致结果不随机、不正确。使用不同的frameID作为种子每个对象得到独立的随机数序列避免了样本上的竞争模式。5.2 本实现中的 frameID 策略staticintframeID0;intmodelCnt4;// 每个对象使用不同的 frameIDshaderQuad_-setInt(frameID,(frameID)%modelCnt);4 个对象1 个 Spot 3 个 WindowframeID 在 0-3 之间循环每个对象有固定的随机种子这意味着结果是静态的不会随时间变化注意这种实现每次运行结果相同不产生时间噪声。如果希望每帧随机化可以将frameID设为随机值或在每帧开始时随机化。6. 与其他 OIT 方案对比特性Linked ListDepth PeelingStochastic排序方式GPU 链表排序逐层剥离随机化不排序Pass 数量3 个N 层 × 1 Pass1 个 PassGPU 存储SSBO Image2 个 FBO 深度纹理无仅 MSAA 缓冲精度精确精确近似随机兼容性GL 4.3GL 3.3GL 4.0性能链表操作开销N × 场景绘制1 × 场景绘制噪声无无有静态/动态实现复杂度高中极低7. 优缺点优点实现极其简单一个 Shader一个 Pass不需要 FBO、SSBO、Atomic Counter性能好不需要多次绘制场景不需要排序完全消除了排序的开销缺点结果是近似的存在随机噪声不是精确结果依赖 MSAA样本数受 GPU 硬件限制通常 16x 或 32x低透明度难处理alpha 很小时只有少数样本被覆盖可能产生闪烁静态噪声本实现中 frameID 固定噪声不随时间平均8. 总结Stochastic Transparency 的核心思路可以概括为用概率替代排序用 MSAA 替代精确混合。gl_SampleMask逐样本控制片段覆盖用 alpha 值作为概率MSAA 硬件自动将样本平均为最终颜色无需手动混合单 Pass 渲染所有对象在一个 Pass 中绘制深度测试处理遮挡随机种子frameID 区分不同对象的随机序列避免样本竞争下篇将深入 Shader 代码实现逐行解析quad.frag中的随机样本掩码生成逻辑。