3DGS 的全貌:从初始化到训练到渲染,每个环节都拆开讲清楚。
3DGS 的训练流程是一个迭代优化过程,整体可以分为以下几个阶段:
多视角图像
│
▼ COLMAP SfM
相机位姿 + 稀疏点云
│
▼ 初始化
数百万个三维高斯基元(初始位置来自点云)
│
▼ 迭代训练(30,000步)
│ ┌─────────────────────────────────────────────┐
│ │ 1. 随机选取一张训练视角图像 │
│ │ 2. 前向渲染(投影 + 排序 + Alpha合成) │
│ │ 3. 计算损失 L = L1 + λ·L_SSIM │
│ │ 4. 反向传播,计算所有高斯参数的梯度 │
│ │ 5. Adam 优化器更新参数 │
│ │ 6. 每100步:自适应密度控制(ADC) │
│ └─────────────────────────────────────────────┘
│
▼ 输出
已训练的高斯场景(.ply文件),可实时渲染

关键数字(Mip-NeRF 360 数据集上的典型值):
| 指标 | 数值 |
|---|---|
| 训练时间(RTX 3090) | ~35分钟 |
| 最终高斯数量 | 300-600万 |
| 场景文件大小(.ply) | 300-800 MB |
| 渲染速度(1080p) | 100-150 FPS |
| PSNR(室内场景) | ~31 dB |
思考题:3DGS 是”有监督学习”还是”无监督学习”?监督信号是什么?
每个高斯基元存储以下可学习参数:
| 参数 | 维度 | 含义 | 激活函数 |
|---|---|---|---|
| 位置 $\boldsymbol{\mu}$ | 3 | 高斯中心在世界坐标系的位置 | 无 |
| 旋转(四元数)$q$ | 4 | 高斯椭球的朝向 | 归一化 |
| 缩放 $\mathbf{s}$ | 3 | 三个主轴长度(对数存储) | $\exp$ |
| 不透明度 $o$ | 1 | 渲染时的基础不透明度 | sigmoid |
| 球谐系数 $\mathbf{f}$ | $16\times3$ | RGB三通道的视角相关颜色 | 无(结果用sigmoid) |
总参数量: $3+4+3+1+48 = 59$ 个标量/高斯
设有 $N = 300$ 万个高斯:
\[300 \times 10^4 \times 59 \times 4\ \text{字节} \approx 708\ \text{MB}\]这就是一个典型3DGS场景的存储需求,比NeRF(通常<50MB)大得多。这也是压缩方向(第9章)如此活跃的原因。

