☰
3DGS渲染表达器拆解:从高斯投影到SLAM实时扩展
2026/10/15 7:05:14 网站建设 项目流程

大概一年前,我第一次在消费级显卡上跑通3D Gaussian Splatting(后面统一叫3DGS)的官方训练流程和SIBR实时查看器。盯着那个能360度转动的重建场景,脑子里只有一个词:快。训练几百步就能看出轮廓,渲染时画面几乎不费劲,和我之前在NeRF上反复等推理的体验完全是两个世界。但“快”只是表象,真正让3DGS从一堆新视角合成方法里杀出来的,是那套经常被当成黑盒的“渲染表达器”。

这个系列前面几篇聊过基本概念、训练流程和数据准备,这篇我打算单独把渲染表达器拎出来讲透。它不是一个普通渲染器,而是把场景表达、投影变换、光栅化、可微回传、自适应密度控制全部耦合在一起的完整系统。理解它之后,你才能回答三个实际问题:为什么3DGS能跑这么快?为什么训练时场景会出现“毛茸茸”或“飘雾”的效果?为什么有人能把3DGS塞进SLAM实时管线当后端地图用?这篇按数学骨架、光栅化流程、密度控制、工程落地和实时系统扩展五条线展开,顺便把我自己在Ubuntu 20.04上编译、自制数据集、踩坑调参的经验也放进来,希望能帮你从“跑通”变成“会用”。

1. 为什么说3DGS的“渲染表达器”是整套系统的灵魂

1.1 从NeRF到3DGS的范式切换

NeRF把场景编码进MLP的权重里,渲染时每个像素沿光线投射方向采样几十上百个空间点,逐个丢进网络推理颜色和密度,再按积分式做体渲染。这个思路数学上很漂亮,但代价是要沿光线做密集采样,同时要反复forward一个不浅的网络,渲染速度天然被卡死。

3DGS换了个完全不同的赛道:不搞连续场了,直接把场景表示成一大堆显式的三维高斯点云。每个高斯带自己的位置、尺度、方向、颜色和不透明度。渲染时要做的事情很简单:先把这些高斯从三维投影到二维屏幕,再按离相机远近距离从近到远“贴”到像素上。整个过程更像传统图形学里的光栅化,而不是光线追踪。

为什么这个范式切换意义重大?我常跟朋友打一个比方:NeRF像在浓雾里用手电筒找东西,每个像素都得把整条光路走完才知道后面有什么;3DGS像往玻璃板上撒了一大把彩色糖豆,只需要按距离顺序把它们排好贴上去。前者每一步都要重算,后者每个点只算一次投影,再多点也是并行铺开。实测在RTX级别显卡上,3DGS推理1080p画面能跑到30fps以上,NeRF系通常还在每秒几帧的区间挣扎。这个数量级的差距,不是靠优化代码能追回来的,是表达方式本身的红利。

1.2 渲染表达器的模块边界

把“渲染表达器”拆开看,它实际包含六个紧密咬合的模块:

  • 高斯参数管理:位置、旋转四元数、缩放、球谐系数、不透明度。
  • 三维到二维的投影变换,以及对应的雅可比近似。
  • 基于Tile的并行光栅化与局部深度排序。
  • 逐像素的alpha混合合成。
  • 从渲染误差反向传播到每个高斯参数的梯度通道。
  • 自适应密度控制,用回传的梯度决定何时克隆、分裂、剔除。

这六个模块缺了谁,3DGS都不成立。没有密度控制,初始点云数量就锁死了表达上限;没有可微投影,训练时梯度落不到高斯参数上;没有Tile光栅化,几十万高斯的逐像素遍历在GPU上也跑不出实时。所以看3DGS,别只盯着“训练快”这个结果,要去理解这套闭环是怎么设计的。后面所有章节,本质上就是在给这六个模块逐个做解剖。

2. 渲染表达器的数学骨架:三维高斯到底在表达什么

2.1 高斯参数组的物理含义

先看一张参数表,这是渲染表达器的“筹码”:

参数符号维度物理含义
中心位置μ3维高斯中心的三维坐标,初始值来自稀疏点云
旋转矩阵R4维四元数高斯椭球在空间中的朝向
缩放向量s3维三个主轴上的尺度,决定椭球各向异性大小
协方差矩阵Σ = R S Sᵀ Rᵀ3×3综合旋转与缩放,构成各向异性高斯椭球
球谐系数SH3阶共16个描述视角相关颜色
不透明度α1维该高斯对光线的阻隔程度

优化的时候,Adam更新的是这些原始参数,而不是直接改协方差矩阵。直接优化Σ有个大坑:任意优化出来的3×3矩阵不一定满足半正定,一旦出现负特征值,渲染时就变成“负宽度的椭球”,这是物理上不存在的形状。用R和s参数化之后,Σ = R S Sᵀ Rᵀ自动保持半正定,而且旋转用单位四元数约束也比较稳定。这是我个人觉得原论文在工程细节上最值得学的一笔。

