高斯泼溅颜色计算

在这篇文章中,我们将经典的体渲染(Volume Rendering)数学模型转化为屏幕上的图像。数学公式初看可能有些令人生畏,但别担心,我们会一点点把它拆解。

在进入具体计算之前,我们需要明确当前所处的阶段:假设图像的“分块(Tiling)”已经完成。它将整个屏幕切分成若干个小块(通常是 16×16 像素),以便我们并行处理。

接下来,我们的终极目标是遍历这些 Tile(图像块),处理与每个 Tile 相交的高斯球,并计算出每个像素的最终颜色。


全局深度排序

渲染 3D 场景的第一直觉是什么?遮挡关系。离相机近的物体会遮挡远处的物体,因此我们必须对高斯球按照深度(z 值)进行排序。

  1. # 6. 全局深度排序 (Global Depth Sorting)
  2. order = torch.argsort(z_cam_v, descending=False)
  3. u_sorted = u_v[order]
  4. v_sorted = v_v[order]
  5. colors_sorted = colors_v[order]
  6. opacity_sorted = opacity_v[order]
  7. sigma_camera_sorted = sigma_camera_v[order]

从体渲染到高斯分布

当我们进入单个 Tile 的处理循环时,核心任务是计算当前像素的颜色 C。在 3DGS 的原论文中,这个过程是立足于经典的体渲染原理,并借鉴了基于神经点的方法演进而来的。

传统的体渲染中,像素的最终颜色 C 可以被重写为以下离散化公式:

C = \sum_{i=1}^{N} T_i \alpha_i c_i \quad

这里的 \alpha_i 是基于体密度 \sigma_i 和步长 \delta_i 计算的:

\alpha_i = (1 - \exp(-\sigma_i \delta_i))

T_i 是累计透射率:

T_i = \prod_{j=1}^{i-1} (1 - \alpha_j)

基于神经点的方法通过混合重叠在该像素上的 \mathcal{N} 个有序点,直接计算像素的颜色 C

C = \sum_{i \in \mathcal{N}} c_i \alpha_i \prod_{j=1}^{i-1} (1 - \alpha_j) \quad

在 3D Gaussian Splatting 中:

  • c_i 是每个高斯球的颜色(通过球谐函数计算得出)。
  • \alpha_i 不再是传统体渲染的积分,而是通过计算具有协方差矩阵 \Sigma 的 2D 高斯函数,并乘以学习到的“每点不透明度(per-point opacity)”得到的。

用公式直观理解就是:

\alpha_i = g(x, \Sigma) \times \text{Opacity}

在 2D 高斯的中心点,g(x) 值最大;随着像素点逐渐远离高斯中心,高斯球的贡献会逐渐衰减。这里我们先假设当前 Tile 内所有高斯球的 \alpha_i 已经计算完毕,它的形状为 [N, T*T]N 是高斯球数量,T×T 是像素数量)。


透射率 T_i 的计算

T_i = \prod_{j=1}^{i-1} (1 - \alpha_j) 表示累计透射率。

它的物理意义非常直观:光线在穿透前面的高斯球后,还有多少能量能继续往下传?如果 T_i = 1,说明前面空无一物;如果 T_i = 0,说明前面有一堵不透明的墙,当前高斯球完全被遮挡。

  1. # 计算累积透射率 T_i = \prod_{j=1}^{i-1} (1 - \alpha_j)
  2. ti = torch.cumprod(1.0 - alpha, dim=0)

直接对 (1 - \alpha_i) 运行 torch.cumprod,PyTorch 计算出的结果是从 j=1 乘到当前索引 i。比如输入 [1, 2, 3, 4],输出是 [1, 2, 6, 24]

但是我们的数学公式中是连乘到 i-1。这意味着当前高斯球的可见度,应该只由它前面的高斯球决定,不包含它自己。正确的输出应该是 [1, 1, 2, 6]

为了对齐数学公式,我们需要将 cumprod 的结果整体向后移动一位,并在最前面补上 1。

  1. # 错位偏置一位,首位补 1
  2. ti = torch.cat([
  3.     torch.ones((1, alpha.shape[1]), device=alpha.device, dtype=alpha.dtype),
  4.     ti[:-1]
  5. ], dim=0)

计算颜色与高阶广播机制

现在我们有了公式中的各个组件:T_i\alpha_ic_i。当前高斯球对像素的最终权重贡献 w 就可以轻松得出:

w = T_i \times \alpha_i

这个乘法是逐元素相乘,结果的形状依然是 [N, T*T]。最后一步是把权重 w 乘以颜色 c_i。这里我们遇到了维度不匹配的问题:

  • w 的形状是 [N, T*T]
  • c_i 的形状是 [N, 3]N 个高斯球,RGB 3个通道)

这时候,PyTorch 的广播机制 (Broadcasting) 就成了我们的利器,我们需要通过添加虚拟维度 (unsqueeze) 让它们对齐。

最后,我们只需要将所有高斯球(沿着 N 的维度)的贡献叠加起来,就得到了每个像素的最终颜色 C

  1. # 计算权重 w_i = alpha_i * T_i
  2. w = alpha * ti  # (N, P)
  3.  
  4. # 混合颜色:\sum_i w_i * c_i
  5. tile_colors = (w.unsqueeze(-1) * colors_tile.unsqueeze(1)).sum(dim=0)  # (P, 3)