A deep-dive · 3DGRT · 3DGUT · 3DGRUT and beyond

光线追踪高斯:当 3DGS 装上 BVH 与 RT Core

一份写给已经懂 NeRF / SDF / 线代但还没系统学过 3D Gaussian Splatting 的读者的「由浅入深」讲解。全文按 问题数学交互代码 的循环展开,主线是 NVIDIA 在 2024–2025 推出的 3DGRT / 3DGUT / 3DGRUT,以及它们引爆的整片衍生工作。

面向:NeRF / SDF / 机器学习入门者 阅读时长 $\approx 35\text{–}45$ 分钟(含 demo 把玩) 最后更新 · 2026-05-19

三句话理解全文

🌫️ 3DGS:把场景表示成几百万颗椭球,光栅化投影 + per-tile sort + $\alpha$-blend。

📐 3DGUT:把 EWA 的 Jacobian 投影换成 unscented transform,代价 23%,换来任意畸变相机支持。

🔦 3DGRT:把每颗椭球包成 icosahedron 进 BVH,OptiX RT cores 光追。慢 ~$3\times$,换来二次光线(反射 / 折射 / 阴影 / 卷帘)。

♻️ 3DGRUT:上面两条混合——主光线走 UT 快,次光线走 RT 准。

§ 1复习:从 NeRF/SDF 走到 3D Gaussian Splatting

你已经熟悉 NeRF:一个把场景编码进 MLP 权重的"隐式辐射场",渲染靠沿光线 march 数百次 query。你大概也接触过 SDF:把场景压缩成"到表面的有符号距离",渲染靠 sphere tracing。3D Gaussian Splatting (3DGS) 走了第三条路:把场景表示成几百万颗带朝向、带颜色、带不透明度的椭球体粒子,渲染靠光栅化把它们 splat 到屏幕上。

直觉
NeRF 用一个"装满辐射场的神经函数"代替几何;3DGS 用"一群发着光的椭球微粒"代替几何。前者是一锅匀浆,后者是一捧米。米粒可以一颗一颗搬动、删除、上色——这正是 3DGS 能被实时渲染、可编辑的根本原因。

1.1 表示与渲染的本质区别

维度NeRF (Mildenhall 2020)3DGS (Kerbl 2023)
场景表示隐式 MLP $F(\mathbf{x},\mathbf{d}) \to (\sigma, c)$显式百万级椭球 $\{\mu, \Sigma, \alpha, c_{\text{SH}}\}$
渲染原语沿射线 ray march,每像素 query MLP 64–256 次椭球投影到屏幕 + per-tile 排序 + $\alpha$-blend
训练时长Vanilla 1-2 天;Instant-NGP 数分钟~30-50 分钟(M360 / T&T)
渲染速度< 1 FPS @ 1080p30-100+ FPS @ 1080p
可微性全可微(autograd)全可微(splatting 解析梯度)
可编辑性差(权重无空间局部性)好(每颗高斯是独立可移动原子)
二次光线 / 反射原理可做,但慢得不可用原生不支持——需要 3DGRT 等扩展

1.2 3DGS 的四个核心公式

把这四条记牢,后面所有"光追扩展"都是在动其中某一条。

① 参数化

$i$ 颗高斯:

$$ \theta_i = \big\{\;\mu_i \in \mathbb{R}^3,\; s_i \in \mathbb{R}^3_+,\; q_i \in \mathbb{S}^3,\; \alpha_i \in [0,1],\; \mathbf{c}_i \in \mathbb{R}^{3(\ell+1)^2}\;\big\} $$

其中 $s_i$ 是三轴 scale,$q_i$ 是单位四元数(旋转),$\alpha_i$ 是 opacity,$\mathbf{c}_i$ 是 spherical-harmonics 系数(让颜色随观察方向变化)。

② 协方差分解

$$ \Sigma_i = R(q_i)\,S(s_i)\,S(s_i)^{\top}\,R(q_i)^{\top} $$

把 6 维 PSD 矩阵的优化转成对 $(s, q)$ 共 7 维的优化——缩放 + 旋转足以参数化任意各向异性椭球。

③ EWA 投影:3D → 2D

$$ \mu'_i = \pi(\mu_i),\qquad \Sigma'_i = J\,W\,\Sigma_i\,W^{\top}\,J^{\top} $$