2.2 三维到二维的投影变换

把三维高斯投到图像平面,第一步是视图变换,把世界坐标转到相机坐标系,乘一个W矩阵就行。但第二步透视投影是除以z的非线性操作,高斯经过透视投影后不再是严格的高斯分布。原论文的处理非常工程化:在像素尺度上,用雅可比矩阵J把透视投影局部线性化。

完整公式长这样:

Σ′ = J W Σ Wᵀ Jᵀ

J是投影函数对三维坐标的偏导数矩阵,本质上是“这个高斯的中心投影到屏幕后,附近一点的位置被放缩拉偏了多少”。因为单个高斯在屏幕上的跨度通常只有几个到几十个像素,局部仿射近似在这个尺度上完全够用。这也是3DGS能把splatting当成前向渲染来做、而不是像NeRF那样逐光线重积分的关键。

代码层面还会有一次视锥剔除:高斯中心在视锥外且远离范围就直接跳过,省掉大量无用投影计算。这一步在室外大场景尤其重要,场景里可能上百万个高斯,真正在一帧里可见的往往只有一小半。

2.3 像素颜色怎么算:alpha混合与有效不透明度

当所有高斯被投影到2D后,一个像素可能同时被几十甚至上百个高斯覆盖。渲染方程是一个标准的从前往后over合成:

C = Σ cᵢ · α_effᵢ · Tᵢ Tᵢ = Π(1 - α_effⱼ),j从1到i-1

其中cᵢ是该高斯在视线方向的球谐颜色,Tᵢ是排在当前高斯前面的所有高斯没挡住目标像素的累计透光率。

有一个细节特别容易踩坑:公式里的α不是高斯自带的那个不透明度,而是“有效不透明度”。每个高斯在具体像素处的实际遮挡能力,还要乘一个二维高斯衰减值,这个衰减值来自像素坐标到高斯中心投影点的马氏距离对应的指数函数。我早期写简化版渲染器时忽略过这步,结果画面边缘全是硬边,像贴纸贴在背景上。正确做法是先算二维高斯概率密度g(x) = exp(-0.5(x-μ′)ᵀ Σ′⁻¹ (x-μ′)),再拿g(x)乘α作为最终混合权重。

3. 光栅化管线:一堆三维高斯如何变成一张高清图

3.1 Tile切分与并行分发

如果每个像素都把全部高斯轮一遍,几十万个高斯乘一百多万像素,计算量直接爆炸。原论文用的是基于Tile的光栅化:先把屏幕切成16×16像素的小块,对每个高斯计算它投影后的二维包围盒,再找到包围盒覆盖的所有Tile,把高斯“分发”给这些Tile。分发不是嵌套循环,而是把“高斯落在哪些Tile”的关系整理成排序键,做一遍并行排序,再用前缀和把数据压平到连续内存。

这步很像图形学里的可见性排序,但做得很轻量。16×16这个尺寸基本是经验值:太小会增大分发和排序的开销,太大则单个Tile内的高斯太多,逐像素遍历变慢。我自己调过32×32的版本,帧率并没有明显提升,显存占用反而涨了一截,最后又改回16×16。

3.2 局部深度排序的取舍

每个Tile内部维护一个高斯列表,渲染时按中心点的深度从近到远排序,然后做alpha混合。和NeRF那种全局光线采样不同,这里的排序是局部的,不要求整帧画面深度一致。好处是并行友好,每个Tile独立处理,GPU大规模并行很舒服;代价是Tile边界可能出现深度顺序不一致的微观瑕疵。

原论文用了一个比较实用的缓解手段:投影后的包围盒稍微往外扩一点,宁可在边界多算几个高斯,也不要漏掉本应参与混合的点。实际渲染中这些瑕疵基本分布在物体边缘,静止看几乎察觉不到,但用SIBR高速转动视角时偶尔能看到边缘闪烁。知道成因之后就不会一上来怀疑代码写错,而是去查包围盒扩展系数和排序策略。

3.3 提前终止、反走样与显存细节

处理像素时,渲染器会实时维护一个累计透光率T。当T衰减到某个极小阈值(比如1e-4)以下时,后面还没处理的高斯即使有颜色贡献,对像素的影响也不超过万分之一,这时可以直接跳出循环。这个提前终止对帧率提升帮助巨大,毕竟大部分像素真正有视觉贡献的高斯只有几十个。

反走样是另一个隐蔽的坑。基础版实现在每个像素上做单点高斯值采样,没有真正去积分像素覆盖面积,所以高光边缘偶尔会出现锯齿。很多复现项目靠分辨率投机:多渲染一档分辨率再降采样加锐化,也算性价比方案。显存方面,单个高斯列表排序在极端情况下会让中间数据结构膨胀好几个GB,点云数量超过五百万之后,16GB显卡会比较紧张。后面我讲工程落地时会给出一些削减方式。