协方差矩阵的重建:
优化过程中存储四元数 $q$ 和缩放 $\mathbf{s}$,渲染时需要重建协方差矩阵:
\[\Sigma = R \cdot \text{diag}(s_x^2, s_y^2, s_z^2) \cdot R^T\]其中 $R$ 由四元数 $q$ 计算得到(详见第3章公式)。
思考题:3DGS的高斯基元参数在GPU上存储为什么格式?(提示:PyTorch的nn.Parameter,连续存储,利于CUDA并行读取)
这是3DGS的关键数学步骤:将三维高斯椭球投影到相机图像平面,得到二维高斯椭圆。
透视投影本身是非线性的(存在除以深度 $Z$ 的操作),但在局部范围内可以用一阶泰勒展开(雅可比近似)线性化:
\[\mathbf{x} = \pi(\mathbf{X}) \approx \pi(\boldsymbol{\mu}) + J(\mathbf{X} - \boldsymbol{\mu})\]其中 $J$ 是投影函数 $\pi$ 在高斯中心 $\boldsymbol{\mu}$ 处的雅可比矩阵。
对于针孔相机,相机坐标系下的点 $(X, Y, Z)$ 投影到图像坐标 $(x, y)$ 的雅可比为:
\[J = \begin{bmatrix} \frac{f_x}{Z} & 0 & -\frac{f_x X}{Z^2} \\ 0 & \frac{f_y}{Z} & -\frac{f_y Y}{Z^2} \end{bmatrix} \in \mathbb{R}^{2\times3}\]设世界坐标系下的三维协方差为 $\Sigma_W$,相机外参旋转为 $W$($3\times3$),则二维投影协方差:
\[\Sigma' = J W \Sigma_W W^T J^T \in \mathbb{R}^{2\times2}\]这是一个 $2\times3 \cdot 3\times3 \cdot 3\times3 \cdot 3\times3 \cdot 3\times2$ 的矩阵运算,结果是 $2\times2$ 的对称矩阵,完全确定了屏幕上二维高斯椭圆的形状。
对屏幕上的像素 $\mathbf{p} = (u, v)$,高斯基元的贡献(”影响力”):
\[\alpha(\mathbf{p}) = o \cdot \exp\!\left(-\frac{1}{2}(\mathbf{p} - \boldsymbol{\mu}')^T (\Sigma')^{-1} (\mathbf{p} - \boldsymbol{\mu}')\right)\]对 $2\times2$ 矩阵求逆:$(\Sigma’)^{-1} = \frac{1}{\det(\Sigma’)} \begin{bmatrix} \Sigma’{22} & -\Sigma’{12} \ -\Sigma’{12} & \Sigma’{11} \end{bmatrix}$,计算量极小。
实践中的优化: 只渲染 $\alpha > \epsilon$(如 $10^{-3}$)的像素,用高斯的边界包围盒裁剪,大量减少无效计算。

思考题:雅可比近似在高斯基元很大(覆盖大片视野)时会失效,因为透视变形不再是线性的。3DGS如何处理这种情况?(提示:大高斯会被ADC分裂)
3DGS 的核心性能来自于专门设计的 Tile-based CUDA 光栅化器。
(tile_id << 32 | depth_uint32)Radix Sort 的效率: 时间复杂度 $O(N \cdot b/w)$,其中 $b$ 是位宽(64位),$w$ 是每次处理的位数(通常8位)。对数百万高斯,GPU上只需几毫秒。
每个 tile 对应一个 CUDA 线程块($16\times16 = 256$ 个线程,每线程处理一个像素)。线程块内:
反向传播的挑战:Alpha 合成是从前到后的,但反向传播需要从后到前(需要知道后面的 $T$ 值)。
解决方案:从后向前扫描(Backward Pass Reverse Scan),或在前向时缓存每个像素最后存活的高斯 ID,从那里向前反推梯度。

思考题:为什么共享内存(Shared Memory)比全局显存(Global Memory)快这么多?(提示:延迟差距约100倍。思考L1 Cache和主存的类比)
每训练 100步,系统执行一次自适应密度控制(Adaptive Density Control,ADC),动态调整高斯的数量和分布。
训练过程中,记录每个高斯的屏幕空间位置梯度均值:
\[\bar{g}_k = \frac{1}{T} \sum_{t=1}^{T} \left\|\frac{\partial \mathcal{L}}{\partial \boldsymbol{\mu}_k'}\right\|\]位置梯度大 = 该高斯所在区域细节不够,需要更多基元来覆盖。
克隆(Clone):适用于小高斯($\max(s_x, s_y, s_z) < \tau_s$)且梯度大的情况。
分裂(Split):适用于大高斯($\max(s_x, s_y, s_z) \geq \tau_s$)且梯度大的情况。
剪枝(Prune):满足任一条件就删除:
不透明度重置(Opacity Reset):每隔约 3000 步,将所有高斯的不透明度重置为接近0的小值。这迫使真正有用的高斯重新”证明自己”,并给低不透明度高斯一个被剪枝的机会,防止高斯数量无限增长。

典型高斯数量变化:
思考题:为什么不直接从一开始就用600万个高斯,而是要从少量高斯开始,逐步增加密度?(提示:思考初始化质量和优化稳定性)
3DGS 的训练损失由两项组成:
\[\mathcal{L} = (1 - \lambda) \mathcal{L}_1 + \lambda \mathcal{L}_\text{D-SSIM}\]其中默认 $\lambda = 0.2$。
逐像素绝对误差。优点:对每个像素平等对待,训练稳定;缺点:对整体结构感知差(见第1章指标讨论)。
D-SSIM = 1 - SSIM,范围 $[0,1]$,越小越好。
\[\mathcal{L}_\text{D-SSIM} = 1 - \text{SSIM}(\hat{I}, I)\]SSIM 考虑局部 patch 内的亮度、对比度、结构,对于恢复清晰的边缘和纹理有帮助,是 L1 的有效补充。
0.2 的权重选择:原论文通过实验选定,D-SSIM权重过高会导致训练不稳定,过低则结构细节恢复不足。
为什么不用 LPIPS? LPIPS 是用神经网络计算的,前向+反向开销很大,在需要快速训练的3DGS中代价太高。L1 + D-SSIM 是性能与质量的工程平衡。

思考题:对于一张有强高光(specular highlight)的图像,L1损失会给高光区域分配怎样的梯度?这会影响高斯的球谐系数如何更新?
3DGS 使用 Adam 优化器,不同参数有不同的学习率:
| 参数 | 初始学习率 | 调度方式 | 终止学习率 |
|---|---|---|---|
| 位置 $\boldsymbol{\mu}$ | $1.6\times10^{-4}$ | 指数衰减至 $1.6\times10^{-6}$ | 低 |
| 旋转 $q$ | $1\times10^{-3}$ | 常数 | 常数 |
| 缩放 $\mathbf{s}$ | $5\times10^{-3}$ | 常数 | 常数 |
| 不透明度 $o$ | $5\times10^{-2}$ | 常数 | 常数 |
| 球谐系数 $\mathbf{f}$ | $2.5\times10^{-3}$ | 常数 | 常数 |
位置学习率指数衰减的原因:
训练初期,高斯位置变化剧烈(需要从初始点云向正确位置移动);后期,高斯已大致到位,需要精细调整,过大的学习率会导致震荡。指数衰减让训练从”粗定位”过渡到”精细调整”。
关键训练节点:
| 迭代步数 | 发生的事 |
|---|---|
| 0 | 从SfM点云初始化所有高斯(位置+默认参数) |
| 1~999 | 只训练颜色($l=1$球谐),固定位置和形状,让颜色先收敛 |
| 1000+ | 开始优化所有参数;每100步ADC |
| 3000, 6000, 9000 | Opacity Reset |
| 7000 | 保存一次中间检查点 |
| 30000 | 训练结束,保存最终模型 |
思考题:为什么训练初期(前1000步)要固定位置和形状,只训练颜色?这样的课程训练(Curriculum Learning)有什么好处?
gaussian_renderer/__init__.py(前向渲染实现)submodules/diff-gaussian-rasterization/cuda_rasterizer/(CUDA光栅化核心代码)| *上一章:第五章:体渲染与 Alpha 合成 | 下一章:第七章:从原始论文到开源代码* |