$W$ 是 view 矩阵的 $3\!\times\!3$ 旋转部分,$J$ 是相机投影 $\pi$$\mu_i$ 处的 Jacobian。注意这一步是"在均值处的一阶 Taylor 展开"——这正是后面 3DGUT 要替换掉的地方。

④ Alpha-blending(front-to-back)

$$ C(\mathbf{x}) = \sum_{i=1}^{N} \mathbf{c}_i\,\alpha'_i\,T_i,\quad T_i = \prod_{j\lt i}(1-\alpha'_j),\quad \alpha'_i = \alpha_i \cdot \exp\!\Big(-\tfrac{1}{2}(\mathbf{x}-\mu'_i)^{\top}\Sigma'^{-1}_i(\mathbf{x}-\mu'_i)\Big) $$
与 NeRF 完全等价
把 NeRF 的连续积分 $C = \int T(t)\sigma(t)\mathbf{c}(t)\,dt$ 离散化(Max 1995),令 $\alpha_i := 1 - e^{-\sigma_i \delta_i}$,立刻得到上面这条公式。3DGS 不是替换了 NeRF 的体渲染,而是把"采样点"从"沿射线的等距步进"换成了"被射线穿过的、已排序的几何粒子"。

1.3 你的第一个 demo:沿射线的 alpha-blend

下面这个 demo 把 NeRF/3DGS 共享的 volume rendering integral 拆开给你看。沿一条横向射线放了若干颗 1D 高斯(你可以把它当成"3D 高斯被一条射线穿过时的横截面强度"),下面画出 transmittance $T(t)$ 一路衰减、最终颜色 $C$ 一颗颗累积的过程。拖动高斯的中心、改变它们的 opacity 与 sigma,看 alpha-blending 如何工作。

Demo A · 沿射线的 alpha-compositing

每颗高斯都把"自己的颜色"按 $\alpha_i T_i$ 贡献到最终像素。拖动顶部圆点移动高斯,下方滑块控制 sigma 与 opacity。

响应 g_i(t) 透射率 T(t) 累积颜色 C(t)

注意几个数学事实:(a) 加入越多高斯,$T$ 越快衰减到 0(b) 一旦 $T \to 0$,后面的高斯就再不贡献了——这正是 ray tracing 时 "k-buffer / early-out" 的依据;(c) 公式对"沿射线积分"是天然适配的——这就是为什么 3DGS 能被自然地搬到 ray tracing 上。

§ 2光栅化高斯的三条软肋

把 3D Gaussian 投影到屏幕、按 tile 排序、再 alpha-blend——这条流水线在针孔相机 + 主光线 only 的假设下闪闪发光,但它有三个隐含的雷区。理解它们是理解 3DGRT/3DGUT 为什么必须存在的前提。

2.1 软肋一:非针孔相机让 EWA 投影崩坏

EWA 投影 $\Sigma'_i = J\Sigma_i J^{\top}$非线性投影 $\pi(\cdot)$$\mu_i$ 处一阶 Taylor 展开。在标准针孔相机下,FOV $60^{\circ}$ 左右、Gaussian 又小,一阶 Taylor 的残差可以忽略。但是:

  • 📷 鱼眼 / 全景相机:FOV $\ge 120^{\circ}$,$\pi$ 在 Gaussian 支撑范围内已经强烈弯曲,"投影还是椭球" 这个结论失效。
  • 📐 Gaussian 横跨视场边缘:粒子一半在中心一半在 $90^{\circ}+$ 处,$\mu_i$ 处的 Jacobian 只代表局部斜率。
  • 🚗 卷帘快门(rolling shutter):每一行的曝光时刻不同,等价于一个时间相关的 $\pi(\mathbf{x}, t)$,EWA 干脆不能表达。

下面这个 demo 把这件事画给你看:一颗 3D Gaussian,用真实非线性投影(在每个像素上各自算)与EWA 一阶近似(只在均值处算一次 Jacobian)画到屏幕上,对比两者的差距。把 FOV 推到 $160^{\circ}+$,看 EWA 的椭圆怎么和真实形状渐行渐远。

Demo B · 针孔 vs 鱼眼:EWA 在哪里崩

左:相机真实投影(每像素独立采样)。右:EWA 一阶近似(投影 + Jacobian 拉伸)。把 Gaussian 拖到视场边缘,或拉大 FOV,看红色 (EWA) 椭圆怎么从真实形状(蓝色 splat)偏离。

真实投影(per-pixel sample) EWA 一阶 Taylor Gaussian 中心

2.2 软肋二:per-tile sort 引发 popping

