图形渲染中的透明混合

图形渲染中的透明混合 现代 GPU 默认采用的是经典的 Source Over 混合公式。设当前绘制颜色Source为Cs透明度为αs。帧缓冲已有颜色Destination为Cd, 透明度为αd。那么混合后的颜色为CoutαsCs(1−αs)Cd(28)对应透明度为αoutαs(1−αs)αd(29)GPU 中对应的 OpenGL 设置通常就是glEnable(GL_BLEND);glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);Unity、Unreal、Vulkan、DirectX 等现代图形 API 本质上都是这一套公式。从上述公式可以看出Alpha Blending 是不满足交换律的两个透明的物体谁前谁后对最终混合的颜色影响巨大。因此透明物体的渲染队列必须遵循由远到近的顺序这就说经典的画家算法Painter’s Algorithm。它的思想非常符合现实像画画一样先画远处再画近处。3DGS中的透明混合3DGS 的透明混合与传统 GPU 几乎完全一致。区别仅在于传统图形学混合的是三角形片元Fragment而 3DGS 混合的是二维高斯椭圆Gaussian Splat。设第 (i) 个高斯在当前像素上的透明度为αi颜色为ci若所有高斯已经按照深度完成排序则最终颜色可写成CN∑i1Tiαici(30)其中:Tii−1∏j1(1−αj)(31)表示第 (i) 个高斯之前剩余的透射率Transmittance。这个公式与传统 Alpha Blending 完全一致只不过将 GPU 固定功能中的混合操作显式写成了数学递推。具体的含义可以这样理解最终颜色 每一个高斯球的颜色贡献 × 它前面所有高斯球的累积透明度然后把所有高斯球的贡献加起来。图形渲染实现从数学角度来看3DGS 并没有发明一种新的透明混合算法。与传统图形渲染最大的区别在于透明度的来源不同。在 3DGS 中每一个二维高斯都会根据高斯函数实时计算当前像素的透明度αα0exp(−12d2)(32)其中α0为训练得到的不透明度参数(d) 为当前像素到高斯中心的马氏距离。因此高斯自身天然就是一个连续变化的半透明体而无需依赖额外的 Alpha 纹理。从工程实现来看这种做法也带来了新的挑战。传统渲染管线依赖 GPU 固定功能完成透明混合而 3DGS 通常需要自行控制高斯排序、颜色累积以及透射率计算因此很难直接融入已有的 Forward 或 Deferred 渲染流程。对于需要与传统三角形场景共同渲染的应用如 AR、数字孪生、游戏引擎等一种较为合理的方案是在现有渲染管线中增加连续后处理Post Process或自定义渲染队列Custom Render Queue将 3DGS 作为独立的透明渲染阶段与传统光栅化渲染协同工作而不是完全替代现有的图形流水线。实现伪代码在常规 GPU 图形渲染中我们无法像 CPU 那样简单地用一个变量去循环累加颜色。因此3DGS 需要借助额外的纹理来在像素之间传递混合状态。通常我们会引入两张关键的纹理作为额外的渲染目标累积颜色纹理Accumulated Color Texture存储公式 (30) 中的最终颜色C。累积透射率纹理Accumulated Transmittance Texture存储公式 (31) 中的剩余透射率T初始值为 1.0。以下是 3DGS 渲染单个高斯椭球时的片元着色器Fragment Shader核心伪代码// 片元着色器伪代码// 输入uniform sampler2D accumulated_color_tex; // 累积颜色纹理uniform sampler2D accumulated_transmittance_tex; // 累积透射率纹理in vec2 screen_uv; // 当前像素的屏幕坐标in vec3 gaussian_color; // 当前高斯的颜色 c_iin float gaussian_alpha; // 当前高斯在当前像素的透明度 α_ivoid main() {// 1. 读取当前像素之前的累积状态vec3 C_accum texture(accumulated_color_tex, screen_uv).rgb;float T_accum texture(accumulated_transmittance_tex, screen_uv).r;// 2. 计算当前高斯的贡献 // 当前高斯的颜色贡献 自身颜色 * 自身透明度 * 之前所有高斯的累积透射率 vec3 contribution gaussian_color * gaussian_alpha * T_accum; // 3. 更新累积颜色 vec3 C_new C_accum contribution; // 4. 更新累积透射率 // 新的透射率 旧的透射率 * (1 - 当前高斯的透明度) float T_new T_accum * (1.0 - gaussian_alpha); // 5. 将新的状态写回纹理供下一个高斯读取 // 注意实际工程中是写出到绑定的渲染目标 imageStore(accumulated_color_image, pixel_coord, vec4(C_new, 1.0)); imageStore(accumulated_transmittance_image, pixel_coord, T_new);}在这段伪代码实现中关键在于以下两点计算贡献根据公式 (30) 和 (31)当前高斯对最终颜色的贡献是 gaussian_color * gaussian_alpha * T_accum。这里的 T_accum 完美对应了公式中的∏i−1j1(1−αj)。累加与更新将贡献值加到Caccum上并将Taccum乘以(1−αi)得到新的透射率。八、深度排序上一节介绍了过 3DGS 的透明混合公式成立的前提是所有 Gaussian 必须按照距离摄像机由远到近Back-To-Front完成排序。否则由于 Alpha Blending 不满足交换律会出现透明颜色错误、遮挡关系异常等问题。排序依据一个 Gaussian 本身是一个三维椭球而不是一个点那么它究竟按照什么深度排序原版的 3DGS 没有考虑那么复杂仅使用 Gaussian 中心在相机空间中的深度作为排序依据。设高斯中心为μ(x,y,z), 经过视图变换后得到相机空间坐标μc(xc,yc,zc)。排序使用的深度就是Depthzc(33)按照 Depth 从远到近Back-To-Front排序。伪代码如下for (Gaussian g){vec3 centerView ViewMatrix * g.mean;g.depth centerView.z;}Sort(depth);也就是说一个 Gaussian 是否排在前面与它的协方差矩阵、椭球长短轴以及屏幕覆盖面积均无关系而仅取决于它中心点距离摄像机的远近。从理论上来说仅使用中心点排序确实不是完全准确但这种误差通常不会十分明显不会影响最终渲染效果是精度与效率之间的一种工程折中。全局排序明确了排序依据那么是不是对所有 Gaussian 按照深度排序再依次进行透明混合就可以了呢我们知道一般比较好的排序算法复杂度是O(NlogN)这对于百万级甚至千万级 Gaussian 来说开销十分巨大。更重要的是由于摄像机可以自由移动每一帧所有 Gaussian 到摄像机的距离都会发生变化因此排序结果也必须实时更新。如果采用传统 CPU 排序Camera Move↓CPU Sort↓Upload GPU↓Render不仅排序耗时巨大还会产生大量 CPU 与 GPU 之间的数据传输几乎无法满足实时渲染需求。Tile-Based Rendering原版的 3DGS 借鉴了现代 GPU 的 Tile-Based Rendering 思想。将屏幕划分为固定大小的小块Tile例如±—±—±—| T0 | T1 | T2 |±—±—±—| T3 | T4 | T5 |±—±—±—| T6 | T7 | T8 |±—±—±—通常每个 Tile 为 16 × 16 像素。随后每个 Gaussian 根据自己的二维包围盒被分配到它覆盖到的所有 Tile。例如Gaussian A┌────────┐│ │±—±—±—| T0 | T1 | T2 |±—±—±—| T3 | T4 | T5 |±—±—±—Gaussian A 同时覆盖T1、T2、T4 和 T5。因此只需要加入这些 Tile 的渲染列表即可而无需参与整个屏幕的排序。局部排序完成 Tile 划分之后每一个 Tile 都维护自己的 Gaussian 列表Tile 0Gaussian 15Gaussian 28Gaussian 91Gaussian 302…随后仅在当前 Tile 内按照深度排序Far↓Gaussian302Gaussian91Gaussian28Gaussian15Near最后由 GPU 对 Tile 内所有 Gaussian 完成透明混合。由于每个 Tile 覆盖的 Gaussian 数量通常只有几十到几百个因此排序成本相比全局排序大幅降低。整个渲染流程可以表示为Gaussian│ ▼Screen Projection│ ▼Assign Tile│ ▼Sort Inside Tile│ ▼Alpha Blending可以看到真正参与排序的不再是整个场景而只是每一个 Tile 内部的 Gaussian。图形实现以上是原版 3DGS 使用 CUDA 进行高效排序的实现思路。那么如果想在现代图形引擎中实现同样的效果该怎么做呢答案是可能并不太容易实现至少没有形成工程上的最优解。虽然现代 GPU 的算力非常强大但在 GPU 中进行高效的全局或局部排序本身就不是一件容易的事情。传统的图形渲染管线并没有提供现成的“排序”指令。一种可行的工程思路是利用计算着色器Compute Shader来复用上述的 Tile-Based 排序逻辑。具体流程可能如下投影与分配在 Compute Shader 中首先将所有 3D 高斯投影到 2D 屏幕空间并计算它们覆盖了哪些 Tile生成 (Tile ID, Depth, Gaussian ID) 的列表。GPU 排序在 Compute Shader 中实现或调用一个高效的并行排序算法如 GPU 版本的 Radix Sort 或 Bitonic Sort对这个列表进行排序。光栅化与混合排序完成后再将排序好的高斯列表传递给后续的渲染阶段可能是另一个 Compute Shader 或传统的片元着色器按照 Tile 顺序进行透明混合。因此尽管 3DGS 的渲染质量极高但其在通用图形引擎中的集成门槛很大程度上就卡在了这个“GPU 排序”的环节上。九、总结回顾整篇文章我们已经完整分析了 3D Gaussian Splatting 的整个可视化流程。从最初的三维高斯表示到最终生成屏幕上的每一个像素其渲染过程可以概括为以下几个阶段训练完成的三维高斯点云世界坐标转相机坐标3D高斯协方差矩阵投影至2D平面计算屏幕空间二维椭圆包围盒图像平面Tile网格划分高斯分配至对应Tile单个Tile内所有高斯按深度升序排序由远至近Alpha透明混合叠加输出最终渲染图像