想象你站在展厅中央,用手机绕着展品拍了二三十张照片。你的目标,是把这些零散照片变成一件可以在屏幕里随意旋转、实时浏览的立体影像。老一代算法 NeRF 像个“用绣花针绣画的人”:它从眼睛出发,沿着屏幕上的每一条光线,在空间里一针一线地采样,每走一小步都要问一次神经网络“这里是什么颜色”,一条光线要问一两百遍,绣完一整幅画自然要好几秒。3D 高斯泼溅(3D Gaussian Splatting,简称 3DGS)则完全不同,它像一位“泡泡拼贴师”:先根据照片撒下一批种子(稀疏点云),把每颗种子吹成一个半透明、可拉伸可旋转的彩色泡泡(高斯椭球)。出图时,按照从远到近的顺序把泡泡一颗颗贴到屏幕上,颜色自然叠加,千万颗泡泡眨眼间就拼出一幅逼真的立体画。泡泡能被显卡并行地贴,所以渲染不再是绣花,而像倒豆子一样哗啦完成,速度比 NeRF 快了几十倍,画质却毫不逊色。本文就用“泡泡拼贴”这条线,拆解它的三大法宝——高斯表示、可微光栅化、自适应密度控制,再看看 GPU 并行与训练如何让这一切落地。

岗位关联

理解 3DGS 的三大关键设计,你将看懂当前最热门的实时神经渲染方案,也为数字孪生、XR、自动驾驶仿真等高帧率三维应用打下扎实的图形学基础。

目录
  1. 可视化演示
  2. 渲染范式对比
  3. 高斯表示原理
  4. 可微光栅化
  5. 密度自适应控制
  6. GPU并行渲染
  7. 训练与画质
  8. 应用前景与局限
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示3D高斯表示模型、可微光栅化泼溅算法、自适应密度控制策略及与NeRF的性能对比。

本节配有交互动效(进入视口后自动加载,约 3KB)。

3D高斯表示模型 均值μ:高斯椭球在三维空间中的中心位置坐标 协方差Σ:3×3矩阵描述高斯的形状、方向和尺寸分布 不透明度α控制透明度,球谐系数c表示视角相关颜色

渲染范式对比

绣花匠和泡泡拼贴师的较量,本质上是两种数学路线的较量。绣花匠(NeRF)相信“要想画得准,必须每一寸空间都问到”:一条光线从相机出发穿透场景,每隔一小段距离就停下来采样颜色和密度,把所有采样积分起来才得到最终像素。这个过程叫体渲染,它在数学上非常优美,代价是惊人地昂贵——一帧 1080p 的画面,每个像素都要重复上百次神经网络前向计算,算下来一帧就有上亿次查询,于是渲染一张图动辄数秒甚至更久。

泡泡拼贴师则相信“既然物体是实实在在摆在空间里的,就不用猜每一寸空气的颜色,只看离眼睛最近的表面就行”。光栅化(rasterization)正是这样:把三维物体直接投影到屏幕上的落点,按遮挡关系一层层画上去。3DGS 把场景建模成成千上万颗高斯泡泡,渲染时只需把每颗泡泡的投影贴到画布上,每颗泡泡只被处理一次,计算量不再随像素数增长。

所以泡泡拼贴师能赢,不是他力气大,而是他根本不干“逐点绣花”这种苦差事:每颗泡泡只贴一次,天生就适合实时。

NeRF 每帧计算量≈Npixels×Nsamples≫3DGS 每帧计算量≈Ngaussians\displaystyle \text{NeRF 每帧计算量} \approx N_{\text{pixels}} \times N_{\text{samples}} \quad\gg\quad \text{3DGS 每帧计算量} \approx N_{\text{gaussians}}
  1. 像素数 $N_{\text{pixels}}$屏幕上的像素总量。1080p 约有 200 万个像素,这是 NeRF 无法绕过的基础乘数。
    Npixels≈2×106N_{\text{pixels}} \approx 2 \times 10^6
  2. 采样数 $N_{\text{samples}}$每条光线在空间中采样多少步。NeRF 通常要 100-300 步,相当于每绣一针都要问一次网络。
    Nsamples≈102∼103N_{\text{samples}} \approx 10^2 \sim 10^3
  3. 高斯数 $N_{\text{gaussians}}$场景中高斯泡泡的总数,常见规模是 100 万-500 万颗,但每颗只需投影一次。
    Ngaussians≈106N_{\text{gaussians}} \approx 10^6
  4. 量级对比NeRF 的总计算量是“两百万像素 × 几百次采样”的乘法关系,3DGS 却把计算量压缩到“只与物体复杂度有关”,两者相差几十到上百倍。
    Npixels×Nsamples≈109≫Ngaussians≈106N_{\text{pixels}} \times N_{\text{samples}} \approx 10^9 \gg N_{\text{gaussians}} \approx 10^6