3DGS 把屏幕切成 16×16 的 tile,同一个 tile 里的所有像素共享同一次按深度的排序。这是为了让 GPU shader 能做整齐的并行 alpha-blend——但代价是:当相机微动时,某两颗 Gaussian 在 tile 内的"代表深度"突然互换,整个 tile 的所有像素同步发生颜色跳变。这就是著名的 popping artifact。

隐藏 bug
per-tile sort 是一个近似,它假设"tile 这么小,深度顺序差不多"。但 anisotropic Gaussian 可以非常扁、非常长——同一颗 Gaussian 在 tile 不同像素的深度差距可能比相邻 Gaussian 还大。3DGRT 把"排序"还给每条 ray 自己来做(per-ray k-buffer),是这个问题的根治。

2.3 软肋三:光栅化里没有"光线"这个一等公民

反射、折射、阴影、Ambient Occlusion、参与介质——这些都需要从某个 3D 点再发射一条 ray。光栅化是前向投影:从 3D 几何到 2D 屏幕,是一条单向流。没有"光线 hit 之后从交点继续 trace"的概念。

实践上 3DGS 社区有各种 hack(虚拟相机做镜子、deferred shading 做反射 lobe……),但这些都是"绕着光线走"。要做真正的二次光线,你必须有 BVH,必须能从任意 3D 点向任意方向投射 ray——这正是 ray tracing 提供的。

§ 33DGRT:让光线真的穿过高斯

Moenne-Loccoz, Mirzaei, et al. — 3D Gaussian Ray Tracing: Fast Tracing of Particle Scenes, NVIDIA + U. Toronto, SIGGRAPH Asia 2024. 这是把整套光线追踪流水线(OptiX、BVH、RT cores、any-hit shader)接到 3DGS 上的开山之作。它要解决的核心问题是:BVH 的最小单元是三角形,可 3D Gaussian 是一团发着光的雾,怎么让它进 BVH?

3.1 Proxy primitive:拉伸的 icosahedron

答案优雅而务实:给每颗 Gaussian 套一个紧致的 20 面凸壳(icosahedron),把它的 20 个三角面交给 OptiX 的 RT cores。Hull 的顶点按协方差被拉伸:

$$ \mathbf{v}' = \sqrt{2\log(\sigma_i / \alpha_{\min})}\; S_i\,R_i^{\top}\,\mathbf{v} + \mu_i \qquad\text{(3DGRT Eq. 7)} $$

其中 $\alpha_{\min}$ 通常取 0.01。这个系数保证:任意一条 ray 只要在 hull 外侧,它对该 Gaussian 的 alpha 贡献必小于 $\alpha_{\min}$。换句话说,截断误差可控。论文消融过 AABB(太松)、octahedron(够紧但三角形不够多 → 在 RT cores 上跑得不快)、icosahedron(甜点):icosahedron 是 $\text{bounding 紧致度} \times \text{三角形数}$ 的最优解。

为什么要 hull,而不是直接 trace 椭球?
OptiX / RT cores 的硬件 ray-triangle 求交是专门固化在芯片里的。给它三角形它能跑几十亿条/秒;给它 implicit ellipsoid,你要走 software intersector,慢 $10\times$。Icosahedron 是"用 20 个三角形近似一颗椭球"的工程妥协——既保留了 RT core 加速,又把误差压在 1% 之下。

3.2 沿射线的 response 与 t*

当一条 ray $\mathbf{r}(t) = \mathbf{o} + t\mathbf{d}$ 穿过 Gaussian $(\mu_i, \Sigma_i)$,沿途的响应是:

$$ g_i(\mathbf{r}(t)) = \exp\!\left(-\tfrac{1}{2}(\mathbf{r}(t)-\mu_i)^{\top}\Sigma_i^{-1}(\mathbf{r}(t)-\mu_i)\right) $$

它是 $t$ 的二次型 exp,恰好有一个解析最大点。把 $\mathbf{o}_g = S_i^{-1}R_i^{\top}(\mathbf{o}-\mu_i)$$\mathbf{d}_g = S_i^{-1}R_i^{\top}\mathbf{d}$ 代入:

$$ t^{*}_i = \frac{(\mu_i - \mathbf{o})^{\top}\Sigma_i^{-1}\mathbf{d}}{\mathbf{d}^{\top}\Sigma_i^{-1}\mathbf{d}} = -\,\frac{\mathbf{o}_g^{\top}\mathbf{d}_g}{\mathbf{d}_g^{\top}\mathbf{d}_g} \qquad\text{(3DGRT Eq. 8)} $$