4. 自适应密度控制:渲染表达器的“自我修复”机制

4.1 欠重建和过重建:两个完全相反的问题

高斯数量不是越多越好。场景某个区域如果初始点云稀疏,单个高斯覆盖面积太大,渲染出来就是一片“毛玻璃”;反过来如果高斯堆在某处且尺寸过小,又会留下空洞。这两个问题分别对应欠重建和过重建,处理手段完全不同。

原论文把判断依据放在位置梯度上:每个高斯在优化过程中会累加投影后的位置梯度响应。如果某处经常产生大梯度且方向稳定,说明这里表达不够,需要增加高斯。具体做法是:欠重建时克隆一个一模一样的高斯,顺着梯度方向推出去;过重建时把一个大高斯分裂成两个,各自向两侧偏移一点距离,同时把缩放尺度降下来。

4.2 密度化与梯度的判断逻辑

实现上的关键参数是view-space position gradient阈值,默认0.0002。每100次迭代做一次全量盘点,统计每个高斯在最近100步内的累积梯度,超过阈值才触发克隆或分裂。克隆适合“这里有东西但覆盖不足”的情况,复制一个再挪一点,等于在原位置附近多铺了一层表达;分裂适合“这个高斯太大,包住太多本不该归它的区域”的情况,拆成两个细节更高的小椭球。

为什么分裂时缩放系数除以1.6而不是2?这是论文里反复调出来的经验值。除2会让两个子高斯离得太开,中间出现明显缝隙,后续训练又得慢慢补;除太小则两个子高斯高度重叠,等于没分裂。1.6这个数看起来有点玄,但确实是被大量实验验证过的稳定取值。

4.3 不透明度重置与剔除

训练过程中还会定期把所有高斯的α重置到接近0的数值,原论文实现是每隔3000轮左右做一次,然后让优化器从头把它们拉起来。这么做本质上是“扫地”:很多高斯在训练后期变成半透明状态,既挡不住光也不贡献清晰细节,留在场景里只会拖慢渲染、浪费显存。重置之后,没用的点自然沉底,真正有用的点会重新获得不透明度。

剔除规则也很简单:α小于某个极小值的高斯直接删掉;缩放尺度超出合理范围的高斯也要处理掉,避免远处飘着“飞絮”。我跑过几次长时间训练,如果不做重置和剔除,最终点数可能膨胀到初始的8到10倍,渲染速度肉眼可见地往下掉。反过来把这两个机制调激进一点,画面更干净,训练和渲染都会轻快不少。

5. Ubuntu 20.04落地:编译、数据集与训练收敛的实战经验

5.1 依赖环境与扩展编译的坑

很多朋友在Ubuntu 20.04上卡住,不是因为3DGS训练代码本身复杂,而是编译扩展容易翻车。官方仓库依赖几个需要自编译的CUDA扩展:diff-gaussian-rasterization和simple-knn。我遇到过的几类问题整理成表:

问题表现常见原因解决办法
编译时 undefined reference to cublasCUDA Toolkit与PyTorch的CUDA版本不一致统一用同一套CUDA版本,比如PyTorch 2.0.0 + cu118
CUDA与g++版本不匹配CUDA 11.x不支持过新的g++降级到gcc/g++ 9,或直接上CUDA 12.x后换环境
找不到CUDA_HOMEsetup.py没识别默认路径export CUDA_HOME=/usr/local/cuda-11.8
Python版本太新官方脚本在3.10+上兼容性一般用conda建Python 3.8或3.9环境

我自己最稳的一套组合是:Ubuntu 20.04 + CUDA 11.8 + PyTorch 2.0.0 + Python 3.9,编译一次通过,训练多轮也没遇到玄学崩溃。如果用的Ubuntu 22.04,建议统一上CUDA 12.x系列,别混着来,混版本大概率会浪费一晚上。

5.2 自制数据集:从拍摄到COLMAP稀疏重建

3DGS训练开始时需要一份三维稀疏点云做高斯位置初始化,官方默认用COLMAP生成。自制数据集的质量直接决定最终渲染上限,比训练参数的影响还要大。

拍摄环节几条硬性建议:

  1. 围绕物体做半圆或全圆运动,轨迹尽量平滑,镜头始终对准主体。不要一直原地转圈,纯旋转会让COLMAP很难估计出正确的三角化深度。
  2. 相邻画面保持30%-60%重叠率,画面间隔别太大,抽帧宁可密一点,后期可以删。
  3. 光照稳定,避免强逆光和玻璃反光面。纯白墙面和低纹理区域是特征匹配的重灾区,拍出来经常是稀疏点云稀稀拉拉。
  4. 用视频抽帧的话,每2到4帧抽一张比较合适,兼顾数量与拍摄效率。

