3D 高斯泼溅:从零实现


文档摘要

3D 高斯泼溅:从零实现 本节摘要:一个场景就是数百万个 3D 高斯的点云,每个高斯有位置、朝向、尺度、不透明度,以及随视角变化的颜色;把它们光栅化、对光栅化反传,就完了。本节讲清 3D 高斯泼溅(3DGS)为何在 2026 年取代 NeRF 成为照片级 3D 重建的生产默认:光栅化而非光线步进,分钟级训练、100+ fps 渲染、参数可直接编辑。我们从零搭一个 2D 泼溅光栅化器(投影、瓦片分配、深度排序、alpha 合成),再把它扩展到 3D(四元数旋转、协方差、球谐颜色、致密化与剪枝),最后用 nerfstudio/gsplat 从 2050 张照片重建场景并导出 glTF/USD。读完本节,你理解了 3DGS 的全部数学与工程。

3D 高斯泼溅:从零实现

本节摘要:一个场景就是数百万个 3D 高斯的点云,每个高斯有位置、朝向、尺度、不透明度,以及随视角变化的颜色;把它们光栅化、对光栅化反传,就完了。本节讲清 3D 高斯泼溅(3DGS)为何在 2026 年取代 NeRF 成为照片级 3D 重建的生产默认:光栅化而非光线步进,分钟级训练、100+ fps 渲染、参数可直接编辑。我们从零搭一个 2D 泼溅光栅化器(投影、瓦片分配、深度排序、alpha 合成),再把它扩展到 3D(四元数旋转、协方差、球谐颜色、致密化与剪枝),最后用 nerfstudio/gsplat 从 20~50 张照片重建场景并导出 glTF/USD。读完本节,你理解了 3DGS 的全部数学与工程。

对应原课程:Phase 4 · Lesson 22 · 3d-gaussian-splatting(原英文 phases/04-computer-vision/22-3d-gaussian-splatting/docs/en.md)。

学习目标

阅读完本节,你应当能够:

  1. 解释 3D 高斯泼溅为何在 2026 年取代 NeRF 成为照片级 3D 重建的生产默认。
  2. 说出每个高斯的六类参数(位置、旋转四元数、尺度、不透明度、球谐颜色、可选特征)及各自贡献几个 float。
  3. 从零用 alpha 合成实现一个 2D 高斯泼溅光栅化器,并说明 3D 情形如何投影到同一循环。
  4. nerfstudiogsplatSuperSplat 从 20~50 张照片重建场景,导出到 KHR_gaussian_splatting glTF 扩展或 OpenUSD 26.03 的 UsdVolParticleField3DGaussianSplat 模式。

一、问题与直觉

NeRF 把场景存在 MLP 权重里,每个渲染像素是沿光线上百次 MLP 查询,训练数小时、渲染数秒,且权重不可编辑——想挪一把椅子就得重训。

3D 高斯泼溅(Kerbl、Kopanas、Leimkühler、Drettakis,SIGGRAPH 2023)把这一切都换掉了。场景是一组显式 3D 高斯,渲染是 100+ fps 的 GPU 光栅化,训练只需几分钟,编辑是直接的:平移一组高斯,椅子就挪走了。到 2026 年,Khronos 已批准高斯泼溅的 glTF 扩展,OpenUSD 26.03 内置高斯泼溅模式,Zillow 和 Apartments.com 用它渲染房产,大多数 3D 重建新论文都是 3DGS 核心思想的变体。

心智模型简单,但数学部件够多,多数介绍从光栅化讲起、跳过投影和球谐。本节搭全——先 2D,再扩展到 3D。

一个高斯带什么

一个 3D 高斯是空间里的一个参数化斑,带这些属性:

位置 mu (3,) 世界坐标中心 旋转 q (4,) 单位四元数编码朝向 尺度 s (3,) 每轴对数尺度(渲染时取指数) 不透明度 alpha (1,) sigmoid 后的 [0, 1] SH 系数 c_lm (3 * (L+1)^2,) 视角相关颜色

旋转 + 尺度构建 3×3 协方差:Sigma = R S S^T R^T,即高斯在 3D 里的形状。球谐让颜色随视角变化——镜面高光、微妙光泽、视角相关辉光——无需存每视角纹理。SH 度 3 每颜色通道 16 个系数,仅颜色就每高斯 48 个 float。

一个场景通常有 100~500 万个高斯,每个约存 60 个 float(3+4+3+1+48+杂项)。500 万高斯约 240 MB——远小于带逐点纹理的等价点云,也比高分辨率重渲染的 NeRF MLP 权重小一个数量级。

是光栅化,不是光线步进

五步,全是 GPU 友好的。没有每像素 MLP 查询。单张 RTX 3080 Ti 把 600 万泼溅渲染到 147 fps。

投影这一步