关键近似:不沿 ray 积分整段响应,只取响应最大点处的值作为该 Gaussian 在该 ray 上的"有效 alpha"

$$ \alpha_i^{\text{ray}} = \sigma_i \cdot g_i(\mathbf{r}(t^{*}_i)) $$

这与 splatting 中"2D EWA peak"在数学上等价,但在真实 3D 空间里计算——不受相机投影模型的影响。

3.3 BVH 遍历 + per-ray k-buffer(demo)

有了 hull、有了 BVH、有了 $t^*_i$,整个流水线变成:

  1. 每条 ray 走 BVH,命中的所有 hull 触发 any-hit shader
  2. any-hit shader 计算 $t^*$$\alpha^{\text{ray}}$插入到 ray 自己的长度 k=16 的排序缓冲
  3. 缓冲满了就做一次 alpha-composite 并消费,继续 trace;
  4. 直到 transmittance $T \lt \epsilon$ 或 ray 走出场景。

下面这个 demo 把上面的过程在 2D 里演给你看:屏幕里散布了若干 Gaussian,左下角的 BVH(绿色矩形)在动态打包它们。拖动相机原点和方向发射一条 ray,看遍历到的 Gaussian 被按 $t^*$ 排进 k-buffer,最后合成最终颜色。打开 "per-tile sort 模式" 看光栅化方案与之的对比。

Demo C · BVH 遍历与 per-ray k-buffer

拖蓝色原点移动相机;拖紫色端点改变 ray 方向。下方面板显示沿 ray 排序的 k-buffer 与合成颜色。切换 per-ray sort(3DGRT)与 per-tile sort(3DGS)看 popping 现象。

ray hit / 在 k-buffer 中 BVH 包围盒 hull 三角网

3.4 OptiX any-hit shader 伪代码

整个核心循环差不多长这样(语义化版本,省略 OptiX 7 boilerplate):

CUDA / OptiX 7 (semantic)// Per-ray payload: 长度 k 的 sorted hit buffer + 累积颜色 + 透射率
struct Payload {
    float3 color;   // 已累积颜色 C
    float  trans;   // 透射率 T
    int    n;       // 缓冲中 hit 数
    Hit    buf[K];  // 按 t* 升序排好
};

// ----- any-hit: 每命中一颗 hull 三角面就被调一次 -----
extern "C" __global__ void __anyhit__particle()
{
    const int gid     = optixGetPrimitiveIndex() / 20; // 20 三角面/icosahedron
    const Gaussian g  = gaussians[gid];
    const float3 o    = optixGetWorldRayOrigin();
    const float3 d    = optixGetWorldRayDirection();

    // 1) 沿 ray 的解析最大响应点 t*  (Eq. 8)
    const float t_star = ray_tstar(o, d, g);
    // 2) 在 t* 处的 Gaussian 响应  (Eq. 9)
    const float resp   = gaussian_response(o + t_star*d, g);
    const float alpha  = g.opacity * resp;
    if (alpha < 1e-3f) { optixIgnoreIntersection(); return; }

    // 3) 插入 k-buffer 并保持按 t* 排序
    Payload* p = getPayloadPtr();
    insert_sorted(p->buf, p->n, {t_star, alpha, sh_eval(g.sh, d), gid});

    // 4) 缓冲满或某种 flush 策略 → 立即消费一段
    if (p->n == K) flush_buffer(p);

    optixIgnoreIntersection();  // 不要终止 ray,让它继续命中下一颗
}

// ----- ray-gen: 主循环 -----
extern "C" __global__ void __raygen__() {
    Payload p = {make_float3(0), 1.0f, 0, {}};
    Ray r = primary_ray(launch_index);

    while (p.trans > 1e-3f && !ray_exited_scene(r)) {
        optixTrace(handle, r.o, r.d, t_min, t_max,
                   /* mask */ 0xFF, /* flags */ OPTIX_RAY_FLAG_NONE,
                   SBT_RAY_TYPE_PARTICLE, NUM_RAY_TYPES, 0,
                   /* payload */ payload_handle(&p));
        flush_buffer(&p);                 // 消费缓冲 → 累积到 (color, trans)
        r = advance_ray(r, p.last_t);     // 让 ray 继续往前 trace
    }
    framebuffer[launch_index] = p.color + p.trans * background;
}
和 splatting 流水线相比,发生了什么变化?
  • "投影到屏幕" 这一步消失了:取而代之的是 BVH ray query。
  • "per-tile sort" 消失了:每条 ray 自己维护一个长度 k 的小 sort buffer。
  • "EWA Jacobian" 消失了:直接在 3D 空间求 $t^*$ 和响应。
  • 因为 ray 已是一等公民,反射、折射、阴影只需要在 hit point 再 optixTrace 一次