然后是COLMAP处理,基础流程三条命令:

colmap feature_extractor --database_path database.db --image_path images colmap exhaustive_matcher --database_path database.db colmap mapper --database_path database.db --image_path images --output_path sparse

跑完检查sparse/0下面是否生成了cameras.bin、images.bin、points3D.bin三个文件。如果点数极少,或者平均重投影误差在几个像素以上,基本可以判断数据本身有问题,先别急着调训练参数,回头补拍重来更省时间。还可以把sparse/0里的模型转成txt,看一眼拍摄轨迹,确认没有漂移或覆盖空洞。

5.3 训练收敛的判断与可视化检查清单

训练阶段的loss曲线大概长这样:头几百步L1从零点几快速下降,过一千步后进入平台期,后面是D-SSIM在慢慢磨细节。我不太看训练集视角的渲染结果自嗨,而是固定留出几个训练时没见过的视角,每隔500步渲染一次做对比。验证视角如果出现大片“雾感”或边缘光斑,多半是密度控制阈值太严导致高斯数量不够,或者球谐阶数被降太低,颜色表达不足。

官方SIBR viewer可以在训练过程中直接加载模型实时看,Windows和Linux都有,我强烈建议每隔500步刷新一次,比盯着tensorboard直观得多。16GB显存用户建议控制初始点云数量,COLMAP生成的points3D.bin如果超过几十万个点,可以在预处理阶段抽稀到10到20万,或者适当减少迭代轮数,画面差别通常很小。

6. 当渲染表达器遇上实时系统:SLAM与3DGS结合的思路

6.1 为什么SLAM社区盯上了高斯表达

SLAM系统真正缺的不是前端特征,而是后端地图表达。老方案里点云地图稀疏但速度快,稠密重建精度高但计算重,神经隐式表达(NeRF系SLAM)效果好却跑不到实时。

3DGS恰好踩在一个很舒服的位置:表达是显式的,几何信息可以直接用;渲染是可微的,能反传光度误差更新地图和位姿;光栅化是实时的,能在线做新视角合成供前端使用。所以这两年SLAM与GS结合的项目层出不穷,本质上都是把渲染表达器当作一个“可微几何+渲染”插件嵌进原有框架。

6.2 渲染表达器在SLAM里的角色

最常见的用法是把渲染表达器当做一个可微的重投影模块。每一帧新的观测进来,前端先做初步位姿估计,渲染表达器把当前地图渲染成该视角的预测帧,然后计算预测帧与真实帧的光度残差。残差对地图参数求导就是建图更新,对位姿求导就是位姿优化,一次反向传播同时解决两个问题。这和传统bundle adjustment里对齐3D点与2D观测的思路一致,只是把“特征匹配”换成了“像素级光度匹配”,信息量更大。

增量建图也依赖密度控制:新关键帧覆盖的区域如果渲染误差大,梯度会触发克隆或分裂,局部高斯数量上升,新场景就长进了地图。不过高斯没有拓扑结构,无法自己判断哪里已经建过图,这需要SLAM前端的关键帧选取和区域划分来引导。不少工程实现会把地图切成子图块,每个子图独立训练与更新,闭环时再做子图之间的对齐。

6.3 全局一致性、内存与实时性的现实挑战

理想很丰满,落地有几个硬约束。首先是内存带宽:一百万高斯,每个高斯光参数就要存几十个float,加上渲染中间结果,消费级显卡很难支撑大面积场景的实时在线优化。其次是全局一致性:纯GS地图没有回环约束,长期运行会跟着位姿漂移一起歪掉,必须靠位姿图优化把漂移“掰”回来,再变换高斯位置,这部分目前还没有特别优雅的统一方案。

我自己做过小规模实验,结论是:单场景十几秒的扫视,GS-SLAM效果非常惊艳,新视角几乎看不出重建痕迹;但一旦运动范围扩大、光照变化频繁,密度控制带来的点云膨胀会让显存迅速见顶。比较现实的折中思路是金字塔管理:近处用完整渲染表达器,远处退化成特征点云;闭环触发后再把远端子图合并进GS。这也是我目前觉得最有落地希望的方向。

把渲染表达器这几块拆开之后,你会发现3DGS其实没有太多玄学,每一处速度优势、每个训练技巧,最后都能追溯到投影数学、光栅化策略和密度控制这三个源头。我在实际项目里最大的体会是:别急着魔改训练超参,先花一个下午把CUDA光栅化里的排序和混合逻辑读明白,后面做数据集、接SLAM、做轻量化都会顺手很多。这个系列接下来我会换个角度,聊聊怎么把渲染表达器改造成更适合移动端和大场景流式处理的形态,到时候再继续分享实测数据。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询