世界位置 mu、3D 协方差 Sigma 的 3D 高斯投影成屏幕位置 mu'、2D 协方差 Sigma' 的 2D 高斯:

mu' = project(mu) Sigma' = J W Sigma W^T J^T (2×2) W = 视角变换(相机旋转 + 平移) J = 透视投影在 mu' 处的雅可比

2D 高斯的足迹是一个椭圆,轴是 Sigma' 的特征向量。椭圆内每个像素接收该高斯的贡献,按 exp(-0.5 (p − mu')^T Sigma'^{-1} (p − mu')) 加权。

alpha 合成规则

对一个像素,覆盖它的那些高斯按由后向前排序(等价地由前向后用反转公式)。颜色用自 1980 年代起每个半透明光栅化器都用的同一方程合成:

C_pixel = sum_i alpha_i * T_i * c_i T_i = prod_{j < i} (1 - alpha_j) 到 i 为止的透射率 alpha_i = opacity_i * exp(-0.5 d^T Sigma'^{-1} d) 局部贡献 c_i = eval_SH(SH_i, 视方向) 视角相关颜色

与 NeRF 体积渲染是同一方程,只是作用在显式稀疏高斯集上,而非沿光线的稠密采样。这个同一性是渲染质量匹敌 NeRF 的原因——两者都在积分同一个辐射场方程。

为何这可微

每一步——投影、瓦片分配、alpha 合成、SH 求值——都对高斯参数可微。给定真值图,算渲染像素损失,对光栅化器反传,用梯度下降更新所有 (mu, q, s, alpha, c_lm)。约 3 万次迭代后,高斯找到正确的位置、尺度和颜色。

致密化与剪枝

固定的高斯集盖不住复杂场景。训练含两个自适应机制:

  • 克隆(Clone)——当一个高斯梯度幅值大但尺度小时,在当前位置克隆一个,这里需要更多细节。
  • 分裂(Split)——当一个大尺度高斯梯度大时,把它分裂成两个更小的,一个大高斯太平滑、拟不好这块区域。
  • 剪枝(Prune)——不透明度掉到阈值以下的高斯删掉,它们没贡献。

致密化每 N 次迭代跑一次。场景通常从约 10 万初始高斯(从 SfM 点播种)长到训练结束时的 100~500 万。

球谐一段话讲清

视角相关颜色是单位球上的函数 c(direction)。球谐是球面的傅里叶基,截到度 L 每通道有 (L+1)² 个基函数。为新视角求颜色,就是学到的 SH 系数与该视角方向上求值的基做点积。度 0 = 一个系数 = 常数色;度 3 = 16 个系数 = 足以捕捉朗伯着色、镜面、轻微反射。3DGS 论文默认用度 3。

2026 生产栈

1. 采集 手机 / 大疆无人机 / 手持扫描仪 2. SfM / MVS COLMAP 或 GLOMAP 推导相机位姿 + 稀疏点 3. 训 3DGS nerfstudio / gsplat / inria 官方 / PostShot(RTX 4090 上约 10~30 分钟) 4. 编辑 SuperSplat / SplatForge(清漂浮物、分割) 5. 导出 .ply -> glTF KHR_gaussian_splatting 或 .usd(OpenUSD 26.03) 6. 查看 Cesium / Unreal / Babylon.js / Three.js / Vision Pro

4D 与生成式变体

  • 4D 高斯泼溅——高斯是时间的函数,用于体积视频(超人 2026、A$AP Rocky 的 "Helicopter")。
  • 生成式泼溅——文本转泼溅模型(World Labs 的 Marble),幻觉出整个场景。
  • 3D 高斯无味变换——NVIDIA NuRec 用于自动驾驶仿真的变体。

二、从零实现

步骤 1:一个 2D 高斯

先搭 2D 光栅化器,3D 情形投影后归约到它。

import torch import torch.nn as nn import torch.nn.functional as F def eval_2d_gaussian(means, covs, points): """ means: (G, 2) 中心 covs: (G, 2, 2) 协方差矩阵 points: (H, W, 2) 像素坐标 返回: (G, H, W) 每个高斯在每个像素的密度 """ G = means.size(0) H, W, _ = points.shape flat = points.view(-1, 2) inv = torch.linalg.inv(covs) diff = flat[None, :, :] - means[:, None, :] d = torch.einsum("gpi,gij,gpj->gp", diff, inv, diff) density = torch.exp(-0.5 * d) return density.view(G, H, W)

einsum 为每个(高斯, 像素)对算二次型 diff^T Sigma^{-1} diff

步骤 2:2D 泼溅光栅化器

由前向后 alpha 合成。2D 里深度无意义,故用一个可学的每高斯标量定顺序。

def rasterise_2d(means, covs, colours, opacities, depths, image_size): """ means: (G, 2) covs: (G, 2, 2) colours: (G, 3) opacities: (G,) [0, 1] depths: (G,) 用于定序的每高斯标量 image_size: (H, W) 返回: (H, W, 3) 渲染图 """ H, W = image_size yy, xx = torch.meshgrid( torch.arange(H, dtype=torch.float32, device=means.device), torch.arange(W, dtype=torch.float32, device=means.device), indexing="ij", ) points = torch.stack([xx, yy], dim=-1) densities = eval_2d_gaussian(means, covs, points) alphas = opacities[:, None, None] * densities alphas = alphas.clamp(0.0, 0.99) order = torch.argsort(depths) alphas = alphas[order] colours_sorted = colours[order] T = torch.ones(H, W, device=means.device) out = torch.zeros(H, W, 3, device=means.device) for i in range(means.size(0)): a = alphas[i] out += (T * a)[..., None] * colours_sorted[i][None, None, :] T = T * (1.0 - a) return out

不快——真实实现用基于瓦片的 CUDA kernel——但数学完全正确且全可微。

步骤 3:可训练的 2D 泼溅场景

class Splats2D(nn.Module): def __init__(self, num_splats=128, image_size=64, seed=0): super().__init__() g = torch.Generator().manual_seed(seed) H, W = image_size, image_size self.means = nn.Parameter(torch.rand(num_splats, 2, generator=g) * torch.tensor([W, H])) self.log_scale = nn.Parameter(torch.ones(num_splats, 2) * math.log(2.0)) self.rot = nn.Parameter(torch.zeros(num_splats)) # 2D 单角度 self.colour_logits = nn.Parameter(torch.randn(num_splats, 3, generator=g) * 0.5) self.opacity_logit = nn.Parameter(torch.zeros(num_splats)) self.depth = nn.Parameter(torch.rand(num_splats, generator=g)) def covs(self): s = torch.exp(self.log_scale) c, si = torch.cos(self.rot), torch.sin(self.rot) R = torch.stack([ torch.stack([c, -si], dim=-1), torch.stack([si, c], dim=-1), ], dim=-2) S = torch.diag_embed(s ** 2) return R @ S @ R.transpose(-1, -2) def forward(self, image_size): covs = self.covs() colours = torch.sigmoid(self.colour_logits) opacities = torch.sigmoid(self.opacity_logit) return rasterise_2d(self.means, covs, colours, opacities, self.depth, image_size)

log_scaleopacity_logitcolour_logits 都是无约束参数,渲染时通过正确激活映射——这是每个 3DGS 实现的标准模式。

步骤 4:把 2D 高斯拟合到目标图

import math import numpy as np def make_target(size=64): yy, xx = np.meshgrid(np.arange(size), np.arange(size), indexing="ij") img = np.zeros((size, size, 3), dtype=np.float32) # 红圆 mask = (xx - 20) ** 2 + (yy - 20) ** 2 < 10 ** 2 img[mask] = [1.0, 0.2, 0.2] # 蓝方块 mask = (np.abs(xx - 45) < 8) & (np.abs(yy - 40) < 8) img[mask] = [0.2, 0.3, 1.0] return torch.from_numpy(img) target = make_target(64) model = Splats2D(num_splats=64, image_size=64) opt = torch.optim.Adam(model.parameters(), lr=0.05) for step in range(200): pred = model((64, 64)) loss = F.mse_loss(pred, target) opt.zero_grad(); loss.backward(); opt.step() if step % 40 == 0: print(f"step {step:3d} mse {loss.item():.4f}")

200 步内 64 个高斯落进两个形状。这就是全部思想——对显式几何基元做梯度下降。

步骤 5:从 2D 到 3D

3D 扩展保持同一循环,新增:

  1. 每高斯旋转是四元数而非单角度。
  2. 协方差是 R S S^T R^T,R 由四元数构建,S = diag(exp(log_scale))
  3. 投影 (mu, Sigma) -> (mu', Sigma') 用相机外参和透视投影在 mu 处的雅可比。
  4. 颜色变成球谐展开,在视方向处求值。
  5. 深度排序用真实的相机空间 z,而非可学标量。

每个生产实现(gsplatinria/gaussian-splattingnerfstudio)都恰好在 GPU 上用基于瓦片的 CUDA kernel 做这件事。

步骤 6:球谐求值

SH 基到度 3 每通道 16 项。求值:

def eval_sh_degree_3(sh_coeffs, dirs): """ sh_coeffs: (..., 16, 3) 末维是 RGB 通道 dirs: (..., 3) 单位向量 返回: (..., 3) """ C0 = 0.282094791773878 C1 = 0.488602511902920 C2 = [1.092548430592079, 1.092548430592079, 0.315391565252520, 1.092548430592079, 0.546274215296039] x, y, z = dirs[..., 0], dirs[..., 1], dirs[..., 2] x2, y2, z2 = x * x, y * y, z * z xy, yz, xz = x * y, y * z, x * z result = C0 * sh_coeffs[..., 0, :] result = result - C1 * y[..., None] * sh_coeffs[..., 1, :] result = result + C1 * z[..., None] * sh_coeffs[..., 2, :] result = result - C1 * x[..., None] * sh_coeffs[..., 3, :] result = result + C2[0] * xy[..., None] * sh_coeffs[..., 4, :] result = result + C2[1] * yz[..., None] * sh_coeffs[..., 5, :] result = result + C2[2] * (2.0 * z2 - x2 - y2)[..., None] * sh_coeffs[..., 6, :] result = result + C2[3] * xz[..., None] * sh_coeffs[..., 7, :] result = result + C2[4] * (x2 - y2)[..., None] * sh_coeffs[..., 8, :] # 度 3 项此处省略;完整 16 系数版见代码文件 return result

学到的 sh_coeffs 存「该高斯每个方向的颜色」,渲染时对当前视方向求值得 3 维 RGB。

三、框架对比

真 3DGS 工作用 gsplat(Meta)或 nerfstudio:

pip install nerfstudio gsplat ns-download-data example ns-train splatfacto --data path/to/data

splatfacto 是 nerfstudio 的 3DGS 训练器,RTX 4090 上典型场景 10~30 分钟。

2026 年要紧的导出选项:

  • .ply——原始高斯云(便携,文件最大)。
  • .splat——PlayCanvas / SuperSplat 量化格式。
  • glTF KHR_gaussian_splatting——Khronos 标准,跨查看器便携(2026 年 2 月 RC)。
  • OpenUSD UsdVolParticleField3DGaussianSplat——USD 原生,用于 NVIDIA Omniverse 和 Vision Pro 流水线。

4D / 动态场景,4DGSDeformable-3DGS 用时变均值和不透明度扩展同一机制。

四、可复用产物

本节产出两个可复用文件(位于原课程 outputs/):

  • prompt-3dgs-capture-planner.md:一个提示词——为给定场景类型规划采集(照片数、相机路径、打光)。
  • skill-3dgs-export-router.md:一个技能——给定下游查看器或引擎,挑对的导出格式(.ply / .splat / glTF / USD)。

五、练习

  1. (简单) 在另一张合成图上跑上述 2D 泼溅训练器,num_splats[16, 64, 256],画出各自的 MSE 对步数图,找出收益递减点。
  2. (中等) 扩展 2D 光栅化器,让每个高斯的 RGB 颜色经度 2 谐函数依赖一个标量「视角」。在一对目标图上训练,验证模型能重建两者。
  3. (困难) 克隆 nerfstudio,在你手头的任意场景(桌面、植物、人脸、房间)20 张照片上训 splatfacto。导出 glTF KHR_gaussian_splatting 并在查看器(Three.js GaussianSplats3D、SuperSplat、Babylon.js V9)打开。报告训练时间、高斯数、渲染 fps。

本节要点回顾

  1. 3DGS = 显式高斯点云取代 NeRF 的 MLP——光栅化而非光线步进,分钟训练、100+ fps 渲染、参数可直接编辑。
  2. 每高斯六类参数:位置(3)、旋转四元数(4)、对数尺度(3)、不透明度(1)、SH 颜色(度 3 时 48)、杂项;共约 60 float。
  3. 协方差 Sigma = R S S^T R^T——旋转(四元数)+ 各向异性尺度决定单个高斯形状。
  4. 五步光栅化——投影、瓦片分配、深度排序、由前向后 alpha 合成、出像素;全 GPU 友好。
  5. 投影 Sigma' = J W Sigma W^T J^T——3D 协方差经视角变换 W 和透视雅可比 J 投到 2D 椭圆。
  6. alpha 合成 = NeRF 体积渲染同一方程——C=Σ alpha_i T_i c_i,作用在显式稀疏高斯集上,故质量匹敌 NeRF。
  7. 全流程可微——对光栅化反传,梯度下降更新所有参数,约 3 万迭代收敛。
  8. 致密化三招:克隆(梯度大尺度小)、分裂(梯度大尺度大)、剪枝(不透明度过低);从 10 万长到 100~500 万。
  9. 球谐存视角相关颜色——球面傅里叶基,度 3 每通道 16 系数,捕捉镜面/朗伯/反射。
  10. 2026 生态:nerfstudio/gsplat 训练、SuperSplat 编辑、glTF KHR_gaussian_splatting 与 OpenUSD 26.03 跨平台导出、4D 与生成式变体。

下一节进入扩散 Transformer 与整流流——把 U-Net 换成 DiT、把 DDPM 噪声调度换成整流流,这是 SD3/FLUX 背后的新范式。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U