训练(反向传播)怎么做?

OptiX 走两遍。Forward:trace 一次,把每条 ray 命中的 particle index 序列缓存到 global memory。Backward:用相同的 ray 重新 trace(hit 序列已被缓存),按相同顺序 replay,对 $(\mathbf{c}_i, \sigma_i, \mu_i, \Sigma_i)$ 做链式法则梯度,atomicAdd 写回共享梯度缓冲。代价:训练慢 3DGS 约 $2\times$。推理慢 $2\text{-}3\times$(见下表)。

数据集3DGRT PSNR ↑3DGS PSNRFPS 3DGRTFPS 3DGS
Mip-NeRF 36028.8828.8378238LPIPS 显著优 (0.179 vs 0.228)
Tanks & Temples23.0323.35190319SSIM 更高、popping 消除
Deep Blending29.8929.43119267
NeRF Synth (fisheye)40.53不支持splatting 无法工作

结论:同 PSNR、更好 LPIPS、~$3\times$ 慢——但拿回了二次光线、非针孔相机、卷帘补偿这些光栅化拿不到的能力

§ 43DGUT:用无迹变换把畸变相机塞回光栅化

Wu, Martinez Esturo, Mirzaei, Moenne-Loccoz, Gojcic — 3DGUT: Enabling Distorted Cameras and Secondary Rays in Gaussian Splatting, CVPR 2025 Oral. 3DGRT 解决了所有问题但要付 $3\times$ 性能税。NVIDIA 接着想:能不能不上 BVH,仅把 EWA 的"一阶 Taylor"换掉,让 splatting 也能吃鱼眼、卷帘?

4.1 重新看 EWA:它本质上是个 "linearize-then-propagate"

给定 3D Gaussian $\mathcal{N}(\mu, \Sigma)$ 和相机 $\pi(\cdot)$,EWA 的做法是:

  1. $\pi$$\mu$ 处做一阶 Taylor:$\pi(\mathbf{x}) \approx \pi(\mu) + J(\mathbf{x}-\mu)$
  2. 把 Gaussian 直接套上去:$\mu' = \pi(\mu)$$\Sigma' = J\Sigma J^{\top}$

这就是控制论里的 "Extended Kalman Filter"(EKF)!而 EKF 在函数曲率大、协方差大的工况下会迅速积累误差——这也正是 3DGS 在鱼眼下崩坏的物理原因。

把信号处理领域的工具搬过来
Julier & Uhlmann 1997 在 EKF 之外提出了无迹变换 (Unscented Transform, UT):与其线性化函数,不如确定性地采样几个"代表点"穿过原函数,再从穿过结果重新拟合输出的均值和协方差。这套工具直接搬到 3DGS,就是 3DGUT。

4.2 Sigma points 的精确构造(n=3,因此 $2n+1=7$ 个点)

对 3D Gaussian $\mathcal{N}(\mu, \Sigma)$

$$ \chi_0 = \mu,\qquad \chi_i = \mu + \big(\sqrt{(n+\lambda)\,\Sigma}\big)_i,\qquad \chi_{i+n} = \mu - \big(\sqrt{(n+\lambda)\,\Sigma}\big)_i,\quad i=1,\dots,n $$

其中 $\big(\sqrt{(n+\lambda)\Sigma}\big)_i$$\sqrt{(n+\lambda)\Sigma}$(一般用 Cholesky)的第 $i$ 列。尺度参数 $\lambda = \alpha^2(n+\kappa) - n$,论文取 $\alpha=1.0,\ \beta=2.0,\ \kappa=0.0$

Weights:

$$ w_0^{m} = \tfrac{\lambda}{n+\lambda},\quad w_0^{c} = \tfrac{\lambda}{n+\lambda} + (1-\alpha^2+\beta),\quad w_i^{m} = w_i^{c} = \tfrac{1}{2(n+\lambda)},\ i=1\dots 2n $$

任意 非线性相机 $\pi$(鱼眼、卷帘、广角畸变都行)把 7 个 sigma points 各自投到 2D,再加权拟合:

$$ \mathcal{Y}_i = \pi(\chi_i),\qquad \mu' = \sum_{i=0}^{2n} w_i^{m}\,\mathcal{Y}_i,\qquad \Sigma' = \sum_{i=0}^{2n} w_i^{c}\,(\mathcal{Y}_i-\mu')(\mathcal{Y}_i-\mu')^{\top} $$
为什么 UT 更准?
在 Gaussian 输入下,UT 对任意非线性函数精确捕捉到二阶矩(EWA 只精确到一阶),并且对对称分布的三阶矩也精确——相当于免费送到 3 阶。代价是:每个 Gaussian 投影从 1 次变成 7 次,外加一个 $3\!\times\!3$ Cholesky。论文实测端到端 265 FPS vs 3DGS 的 347 FPS(开销约 23%),换来对任意 $\pi$ 的支持。

下面的 demo 把这一切画给你看。左边是一颗 Gaussian + 5 个 sigma points(2D 简化版,$n=2$ 所以是 5 个,3D 真实情况是 7 个);右边是经过非线性 lens 函数 $\pi$ 后的样子。注意「真实 $\pi$ 真值」用 400 点 Monte Carlo 估计的真实投影分布的 mean+cov——这才是 UT 和 EWA 都在估计的目标。把畸变拉大,看蓝色 UT 椭圆怎么贴着绿色真值椭圆不放,而红色 EWA 椭圆则慢慢飘走。

Demo D · Sigma points 投影 vs EWA Jacobian 投影

拖动 Gaussian 在场景中移动;畸变滑块 控制 lens 非线性强度。UT(蓝)应当贴住真值(绿);EWA(红虚线)在大畸变下会偏。真值绿椭圆 = 真实投影分布的 Monte-Carlo Gaussian 拟合(400 点);绿色点线 = 真实投影 $2\sigma$ 边界本身(非高斯)。

真实 $\pi$ 真值(绿实线 = MC Gaussian 拟合;点线 = 实际非高斯轮廓) UT 重建(蓝实线) EWA 一阶 Taylor(红虚线) 5 个 sigma points

4.3 NumPy 实现(30 行讲明 UT 投影)

Python / NumPy — 教学版import numpy as np

def unscented_project(mu, Sigma, pi,
                      alpha=1.0, beta=2.0, kappa=0.0):
    """
    把 3D Gaussian (mu, Sigma) 通过任意非线性相机 pi: R^3 -> R^2 投影。
    返回投影后的 2D 均值与协方差。
    pi 可以是鱼眼、卷帘、广角畸变 —— 唯一假设是它可调用。
    """
    n = mu.shape[0]                                  # = 3
    lam = alpha**2 * (n + kappa) - n
    c = n + lam

    # 1) Sigma points
    L = np.linalg.cholesky(c * Sigma)                # L L^T = (n+λ)Σ
    chi = np.empty((2*n + 1, n))
    chi[0] = mu
    for i in range(n):
        chi[1+i]   = mu + L[:, i]
        chi[1+n+i] = mu - L[:, i]

    # 2) Weights
    wm = np.full(2*n+1, 1.0 / (2*c))
    wc = wm.copy()
    wm[0] = lam / c
    wc[0] = lam / c + (1 - alpha**2 + beta)

    # 3) 让每个 sigma point 独立穿过非线性投影
    Y = np.stack([pi(x) for x in chi], axis=0)       # shape (7, 2)

    # 4) 重新拟合
    mu_p = (wm[:, None] * Y).sum(axis=0)             # (2,)
    dY   = Y - mu_p                                  # (7, 2)
    Sigma_p = (wc[:, None, None] * (dY[:, :, None] * dY[:, None, :])).sum(0)

    return mu_p, Sigma_p


# --- 用鱼眼相机模型试一下 ---
def fisheye_pi(x_cam, f=1.0):
    """ Equidistant fisheye: r = f·θ, θ 是与光轴的夹角 """
    x, y, z = x_cam
    r3 = np.sqrt(x*x + y*y + z*z)
    theta = np.arccos(z / r3)
    phi   = np.arctan2(y, x)
    r2    = f * theta
    return np.array([r2 * np.cos(phi), r2 * np.sin(phi)])

mu    = np.array([0.4, 0.3, 1.0])
Sigma = np.diag([0.04, 0.04, 0.01])
mu_p, Sigma_p = unscented_project(mu, Sigma, fisheye_pi)
print("Projected mean:", mu_p)
print("Projected cov: \n", Sigma_p)

