3DGS 如何工作:Gaussian、投影、排序与可微训练
用工程直觉和必要公式拆解 3D Gaussian Splatting 的 primitive、协方差、屏幕投影、球谐颜色、Alpha 合成与密度控制。
3DGS 的核心可以压缩成一句话:用一组可学习的三维椭球描述场景,把可见椭球投影成屏幕上的二维椭圆,再按前后关系进行透明度合成。
它的速度并不是因为 Gaussian 天生神奇,而是表示、优化和 GPU rasterization 被设计成了同一条路径:训练时渲染的就是部署时要显示的显式 primitive,不需要在每个像素的光线上反复查询大型神经网络。
理解这条路径后,许多概念会自然连起来:协方差控制椭球形状,球谐函数控制视角相关颜色,排序和 Alpha 合成产生像素,梯度再把图像误差传回每个 Gaussian。3DGS 官方项目页
从一个三维 Gaussian 开始
一个未归一化的三维 Gaussian 可以写成:
G(x) = exp(−½(x − μ)TΣ−1(x − μ))
其中:
x是空间中的查询位置;μ是 Gaussian 中心;Σ是 3×3 协方差矩阵,决定椭球朝向和三个主轴的长度。
距离中心越远,贡献按指数衰减。和无限小的点相比,一个 Gaussian 能覆盖一片空间;和固定体素相比,它可以任意旋转和拉伸,更容易沿真实表面分布。
为什么不用直接优化完整协方差矩阵
任意 3×3 数值矩阵不一定是有效协方差。原始 3DGS 将它分解为旋转与尺度:
Σ = RSSTRT
R 通常由四元数参数化,S 是三个方向的尺度。这样既便于优化,也能保证协方差保持半正定。工程实现通常还会对尺度、透明度等原始参数应用激活函数,避免直接优化时越过有效范围。
颜色为什么会随视角变化
每个 Gaussian 可以只保存固定 RGB,但真实场景中的高光、反射和细薄结构会随观察方向变化。原始 3DGS 使用球谐函数(Spherical Harmonics,SH)把观察方向映射成颜色修正。
可以把它理解成一个低阶方向函数:相机从不同方向看同一个 Gaussian,得到略有不同的颜色。SH 阶数越高,可表达的方向变化越复杂,但每个 Gaussian 要保存的系数也更多。
必须注意:**SH 记录的是训练数据里出现过的视角相关外观,不等于理解了真实材质。**它可能近似一个高光,却不知道光源在哪里,也不能保证换灯光后产生正确反射。
三维椭球怎样投影到屏幕
渲染器先把 Gaussian 中心从世界空间变换到相机空间,再通过相机投影落到屏幕坐标。三维协方差也要变成二维协方差,决定屏幕上椭圆的方向和大小。
在局部仿射近似下,可以把屏幕空间协方差概括为:
Σ′ = J W Σ WTJT
W 表示世界到相机的线性变换,J 是透视投影在 Gaussian 中心附近的 Jacobian。这个二维协方差随后转换为 conic 形式,渲染器就能快速判断一个像素离椭圆中心多远、当前 Gaussian 对它贡献多少。
“在中心附近线性化”也是误差来源。Gaussian 很大、靠近相机或位于宽视场边缘时,真实透视投影不再接近一个简单二维椭圆。Mip-Splatting 处理跨尺度 aliasing;Exact-GS 则直接针对 affine approximation 与投影一致性提出更精确的 splatting。Mip-Splatting Exact-GS
为什么渲染前必须排序
透明物体合成与顺序有关。沿相机方向从前向后排列 Gaussian 后,一个像素的颜色可以写成:
C = Σi Ti αi ci, Ti = Πj<i(1 − αj)
αᵢ 是该 Gaussian 在当前像素的有效透明度,cᵢ 是方向相关颜色,Tᵢ 表示光线到达它之前还剩多少透射率。前面的 Gaussian 已经接近不透明时,后面的贡献可以提前停止计算。
原始 rasterizer 不会为每个像素独立整理所有 Gaussian。它将屏幕切成 tiles,为每个覆盖到的 tile 生成 Gaussian 实例,按 tile 和深度排序,再由 tile 内像素并行合成。这样减少了调度成本,却意味着深度排序是 primitive/tile 层面的近似,不是严格的每像素表面交点排序。
这解释了几个现象:
- 两个大 Gaussian 深度交叉时,可能出现 popping;
- 透明、薄片和近景特别容易暴露排序近似;
- Gaussian 过大不仅损害几何,也会制造巨量 overdraw;
- 文件里 Gaussian 数量相同,屏幕覆盖面积不同,渲染成本可能完全不同。
可微渲染怎样反过来训练场景
每次迭代大致执行:
- 选择一台训练相机和对应真实图像;
- 用当前 Gaussian 渲染预测图;
- 计算像素损失,原始实现组合 L1 与 D-SSIM;
- 通过可微 rasterizer 将梯度传回位置、尺度、旋转、透明度和 SH;
- 周期性调整 Gaussian 数量和透明度。
gsplat 将这套 CUDA rasterization 和常见训练策略做成了更便于研究与集成的开源库,并提供稀疏梯度、分布式训练和多种 rasterization 选项。它很适合用来阅读现代 3DGS 工程实现,但其 API 和性能特性仍应以当前官方文档为准。gsplat 论文 gsplat 文档
Densification:为什么训练中会“长出”新 Gaussian
固定数量的 primitive 很难同时拟合大平面和细枝。原始 3DGS 根据位置梯度等信号识别重建不足的区域,再进行两类操作:
- 小 Gaussian 对局部细节不够时,可以 clone 到附近;
- 大 Gaussian 承担过多结构时,可以 split 成更小的 Gaussian。
同时,透明度很低、过大或不再有贡献的 primitive 会被 prune。训练还会阶段性重置透明度,避免早期不透明 Gaussian 长期挡住后方区域。
这是一套有效但带有启发式色彩的控制系统。阈值改变会影响质量、Gaussian 数量、显存和训练稳定性;后来的 MCMC、anchor 和压缩方法,很多都在重新设计“何时长点、何时删点、属性如何共享”。
为什么训练图很漂亮,新视角仍会坏
优化目标只能约束输入相机看到的内容。以下区域天然证据不足:
- 所有相机都没拍到的背面;
- 只有极少视角覆盖的边界和角落;
- 被动态物体长期挡住的背景;
- 镜面、透明和重复纹理区域;
- 相机位姿本身错误的图像。
模型可能用“从训练视角看起来正确”的浮动 Gaussian 解释这些像素。换一个新角度,伪解就会暴露。这不是简单增加迭代次数能解决的问题,而是观测不足和表示先验的问题。
显存和速度应该怎样估算
只统计 Gaussian 数量不够。实际成本至少包括:
- 每个 Gaussian 的位置、尺度、旋转、透明度和 SH 属性;
- 优化器状态与梯度,训练成本远高于只读渲染;
- 每帧 tile-instance 列表和排序临时缓冲;
- 屏幕分辨率、Gaussian 平均覆盖面积与 overdraw;
- 双眼、多相机或多层合成带来的重复工作;
- 解压、上传和格式转换的峰值内存。
因此“论文报告 100 FPS”不能直接推导“移动 VR 可用”。必须同时说明 GPU、分辨率、场景、Gaussian 数量、渲染器、是否双眼以及测量的是纯 rasterization 还是完整应用帧。
从原理推导出的工程结论
采集覆盖比盲目调参重要
没有图像证据的区域无法靠优化可靠恢复。先修复相机轨迹、曝光和覆盖,再讨论损失函数。
大 Gaussian 是质量和性能的双重风险
它们可能跨越真实深度边界,近看形成软片,还会覆盖更多 tiles 和像素。生产资产应检查尺度分布与屏幕 footprint,而不是只删“看起来飘”的点。
SH 不是 PBR 材质
它适合复现观察方向相关外观,不提供可编辑粗糙度、金属度和真实光照响应。需要重光照时,应增加材质分解或保留 Mesh/PBR 层。
Splat 不是可靠碰撞面
透明度合成给出的是视觉遮挡,不自动定义封闭表面。游戏、AR 和机器人任务需要深度、Mesh、SDF 或其他几何代理。
下一篇将把这些原理落到完整实践:怎样拍摄、怎样判断 COLMAP 是否成功、怎样用 Splatfacto 训练,以及为什么导出 PLY 后还没有完成交付。