高斯表示原理

泡泡拼贴师手里的每一颗泡泡,都不是普通的圆球——它是一颗“会说话”的软边彩色椭球,由五个属性完整描述:中心位置 $\boldsymbol{\mu}$(泡泡悬在哪里)、胖瘦与朝向(由协方差矩阵 $\Sigma$ 决定)、基础颜色(可随观察角度变化)、以及不透明度 $\alpha$(泡泡是透亮还是厚实)。

“高斯”这个词听起来吓人,其实就是数学里的钟形曲线推广到三维:离泡泡中心越远,泡泡的“存在感”越弱,边缘呈现柔和的渐变而不是生硬的边界。这种软边正是保证拼贴画面没有接缝的关键——就像水彩画里相邻色块靠晕染自然过渡。为了描述一个任意旋转、被拉伸的椭球,3DGS 不会直接存 3×3 的协方差矩阵,而是把它拆成独立的缩放矩阵 $S$(控制三根轴的胖瘦)和旋转矩阵 $R$(控制朝向),两者相乘再转置回来,既保证矩阵合法,又方便优化。

除了几何属性,泡泡的颜色用球谐函数(spherical harmonics)编码——你可以把它理解成“在泡泡表面写了几行颜色备忘录”,不同角度看向泡泡时,它能读出略有差异的颜色,从而还原真实物体随视角变化的光泽感。一颗泡泡的参数就是它的全部"DNA":位置 3 个、旋转 4 个、缩放 3 个、颜色若干、透明度 1 个,训练就是不断修改这套 DNA。

G(x)=exp⁡(−12(x−μ)⊤Σ−1(x−μ))\displaystyle G(\mathbf{x}) = \exp\left(-\frac{1}{2}(\mathbf{x}-\boldsymbol{\mu})^{\top} \Sigma^{-1}(\mathbf{x}-\boldsymbol{\mu})\right)
  1. 位置 $\boldsymbol{\mu}$高斯泡泡的中心坐标,决定它悬浮在场景的哪个位置。
    μ=(μx,μy,μz)\boldsymbol{\mu} = (\mu_x, \mu_y, \mu_z)
  2. 协方差矩阵 $\Sigma$一个 3×3 矩阵,编码椭球的胖瘦、拉伸和朝向,由缩放与旋转组合而来。
    Σ=RSS⊤R⊤\Sigma = R S S^{\top} R^{\top}
  3. 指数衰减 $\exp$高斯值从中心 1 开始向四周平滑衰减到 0,让泡泡拥有水彩般的软边缘。
    G(x)→0当∥x−μ∥→∞G(\mathbf{x}) \to 0 \quad \text{当} \quad \|\mathbf{x}-\boldsymbol{\mu}\| \to \infty
  4. -1/2 系数保持标准高斯分布的形式,让衰减速度与协方差的尺度自然匹配。
    −12(x−μ)⊤Σ−1(x−μ)-\frac{1}{2}(\mathbf{x}-\boldsymbol{\mu})^{\top}\Sigma^{-1}(\mathbf{x}-\boldsymbol{\mu})

可微光栅化

泡泡拼贴师贴画的方式很讲究:他先在心中把所有泡泡按“离眼睛的远近”排好队,然后从最远的开始,一颗颗往幕布上按。每一颗泡泡都是半透明的,所以后来的泡泡要透过前面泡泡的“透光率”才能叠上去——这就是alpha 混合(alpha blending),远看是照片,近看是层层叠叠的彩色光斑。

要把 3D 的泡泡贴到 2D 屏幕上,第一步是投影:透视原理告诉我们,同样大小的球,离得远就看起来小。因为高斯函数经过线性变换后仍然是高斯,所以 3D 椭球投到屏幕上变成 2D 椭圆,只需用相机变换和投影矩阵把协方差转换到屏幕坐标系即可:

投影完成后再按深度排序,对每个像素遍历“覆盖到这里的泡泡”,执行 alpha 混合。假设某像素被 $n$ 颗泡泡覆盖,渲染颜色 $C$ 是由远到近逐层累加的:

关键在于,公式里的每一步——投影、排序、混合——都是数学函数,误差从像素一路“倒流”回每颗泡泡的参数,轻轻拨动它们的位置、大小、颜色和透明度。这就是“可微”的含义:泡泡拼贴师每贴完一张,就知道该往哪个方向微调哪颗泡泡。

Σ′=JWΣW⊤J⊤\displaystyle \Sigma' = J W \Sigma W^{\top} J^{\top}
  1. 颜色 $c_i$ 与覆盖率 $\alpha_i$第 $i$ 颗泡泡在屏幕上该像素的颜色,以及它对这个像素的覆盖强度(高斯值 × 全局不透明度)。
    αi=αglobal,i⋅Gi(pixel)\alpha_i = \alpha_{\text{global},i} \cdot G_i(\text{pixel})
  2. 透射率 $\prod_{j=1}^{i-1}(1-\alpha_j)$前面所有更远的泡泡都没有挡住的部分,越靠前的泡泡“话语权”越大。
    Ti=∏j=1i−1(1−αj)T_i = \prod_{j=1}^{i-1}(1-\alpha_j)
  3. 求和把所有泡泡贡献叠起来,就是该像素的最终颜色;这个公式对每个像素独立成立,天然适合并行。
    C=∑i=1nciαiTiC = \sum_{i=1}^{n} c_i \alpha_i T_i

密度自适应控制

泡泡拼贴师最厉害的本事,是“越贴越准”。训练刚开始,泡泡稀疏、画面糊成一团;他会盯着拼贴结果和真实照片的差距,不断调整泡泡。但光调整还不够——有些地方泡泡太少,怎么调都盖不住细节;有些地方泡泡叠得又密又大,白白浪费材料。于是他想出了三条园艺法则。

这套策略叫密度自适应控制(adaptive density control),灵感来自“把好钢用在刀刃上”:训练时跟踪每颗泡泡位置对误差的梯度——梯度就是“这颗泡泡该往哪儿挪才能改善画面”的指示牌。梯度又大又持久,说明这里确实缺泡泡:如果泡泡本身太小,就克隆一颗一模一样的放在旁边;如果泡泡太大、糊成一大片,就沿最大尺寸方向分裂成两颗小泡泡。同时,透明度太低、几乎看不见的泡泡会被剪枝掉,定期把全体的不透明度调低逼着系统重新反思。泡泡的初始种子也不是乱撒的,而是来自摄影测量中经典的 SfM(运动恢复结构)稀疏点云——就像装修前先在地板上钉图钉定位。

凭借这套“园艺术”,高斯数量会从初始几千颗自动生长到几十万甚至上百万颗:沙发坐垫处密密麻麻,空旷天花板稀疏零落。哪里需要细节泡泡就长在哪里,这就是自适应密度控制最迷人的地方。