整段实现里没有任何对 $\pi$ 的导数运算。把鱼眼换成卷帘快门(带 $t$ 输入的 $\pi(x, t)$),代码一行不用动——这就是 UT 相对 EWA 的核心优势。

§ 53DGRUT:混合架构,又快又能反射

3DGRT 全部走 RT cores → 准、能做反射,但慢。
3DGUT 全部走光栅化 → 快、能吃畸变相机,但还是没二次光线。
那能不能让主光线走 UT,二次光线走 RT?这就是 NVIDIA 实际开源的 3DGRUT——不是第三篇论文,而是把 3DGUT 与 3DGRT 合并成一条混合管线的系统

混合可行的数学前提
3DGUT 论文里刻意把光栅化的渲染方程重写成"每条 ray 在 3D 最大响应点 $t^*$ 处评估 + multi-layer alpha blend"——和 3DGRT 完全同构。因此同一份 $\{\mu_k, \Sigma_k, c_k, \alpha_k\}$ 既能被光栅化器消费,也能被 OptiX BVH 消费,两条管线共享梯度,loss 同时反传。
能力原版 3DGS3DGRT3DGUT3DGRUT(混合)
针孔相机
鱼眼 / 卷帘
反射 / 折射 / 阴影(二次光线)
速度(相对 3DGS)$1\times$$\sim 0.3\times$$\sim 0.75\times$$\sim 0.5\times$ 主路径快, 二次慢
需要 RT cores

训练时混合 pipeline 的 loss 与原版同构:$\mathcal{L} = (1-\lambda_s)\mathcal{L}_2 + \lambda_s \mathcal{L}_{\text{SSIM}}$$\lambda_s = 0.2$。带反射/折射 mask 的像素走 RT 分支,其它像素走 UT 分支,因为参数共享,梯度在两条路径上叠加回传。

§ 6衍生宇宙:基于光追思想的 3DGS 工作地图(2024.07–2026.05)

3DGRT/3DGUT 砸开了一扇大门,2025 年下半年到 2026 上半年涌出大量后续工作。下面按主题分类——每条只给一句话 takeaway 与论文链接,便于你按图索骥。

6.1 反射 / 镜面 / 玻璃

  • EnvGS (CVPR 2025) — 用一组"环境高斯"代替环境贴图建模远场反射,RT 采样。arXiv 2412.15215
  • RaySplats (2025) — 最干净的"全程光追 3DGS"版本,主/次光线统一。arXiv 2501.19196
  • Stochastic Ray Tracing for 3DGS (2026) — 用俄罗斯轮盘随机采样,$4\text{-}8\times$ 加速。arXiv 2603.23637
  • Ref-Gaussian (ICLR 2025) — mask-free 的反射解耦。arXiv 2406.05852
  • Mirror-3DGS / MirrorGaussian — 专攻平面镜场景。arXiv 2404.01168
  • TransparentGS (SIGGRAPH 2025) — 透明高斯 + deferred refraction。arXiv 2504.18768
  • GlossyGS (TVCG 2025) — glossy 物体的 BRDF inverse rendering。project

6.2 二次光线 / Relighting / 全局光照

  • IRGS (CVPR 2025) — 2D Gaussian 光追 + 完整渲染方程的可微 inter-reflection。arXiv 2412.15867
  • Relightable 3D Gaussians — BRDF + 光追阴影的早期工作。arXiv 2311.16043
  • RTR-GS (2025) — 统一可见性、间接光、材质回归。arXiv 2507.07733
  • GeoSplatting — mesh-based 光追做 PBR inverse rendering。arXiv 2410.24204
  • Approximated GI for 3DGS — 屏幕空间 Monte Carlo 一次反弹 GI。arXiv 2504.01358
  • ShadowGS — 卫星图像 sun-direction shadow ray。arXiv 2601.00939

6.3 参与介质 / 雾 / 烟

  • Don't Splat your Gaussians (SIGGRAPH 2025) — scattering/emissive media 用 Gaussian 体素核 + 闭式 transmittance。arXiv 2405.15425
  • DehazeGS — 多视角去雾 + 重建。arXiv 2501.03659
  • SmokeSeer (CMU MS thesis 2025) — 同时去烟与重建。

