老读者应该知道我最近一直在折腾多视角三维重建这条线。手头有一组固定相机位拍的物件照片,想把它变成能直接进渲染引擎的三角网格。按常规流程走一遍之后,最大的感受是:NeRF家族在新视角合成上确实惊艳,可一旦你要的是“能拖进Blender继续编辑的干净表面”,整个过程就成了煎熬——训练几小时是家常便饭,提网格还要再卡一次阈值。后来换了一条思路,用SDFoam这种把有符号距离场放到体素网格里、再用可微光栅化出图的做法,速度和几何质量同时舒服了很多。这篇文章就把这条技术路线从原理到实操完整拆开讲。
SDFoam解决的核心矛盾其实很直接:NeRF的隐式体渲染太贵,显式SDF重建虽然几何准了,但依然没有摆脱逐点采样的老路。它换个玩法,把SDF直接存在离散体素顶点上,用Marching Cubes提出显式网格,然后走可微光栅化管线去优化几何和颜色。这样一来,训练慢和表面模糊这两个老大难问题,被同一个架构变化一起处理掉了。适合谁看?正在做多视角重建、三维资产生成、以及研究神经渲染但受够了速度和精度两头纠结的人。
1. 先别急着跑NeRF:它渲染慢、表面模糊的真实原因
NeRF最迷人的地方在于它用一个多层感知机把整个场景的辐射场装进连续函数里。输入一个三维坐标和观察方向,输出这个点的颜色和体密度。新视角合成时,每个像素沿视线发射一条光线,在光线上均匀或层级化地采样几十上百个点,把点的颜色按体渲染积分公式加权求和,得到最终像素值。这套机制在效果上确实能吊打传统图像插值和早期基于网格的方法,可它的代价也全埋在这套机制里。
第一痛点是训练速度。每个像素都要跑一条光线,每条光线动辄几十个采样点,每个采样点都要过一次神经网络前向。一张512乘512的图,大概26万个像素,每轮迭代光是采样推理就已经是天文数字。为了收敛,NeRF通常还要训到数万轮甚至更多,所以单卡训一个场景几小时属于常态。这种开销不是工程上优化就能彻底解决的,因为算法结构就要求你必须在光线方向上做大量评估。
第二痛点是表面几何的模糊性。NeRF的“表面”其实并不存在,它只有一个连续的密度场。最终要拿到网格,就得对密度场做Marching Cubes,阈值怎么选直接决定了模型是“虚胖”还是“缺肉”。密度在表面附近往往是缓慢过渡的,没有一个干净的跳变边界,所以你提出来的网格经常带一层厚壳状结构,看起来像被泡发的海绵。这在合成图像的时候问题不大,但一旦进入建模流程,这就是灾难级的脏数据。
第三痛点是几何和颜色的纠缠。NeRF里密度不仅决定几何,还通过影响最终颜色累积权重,导致它经常用“半透明的烟雾”这种手段去拟合训练视角下的颜色。结果就是从某些角度看着对,换个视角几何就露馅。很多后续工作尝试给密度加正则,比如约束密度稀疏性,效果有改善,但依然是在那个不够明确的隐式场里面打补丁。
我用一句话给NeRF家族定性:它擅长生成“看起来对的图像”,但不太擅长生成“定义了明确边界的几何”。这恰恰是后面所有改进工作的出发点。
2. SDF重建补了精度,但没补效率:这条路线卡在哪
为了把几何做扎实,学界很自然地想到了用有符号距离场替代密度场。所谓SDF,就是空间中每个点到物体表面的带符号距离,表面被定义为零等值面。基于这个思路出现了好几套神经SDF方法,它们在体渲染里用一个关于SDF的权重函数替代原来基于密度的透射率累积,让光线在穿过表面附近时对最终颜色贡献最大,远离表面时快速衰减。这样做的好处是,训练出来的隐式场有一个明确的零面,几何精度显著提升,提取出的表面干净很多。
但这些方法依然沿用“逐光线采样、逐点积分”的体渲染模式。大多数实现里,每条光线还是要分配几十个采样点,只是采样策略可以围绕SDF的零点做自适应,把采样集中在表面附近。计算量比NeRF少一些,但整体还是没有跳出“隐式场加积分”的框。一个场景从训练到可用的网格,通常依然要几十分钟到几小时量级。如果你的目标是快速迭代、批量生成资产,这个成本仍然不友好。
另外一个隐藏的坑是掩膜依赖。不少神经SDF方法把SDF和无背景建模绑定,训练时需要一个相对准确的物体前景掩膜,否则边界区域会被背景颜色污染。现实项目里,掩膜往往也是半自动抠出来的,额外引入一轮人工成本。精度提高了,流程变重了,这跟很多人想要的“拿一堆照片就直接出干净网格”的诉求是有矛盾的。
所以那条路线的贡献在于证明了“用SDF约束几何是有效的”,但工程上它仍然停留在实验室友好型流程。我自己的感受是:你花大力气调好了损失权重,得到了一个精细的表面,但中间等待和调试成本已经消耗掉了很大一部分耐心。效率问题不解决,这套东西就难真正走进生产管线。
3. SDFoam的破局关键:从“逐像素采样”切换到“可微光栅化”
SDFoam做的第一件事,就是抛弃“每条光线几百个采样点”这个昂贵积分的底层设定。它把整个场景空间切成一个个离散的立方体体素,像一块泡沫塑料,每个体素的顶点存一个SDF值。几何表面不在训练中隐式藏起来,而是每隔几步用Marching Cubes直接从体素网格里提出一个显式三角网格。这个网格就是当前模型理解的物体表面。
有了显式网格,渲染方式就完全不同了。它不再发射光线做体积累积,而是把网格投影到图像平面,用可微光栅化器生成深度图、法线图和颜色图。光栅化本来就是显卡最擅长的事情,一个标准的前向渲染只需要对每个三角形做一次投影和覆盖判断,复杂度完全不在同一个量级。前向传播的速度比我之前跑体渲染快出一个数量级,且随着训练进行,网格越贴近真实表面,这种优势越明显。
关键问题来了:网格是可微的吗?这决定了梯度能不能从颜色损失回传到SDF值。答案是,现代可微光栅化器解决了这个问题。它在光栅化阶段记录每个像素被哪些三角形覆盖,计算颜色损失时把误差按覆盖权重分配给对应的三角形顶点,顶点位置变动会改变投影覆盖和插值颜色,梯度链就此打通。顶点位置又来自Marching Cubes对体素顶点SDF值的线性插值,所以梯度最终能一路回传到SDF场里。这就是整套架构“又快又能收敛”的核心。
这样做的另一个附带好处是:几何精度天然有保障。因为损失直接定义在显式表面投影的颜色差异上,不是某个密度阈值下的模糊猜测。网格只要有一点点位置偏差,投影出来的轮廓和颜色就会和真实照片不一致,优化器会得到一个明确的修正信号。相比体渲染里那种靠积分拉均值、靠正则“劝”密度变锐利的做法,这套信号干净得多。
我当然不否定体渲染在某些情况下的灵活性,比如处理半透明物体、散射介质。但如果你做的是刚性物体、人像、雕塑这类以表面为主的重建任务,光栅化带来的好处几乎是压倒性的:快、清楚、并且产出的网格直接就是管线里能用的那个。
4. 实操视角:SDFoam在训练和网格提取中值得注意的细节
理论说得通,落到工程上还有一堆从文档里看不到的细节。我按自己跑通的流程,挑几个最影响成败的点展开。
结构选择上,体素分辨率是第一个要权衡的参数。常见的选择在256立方体左右的规模,对单个物体来说能保留足够细节,显存占用也可控。如果上来就堆512,显卡直接吃不消,而且训练初期的梯度信号本身就是稀疏的,高分辨率很多时候只会放大噪声。更合理的做法是先用较低分辨率跑出一个大概轮廓,再在表面附近加密体素,或者分块训练。这个由粗到细的思路,和从粗点云逐步上采样的传统重建流派是同一个哲学。
特征表示方面,单纯在顶点存一个SDF标量是不够的,颜色拟合会非常吃力。同一个物体表面可能有复杂纹理,而SDF只给你距离值,不给外观信息。常见的做法是每个顶点额外存一个特征向量,比如16到32维,渲染时对特征做插值再经过一个小型MLP解码成RGB。这个小MLP比起NeRF里那个庞大的坐标MLP轻量太多,因为它不需要再承担建模几何的重任。这样设计之后,兴趣点在于:几何由SDF体素直接决定,外观由特征加解码器决定,两者解耦,优化起来互相干扰也小。
损失函数是另一个容易被忽视的环节。基础配置包括颜色损失,用于对比渲染图和真实图;一个eikonal正则,约束SDF梯度的模长为1,保证距离场在数学上行为正常;如果不幸需要处理背景,还可以加一个稀疏正则或掩膜损失来压低背景区域的SDF值。我调参时发现,颜色损失的权重不能一次给太大,否则优化器会只顾着改变特征去凑颜色,把几何梯度淹没了。比较好的节奏是先让SDF场把形状定住,再逐步加大颜色的比重去补纹理细节。
还有一个很实际的坑:每次迭代都重新做一次Marching Cubes会非常慢,但间隔太远又会用陈旧的网格做渲染,优化信号滞后。我这边用了一个中间策略:每训练固定步数才重新提取一次网格,其余步数沿用当前网格,只更新顶点SDF值。这样一来,训练初期轮廓变化快时网格更新频繁,后期表面稳定后更新频率自然下调,速度和稳定性都能兼顾。
下面是一段简化版的损失函数组织方式,用作参考:
# sdf_values: (V,) 体素顶点SDF值 # mesh: marching_cubes(sdf_values, grid_resolution) # rendered_color, rendered_mask = rasterize(mesh, camera_params, features, mlp) loss = lambda_color * L1_loss(rendered_color, gt_color) # eikonal正则:梯度模长趋近1 grad_sdf = compute_gradient(sdf_values) # 数值差分或自动微分 eikonal = ((grad_sdf.norm(dim=-1) - 1.0) ** 2).mean() loss = loss + lambda_eikonal * eikonal # 若有无背景/掩膜条件 if gt_mask is not None: mask_loss = BCE(rendered_mask, gt_mask) loss = loss + lambda_mask * mask_loss loss.backward()训练期间另一个常见现象是边缘锯齿。可微光栅化虽然比体渲染快,但没有内置反走样的话,渲染图边缘会有明显锯齿,这些高频误差会变成梯度噪声,让表面出现细小波浪。我的解决办法是渲染时开多采样反走样,或者训练时对图像做一次轻微模糊,等到最终评估精度时再用完整分辨率和严格的边缘算法去测,两边各取所需。
5. 实测对比和选型建议:什么时候改用它,什么时候它还不太行
跑完几个场景之后,我做了个很主观但对选型有参考价值的对比,把NeRF、神经SDF和SDFoam放在同一张表里看:
| 维度 | 传统NeRF | 神经SDF(体渲染类) | SDFoam路线 |
|---|---|---|---|
| 渲染管线 | 逐光线体渲染 | 逐光线体渲染 | 显式网格可微光栅化 |
| 表面定义 | 密度阈值 | SDF零等值面 | SDF零等值面 |
| 网格提取 | 阈值敏感、易虚胖 | 相对干净 | 天然显式、干净 |
| 训练速度 | 慢,通常数小时 | 中等,仍需数小时 | 快,常用场景分钟到几十分钟 |
| 新视角渲染 | 慢 | 慢 | 快,接近实时 |
| 对掩膜依赖 | 通常不必要 | 部分方法强依赖 | 视背景处理而定 |
| 几何精度 | 一般 | 高 | 高,且更稳定 |
这个表格是我自己跑下来最直观的结论。SDFoam最大的优势领域是单物体、固定相机、目标是干净网格的场景。比如某个Demo项目要做一个展品的数字化,一组照片进去,我一边调参一边等,基本一顿饭的时间就能拿到可以继续雕刻的表面。这种体验对效率和心情的影响都非常可观。
不过它也有明确的边界,这一点我必须说清楚。因为体素网格的空间范围是预设的,处理无界场景或超大场景时比较麻烦。空间边界画小了会裁掉物体,画大了内存和显存又报警。所以室内大场景、开放街景这类任务,它现在不太合适,那些场景还是老老实实走基于九类大场景设计的方案更稳。
另外,如果你追求的是“看新视角时画面非常连续流畅”的纯粹视觉体验,而不是几何资产,SDFoam不一定比NeRF更惊艳。光栅化渲染出来的图像再真实,它的表现力还是受限于网格分辨率。网格够细的时候完全不输,但网格不够细的地方,体渲染的“模糊美”反而会掩盖误差。
还有一点个人经验:初始化对SDFoam的影响很大。体素SDF的初始值如果全设成大正数,表面一开始就不存在,Marching Cubes提取不到网格,整个训练直接空转。我的做法是给空间中心附近一个半径内设置负值,让初始就存在一个球形种子面,再接下去逐步优化。这个细节看起来很基础,但没注意的人会卡很久。
如果你现在手里正好有一组固定位拍摄的物体照片,想快速又干净地拿到一个三角网格,我建议直接试这条路线。先把栅格分辨率设低,确认整个训练闭环能跑通,再逐级提高细节。别一上来就追求最高精度,先让流程跑起来比什么都重要。这套架构用下来的最终体验是:几何重建本来就不该让渲染的代价拖后腿,显式表面、可微光栅化才是更接近工程直觉的方向。