∥∇μiL∥2>τg⇒{si<τs:克隆 (clone)si>τs:分裂 (split)αi<τα:剪枝 (prune)\displaystyle \lVert \nabla_{\boldsymbol{\mu}_i}\mathcal{L} \rVert_2 > \tau_g\quad\Rightarrow\quad\begin{cases}s_i < \tau_s: & \text{克隆 (clone)} \\s_i > \tau_s: & \text{分裂 (split)}\end{cases}\qquad\alpha_i < \tau_\alpha: \text{剪枝 (prune)}
  1. 梯度阈值 $\tau_g$当某颗泡泡的位置梯度超过阈值,说明它所在的区域误差大,需要增援。
    ∥∇μiL∥2>τg\lVert \nabla_{\boldsymbol{\mu}_i}\mathcal{L} \rVert_2 > \tau_g
  2. 克隆条件泡泡尺寸过小,直接复制一颗略有偏移的同类泡泡,增强局部表达能力。
    si<τs⇒clones_i < \tau_s \Rightarrow \text{clone}
  3. 分裂条件泡泡尺寸过大,沿最大轴劈成两颗较小的泡泡,让覆盖更精细。
    si>τs⇒splits_i > \tau_s \Rightarrow \text{split}
  4. 剪枝条件不透明度低于阈值说明泡泡形同虚设,直接删除,节省显存与计算。
    αi<τα⇒remove\alpha_i < \tau_\alpha \Rightarrow \text{remove}

GPU并行渲染

泡泡再多也不怕,因为贴泡泡这件事天生适合“工厂流水线”。想象一座巨型的泡泡工厂:一千万颗泡泡同时从仓库涌出,工人们分成小组,每组负责幕布上的一小块区域(叫 tile,16×16 像素的小方块),只处理落在自己区域里的泡泡,互不抢活、互不干扰。

GPU 正是这座工厂的厂长。首先,每颗泡泡在屏幕上算出自己的覆盖范围(包围盒),把它登记到所有相交的 tile 的待处理列表中——这一步叫可见性裁剪,保证每颗泡泡只进入它确实盖住的方块。接着,每个 tile 对应一个 GPU 线程块(thread block),线程块里的成百上千个线程并行地对本地泡泡做深度排序,再逐个做 alpha 混合。由于每个 tile 里通常只有几十到几百颗泡泡,排序规模小,可以全部塞进 GPU 的高速共享内存,速度飞快。最后,整个渲染回归流水线作业:写入颜色、深度、排序、混合一气呵成。

泡泡工厂的秘诀就是“分而治之”:每个小组只盯着自己的一亩三分地,千万颗泡泡各行其道,谁也不用等谁。这正是 3DGS 能跑出几百帧每秒的底气。

T排序=O(Glog⁡GP),Gtile≪G\displaystyle T_{\text{排序}} = O\left(\frac{G \log G}{P}\right), \qquad G_{\text{tile}} \ll G
  1. 高斯总数 $G$场景中高斯泡泡的总数,百万量级,但不代表每个像素都要处理这么多。
    G≈106G \approx 10^6
  2. 并行线程数 $P$GPU 上几千到几万个并行执行单元,把排序工作均匀切分。
    P≈103∼104P \approx 10^3 \sim 10^4
  3. 对数因子 $\log G$排序的比较次数下界,GPU 并行归并排序可以把总工作量摊到 $P$ 份上。
    T排序=O(Glog⁡GP)T_{\text{排序}} = O\left(\frac{G \log G}{P}\right)
  4. 分块后 $G_{\text{tile}}$单个 tile 内的高斯数只有几十到几百,排序在共享内存中高速完成。
    Gtile≈10∼102G_{\text{tile}} \approx 10 \sim 10^2

训练与画质

泡泡拼贴师怎么知道拼得好不好?很简单——把拼出来的画面和真实照片放在一起比。像素颜色差多少、局部明暗结构像不像,两项加起来就是总损失。为了让 3DGS 在“看得准”和“结构稳”之间取得平衡,论文采用 L1 损失与结构相似度损失 D-SSIM 的加权组合:

训练过程的亮点是“快”:得益于显式高斯带来的高效梯度回传,3DGS 通常只需数分钟到几十分钟(视场景复杂度而定),而 NeRF 往往要数小时甚至数天。训练完成后,渲染速度高达 100+ FPS(1080p),画质在 PSNR、SSIM 等指标上与 NeRF 不相上下甚至更优。泡泡拼贴师“练”得又快又好,毕业后更是身轻如燕。

L=(1−λ) L1+λ LD-SSIM,λ=0.2\displaystyle \mathcal{L} = (1-\lambda)\, \mathcal{L}_1 + \lambda\, \mathcal{L}_{\text{D-SSIM}}, \qquad \lambda = 0.2
  1. L1 损失 $\mathcal{L}_1$逐像素计算渲染颜色与真实颜色的平均绝对误差,衡量“每个点亮不亮、色不色准”。
    L1=1N∑p∣C(p)−Cgt(p)∣\mathcal{L}_1 = \frac{1}{N}\sum_p \left| C(p) - C_{\text{gt}}(p) \right|
  2. D-SSIM 损失 $\mathcal{L}_{\text{D-SSIM}}$结构相似度指标,比较窗口内的亮度、对比度和结构,防止画面只亮不对。
    LD-SSIM=1−SSIM(C,Cgt)\mathcal{L}_{\text{D-SSIM}} = 1 - \text{SSIM}(C, C_{\text{gt}})
  3. 权重 $\lambda$0.2 意味着训练以 L1 为主、D-SSIM 为辅,相当于“先保证颜色准,再用结构拉一把”。
    λ=0.2\lambda = 0.2
  4. 损失度量这套损失既照顾全局色调,又约束局部纹理,让泡泡逐渐退成一张“会动的水彩画”。
    L=0.8 L1+0.2 LD-SSIM\mathcal{L} = 0.8\,\mathcal{L}_1 + 0.2\,\mathcal{L}_{\text{D-SSIM}}

应用前景与局限

泡泡拼贴师出师之后,几乎什么大场面都能接。在数字孪生领域,无人机绕着工地、园区飞一圈,拍下几百张照片,几十分钟后就能生成一个可以在手机和网页里实时漫游的三维数字副本,比传统倾斜摄影建模更快、更逼真。在VR/AR里,一场演唱会、一场球赛可以用多个相机阵列同步录制,观众戴上头显就能自由切换机位——这正是“六自由度(6DoF)视频”的终极形态。电商平台上,一双鞋、一台冰箱也能用泡泡拼贴生成 3D 展示,用户拖着屏幕就能 360 度看细节。自动驾驶仿真更需要这种“可实时回放的真实路口”,用 3DGS 重建十字路口后,算法可以反复以任意视角、任意光线训练感知模型。

不过泡泡拼贴师也有烦恼。最大的问题是“显存胃口大”:一个场景动辄百万颗高斯,每颗要存 59 个参数,内存开销不容小觑:

除此之外,无界大场景的边缘容易出现雾状伪影,动态物体需要额外引入时间轴建模,超高精度边缘依然不如传统网格直白。泡泡拼贴刚刚兴起,下一代会朝着“更省内存、能变形、能直播”的方向继续突破。

显存≈Ngaussians×59×4 bytes\displaystyle \text{显存} \approx N_{\text{gaussians}} \times 59 \times 4\ \text{bytes}
  1. 高斯数量 $N$百万量级,复杂城市级场景可能更多。
    N≈106N \approx 10^6
  2. 59 维参数位置 3 + 旋转四元数 4 + 缩放 3 + 不透明度 1 + 球谐颜色系数 48。
    D=3+4+3+1+48=59D = 3 + 4 + 3 + 1 + 48 = 59
  3. 4 字节FP32 浮点数存储,一百万颗泡泡约需 236 MB;场景越大,显存越紧张,压缩与流式加载成了热门研究方向。
    106×59×4≈2.4×108 B≈236 MB10^6 \times 59 \times 4 \approx 2.4 \times 10^8\ \text{B} \approx 236\ \text{MB}

AI 应用场景

数字孪生:城市级实时三维重建

阶段:三维场景重建(采集 → 点云 → 高斯优化 → 实时渲染)

解决的问题:传统倾斜摄影建模需要离线烘焙数天,生成的大网格模型笨重、难以实时加载;3DGS 用泡泡拼贴的方式,几十分钟完成重建,网页端即可流畅漫游。

具体案例:某智慧园区项目用无人机拍摄 800 张园区照片,云端 GPU 集群运行 3DGS 训练 40 分钟生成高斯场景,前端通过 WebGL 还原,管理人员在浏览器里秒级加载、任意拖拽视角查看管线与楼宇细节。

文娱与电商:六自由度自由视角内容

阶段:内容生成(多相机采集 → 高斯重建 → 实时互动渲染)

解决的问题:传统视频只能让观众被动观看,3DGS 把演出、球赛变成可交互的立体泡泡场景,观众可以自由移动机位。

具体案例:某电商平台用 3DGS 为 2000 个 SKU 生成 3D 互动商品页,用户拖动旋转查看鞋服细节,下单转化率提升明显;综艺节目录制现场部署 120 路相机,节目播出后用户可在 App 内自由切换观看角度。

自动驾驶仿真:真实路口的高保真渲染

阶段:仿真数据生成(实地采集 → 场景重建 → 传感器仿真)

解决的问题:仿真器需要“真实又可控”的道路场景,传统手工建模成本高、真实感差,3DGS 重建的真实路口支持任意视角渲染,让感知算法在虚拟世界反复训练。

具体案例:某自动驾驶团队用车载相机采集十字路口多圈轨迹数据,重建出包含车辆、行人的高斯场景,随后在仿真器中切换晴天/雨天光照参数,批量生成带有真实纹理的虚拟传感器数据,用于训练红绿灯识别模型。

Python 演示

python95 行
"""mini 3D Gaussian Splatting: 可微光栅化 + 密度控制的最小实现用 2D 高斯泡泡拟合一张目标图像(发光圆盘)."""import numpy as np H = W = 48yy, xx = np.mgrid[0:H, 0:W] / Htarget = np.clip(1 - 6 * ((xx - 0.55) ** 2 + (yy - 0.5) ** 2), 0, 1)target = np.stack([target, target * 0.6, target * 0.2], axis=-1) rng = np.random.default_rng(0)mean = rng.uniform(0.1, 0.9, (120, 2))color = rng.uniform(0.3, 0.7, (120, 3))opacity = np.full(120, 0.3)scale = np.full(120, 0.15) def render():    img = np.zeros((H, W, 3))    trans = np.ones((H, W, 1))    for i in np.argsort(mean[:, 0]):        dx = xx - mean[i][0]        dy = yy - mean[i][1]        r2 = (dx * dx + dy * dy) / (scale[i] ** 2 + 1e-6)        alpha = opacity[i] * np.exp(-0.5 * r2)[..., None]        img += trans * alpha * color[i]        trans *= 1 - alpha    return img def densify(err_map):    global mean, color, opacity, scale    nm, nc, no, ns = [], [], [], []    for i in range(len(mean)):        if opacity[i] < 0.05:            continue        px = int(np.clip(mean[i][0] * W, 0, W - 1))        py = int(np.clip(mean[i][1] * H, 0, H - 1))        if scale[i] > 0.08 and err_map[py, px] > 0.015:            for d in (0.02, -0.02):                nm.append(mean[i] + (0, d))                nc.append(color[i])                no.append(opacity[i] * 0.7)                ns.append(scale[i] * 0.6)        else:            nm.append(mean[i])            nc.append(color[i])            no.append(opacity[i])            ns.append(scale[i])    mean = np.array(nm)    color = np.array(nc)    opacity = np.array(no)    scale = np.array(ns) def train(steps=300, lr=1.5):    for it in range(steps):        order = np.argsort(mean[:, 0])        alphas, ts = [], []        img = np.zeros((H, W, 3))        trans = np.ones((H, W, 1))        for i in order:            dx = xx - mean[i][0]            dy = yy - mean[i][1]            r2 = (dx * dx + dy * dy) / (scale[i] ** 2 + 1e-6)            alpha = opacity[i] * np.exp(-0.5 * r2)[..., None]            alphas.append(alpha)            ts.append(trans)            img += trans * alpha * color[i]            trans *= 1 - alpha        err = img - target        if it % 75 == 0:            print(it, round(float(np.mean(err ** 2)), 5))        R = np.zeros((H, W, 3))        m = len(order)        for k in range(m - 1, -1, -1):            i = order[k]            alpha = alphas[k]            if k < m - 1:                R = alphas[k + 1] * color[order[k + 1]] + (1 - alphas[k + 1]) * R            color[i] -= lr * np.mean(2 * err * ts[k] * alpha, axis=(0, 1))            dLda = np.sum(2 * err * ts[k] * (color[i] - R), axis=2)            s2 = scale[i] ** 2 + 1e-6            gx = np.mean(dLda * alpha[..., 0] * (xx - mean[i][0]) / s2)            gy = np.mean(dLda * alpha[..., 0] * (yy - mean[i][1]) / s2)            mean[i][0] -= lr * 0.02 * gx            mean[i][1] -= lr * 0.02 * gy        if it > 0 and it % 75 == 0:            densify(np.mean((render() - target) ** 2, axis=2)) if __name__ == "__main__":    print("初始泡泡数:", len(mean))    train()    img = render()    print("最终泡泡数:", len(mean))    print("左半屏亮度 目标/结果:", round(float(target[:, :16].mean()), 3),          round(float(img[:, :16].mean()), 3))    print("右半屏亮度 目标/结果:", round(float(target[:, 32:].mean()), 3),          round(float(img[:, 32:].mean()), 3))
目的:一个 60 行的迷你版 3DGS——把 120 颗高斯泡泡训练成一张发光圆盘,完整演示“深度排序 + alpha 混合 + 梯度回传 + 密度控制”四大核心。
  1. order = np.argsort(mean[:, 0])把泡泡按 x 坐标从远到近排队,远的先贴。
  2. alpha = opacity[i] * np.exp(-0.5 * r2)[..., None]高斯核让泡泡拥有柔软边缘,alpha 是它在每个像素上的覆盖强度。
  3. img += trans * alpha * color[i]逐像素 alpha 混合:透射率 × 覆盖强度 × 颜色叠进画面。
  4. trans *= 1 - alpha不断更新透射率(还能透过的光),实现正确的遮挡叠加。
  5. color[i] -= lr * np.mean(2 * err * ts[k] * alpha, axis=(0, 1))这就是“可微光栅化”:链式法则把像素误差直接变成颜色梯度。
  6. R = alphas[k + 1] * color[order[k + 1]] + (1 - alphas[k + 1]) * R从近到远反向递推“后面泡泡的贡献”,让透明度梯度也能算准。
  7. if scale[i] > 0.08 and err_map[py, px] > 0.015:自适应密度控制:泡泡太大且该区域误差大,就分裂成两颗小的。

常见误区

误区3DGS 名字里带“神经”什么的,肯定和 NeRF 一样,渲染时每帧都要过一遍神经网络,凭什么说是实时的?

正解训练阶段确实用梯度下降优化泡泡参数,但渲染阶段是纯图形学光栅化——投影、排序、alpha 混合,没有任何神经网络前向计算。就像泡泡拼贴师贴画时不需要重新请教老师,老师只在训练时给意见。

为何常见:初学者常把“训练用的网络”和“渲染用的算法”混为一谈,看到论文标题里的 Gaussian 和 NeRF 出现在同一领域就以为是一路人。

误区既然泡泡是画质的基本单位,那就无限增加高斯数量,反正 GPU 快。

正解泡泡过多会造成大量重叠、显存暴涨、背景出现闪烁碎片;3DGS 恰恰靠自适应密度控制在细节不足处增补、在冗余处剪枝,必要时还要加正则项约束。泡泡太密就像颜料挤太多,反而糊成一团。

为何常见:直觉上“更多基元 = 更精细”,却忽略了显存上限和重叠导致的边际收益递减,以及优化困难带来的伪影。

误区3DGS 能实时渲染,那训练过程肯定也是实时或者接近实时的,边拍边重建。

正解渲染(推理)可达 100+ FPS,但训练通常需要数分钟到数十分钟(高配 GPU),因为要反复前向渲染、反向求梯度、做密度控制。实时的是“贴泡泡”这个动作,不是“学会怎么贴”。

为何常见:产业里“实时”通常指推理/渲染阶段,大家容易把训练速度和推理速度混为一谈,忽略了反向传播的成本。

误区都是“神经渲染”,3DGS 应该也是把场景编码在某个黑盒神经网络权重里,看不见摸不着。

正解NeRF 是隐式表示,场景存在 MLP 权重里;3DGS 是显式表示,场景就是一组看得见的椭球参数——位置、协方差、颜色、透明度,可以像点云一样拾取、编辑、裁剪和压缩,这也是它能直接光栅化渲染的根本原因。

为何常见:两者同属 AI 渲染论文谱系,共享“照片重建场景”的目标,很容易被归为同一类技术路线。

面试题

Q1. 3DGS 渲染时的核心流程是下列哪一项?

单选题 · easy

  • A.投影高斯到屏幕并按深度合成← 正确答案
  • B.逐像素发射光线做体渲染
  • C.查询神经网络坐标映射
  • D.先重建三角网格再贴纹理

答案:A。3DGS 渲染时把高斯椭球投影到屏幕,按深度排序后逐像素 alpha 混合,所以选 A。B 是 NeRF 的逐光线体采样,C 是坐标映射网络方案,D 是传统网格管线,均与 3DGS 不符。

Q2. 每一颗 3D 高斯椭球主要包含哪些参数?

单选题 · easy

  • A.仅包含中心位置与颜色
  • B.位置、协方差、颜色、不透明度← 正确答案
  • C.网格顶点坐标与法线
  • D.整幅深度图与法线图

答案:B。3DGS 的基元是显式椭球,核心参数包括位置 μ、协方差 Σ(胖瘦朝向)、球谐颜色和不透明度 α。A 遗漏了协方差与透明度,C、D 描述的是网格和图像,与高斯表示无关。

Q3. 3DGS 相对 NeRF 实现实时渲染的关键原因是什么?

单选题 · mid

  • A.以显式高斯代替逐光线体采样← 正确答案
  • B.使用了更大的 GPU 显存
  • C.强制降低输出图像分辨率
  • D.额外训练了更小的神经网络

答案:A。3DGS 把“像素数 × 采样数”的体渲染换成“高斯数”的光栅化,每颗泡泡只投影一次,复杂度与像素数解耦。B 只是硬件堆砌,C 牺牲画质,D 与实时渲染的根因无关。

Q4. 3DGS 的自适应密度控制包含哪些操作?

多选题 · mid

  • A.克隆梯度大但尺寸过小的高斯← 正确答案
  • B.分裂梯度大且尺寸过大的高斯← 正确答案
  • C.定时剪除低透明度冗余高斯← 正确答案
  • D.无条件让每个高斯数量翻倍

答案:ABC。密度控制 = 克隆 + 分裂 + 剪枝:梯度大且尺寸小就克隆,梯度大且尺寸大就分裂,透明度低就剪枝。D 的“无条件翻倍”会导致显存爆炸和伪影,不是论文的做法。

Q5. 3DGS 渲染时对高斯进行排序的依据是什么?

单选题 · mid

  • A.按高斯到相机深度排序← 正确答案
  • B.按颜色亮度从大到小排序
  • C.按不透明度从高到低排序
  • D.按创建时间先后排序

答案:A。为正确混合遮挡关系,必须先按高斯中心到相机的深度排序,远的先画、近的后画。亮度、透明度或创建时间都与空间遮挡无关,排序错误会导致画面穿帮。

Q6. 关于 3DGS 的“可微光栅化”,下列说法正确的是?

单选题 · extreme

  • A.光栅化不可导只能靠强化学习
  • B.屏幕空间梯度可回传以优化参数← 正确答案
  • C.梯度仅能更新颜色不能更新位置
  • D.可微光栅化只支持二维场景

答案:B。3DGS 的投影与混合公式全链路可导:颜色梯度直接穿过混合公式,透明度梯度需穿透后面所有高斯的透射率,位置梯度通过高斯核导数回传。A/C/D 都假设“不可导”或“维度受限”,与论文核心机制矛盾。

Q7. 3DGS 中的高斯协方差矩阵 Σ 是如何参数化的?

单选题 · hard

  • A.缩放矩阵 S 与旋转矩阵 R← 正确答案
  • B.平移矩阵与透视投影矩阵
  • C.颜色向量与不透明度标量
  • D.纹理贴图与材质反射率

答案:A。协方差用缩放矩阵 S 和旋转矩阵 R 组合(Σ = R S Sᵀ Rᵀ),既保证矩阵半正定,又让优化始终落在合法椭球上。B 是渲染时的变换,C、D 是外观属性,与几何协方差的参数化无关。

Q8. 3DGS 的 GPU 渲染中,屏幕分块(tile)的主要目的是什么?

单选题 · hard

  • A.提升画面色彩饱和度与对比度
  • B.完全避免深度排序直接绘制
  • C.把高斯数量增加一倍提升细节
  • D.减少像素合成数并控制排序规模← 正确答案

答案:D。分块把屏幕切成 16×16 小方块,每颗高斯只进入覆盖到的块,每个线程块只处理本地高斯并做块内排序,避免全局排序和“每像素遍历所有高斯”。A、B、C 与分块的并行优化初衷无关。

Q9. 3DGS 相比 NeRF 的优势包括哪些?

多选题 · easy

  • A.训练时间远短于体渲染方案← 正确答案
  • B.渲染速度可达实时帧率← 正确答案
  • C.显式参数便于编辑与控制← 正确答案
  • D.只需要一张照片就能重建

答案:ABC。3DGS 训练只需数分钟到数十分钟,渲染上百帧每秒,且参数显式可编辑,都是实打实的优势。D 错在“一张照片”——重建需要多视角照片输入,单张图像只能依赖先验猜测。

Q10. 关于 3DGS 的应用与局限,正确的说法有哪些?

多选题 · mid

  • A.渲染实时而训练仍需数十分钟← 正确答案
  • B.适合数字孪生与 VR 实时场景← 正确答案
  • C.显存开销与高斯数量线性相关← 正确答案
  • D.已完全替代所有三维重建方法

答案:ABC。3DGS 推理实时、训练分钟级是准确认知,数字孪生与 VR/AR 正是其主战场,显存随高斯数量线性增长也符合事实。D 过于绝对——无界大场景、动态物体的处理仍是挑战,远不能“完全替代”。