6.4 相机模型(鱼眼 / 卷帘 / 事件相机)

  • 3DGUT§4 主线本身。
  • $200^{\circ}$ 鱼眼综述 (2025) — 实拍大畸变下 3DGUT 仍最稳。arXiv 2508.06968
  • GS on the Move — 卷帘 + 运动模糊联合补偿。arXiv 2403.13327
  • Event-based 3D Gaussian Ray Tracing (2025) — megapixel 事件流端到端。arXiv 2512.18640
  • 4D-GRT (2025) — 4D 动态 GS + 物理光追,生成可控相机效果。arXiv 2509.10759

6.5 加速 / 工程化 / 编辑

  • GRTX (2026) — ray-space transform 把各向异性高斯"球化",BVH 更紧。arXiv 2601.20429
  • REdiSplats (2025) — 高斯绑 mesh proxy + 光追 = 可编辑场景。arXiv 2503.12284
  • RaRa Clipper — 光栅 + 光追混合做高质量裁剪。arXiv 2506.20202
  • gsplat 库集成 3DGUT — NVIDIA 已合入 nerfstudio 主线。blog
如果只选 5 篇精读
3DGRT、3DGUT、EnvGS、IRGS、Stochastic Ray Tracing for 3DGS — 它们分别代表 ray tracing 内核、相机模型、反射 primitive、可微的二次光线、Monte Carlo 工程化,把整张地图的支撑点全占了。

§ 7在你的机器上跑:最小路径

NVIDIA 的官方实现集中在 nv-tlabs/3dgrut。要求:Linux、CUDA 11.8-13.0、OptiX、PyTorch、带 RT cores 的 NVIDIA GPU(RTX 20 系以上)。最小 hello-world 流程:

bashgit clone --recursive https://github.com/nv-tlabs/3dgrut.git
cd 3dgrut

# 1) 装环境(CUDA 12.x 推荐)
conda create -n 3dgrut python=3.10 -y && conda activate 3dgrut
./install_env.sh                    # 装 PyTorch、OptiX、自家 CUDA 内核

# 2) 用 NeRF Synthetic 的 lego 数据试一发 3DGUT 训练
python train.py \
    -p configs/paper/3dgut/unsorted_3dgut.yaml \
    --path datasets/lego \
    --out_dir ./runs/lego_3dgut

# 3) 切到 3DGRT (全程光追)
python train.py \
    -p configs/paper/3dgrt/unsorted_3dgrt.yaml \
    --path datasets/lego \
    --out_dir ./runs/lego_3dgrt

# 4) 混合(3DGRUT):主路径 UT、反射区域走 RT
python train.py \
    -p configs/paper/3dgrut.yaml \
    --path datasets/lego_with_mirror \
    --out_dir ./runs/lego_3dgrut

如果你只有 nerfstudio:3DGUT 已经 merged into mainline gsplat,最近的版本里 gsplat.rasterizationcamera_model="fisheye_ut" 直接可用。

§ R参考文献与延伸阅读

  1. Kerbl, Kopanas, Leimkühler, Drettakis. 3D Gaussian Splatting for Real-Time Radiance Field Rendering. SIGGRAPH 2023.
    arXiv:2308.04079 · link
  2. Zwicker, Pfister, van Baar, Gross. EWA Volume Splatting. IEEE Vis 2001.
  3. Max, N. Optical Models for Direct Volume Rendering. IEEE TVCG 1(2), 1995.
    体渲染离散化的奠基论文。
  4. Mildenhall, Srinivasan et al. NeRF. ECCV 2020.
    arXiv:2003.08934
  5. Julier & Uhlmann. A New Extension of the Kalman Filter to Nonlinear Systems. AeroSense 1997.
    Unscented Transform 的原始论文。
  6. Moenne-Loccoz, Mirzaei et al. 3D Gaussian Ray Tracing: Fast Tracing of Particle Scenes. SIGGRAPH Asia 2024.
    arXiv:2407.07090 · project page
  7. Wu, Martinez Esturo, Mirzaei, Moenne-Loccoz, Gojcic. 3DGUT: Enabling Distorted Cameras and Secondary Rays in Gaussian Splatting. CVPR 2025 Oral.
    arXiv:2412.12507 · project page
  8. NVIDIA Developer Blog. Revolutionizing Neural Reconstruction and Rendering in gsplat with 3DGUT. 2025.
本页 demo 全部用纯 vanilla Canvas 2D 实现,无依赖、无后端,开源在与本页同一目录的 demos.js 中。 如果你发现公式或讲解错误,欢迎指正——这只是一份学习笔记。