1. 从“太奶教学”说起:为什么一个函数能装下三维世界
第一次看到“用一个函数装下整个三维世界”这个说法,我脑子里蹦出来的不是数学公式,而是一个特别朴素的画面:小时候过年,太奶奶坐在堂屋正中间,手里捏着一团面,捏什么像什么。你告诉她“捏个兔子”,她手指头搓两下,耳朵就出来了;你说“捏个元宝”,她掌心一压一翻,元宝就成了。她脑子里好像装着一套“从形状到实物”的映射规则,你给个指令,她就能把对应的东西“渲染”出来。
NeRF(Neural Radiance Fields,神经辐射场)干的事情,本质上和太奶奶捏面人是一个逻辑:它想学到一个函数,这个函数的输入是空间中的一个位置加一个观察方向,输出是这个位置往这个方向看过去,是什么颜色、有多亮。你把整个三维场景“塞”进这个函数里,之后无论从哪个角度去看,它都能给你“捏”出对应的画面。
这听起来有点玄乎,但它的确解决了一个非常实际的问题:传统三维重建做完之后,换一个角度去看,往往会出现空洞、拉丝、糊成一团的情况。而NeRF的思路是,我不存点云、不存网格、不存贴图,我只存一个神经网络。这个网络记住了“从任意位置、任意方向看过去应该是什么样”,所以新视角合成出来的画面,过渡自然、细节丰富,尤其是那种反光、半透明、毛发边缘的效果,比传统方法高出一大截。
这篇文章适合谁看?如果你是做三维重建、计算机视觉、图形学的开发者,想搞清楚NeRF到底怎么回事、能不能用到自己的项目里,那这篇就是写给你的。如果你只是听说过这个词,想知道它为什么突然这么火、和传统建模有什么区别,也能看懂。我会尽量少堆公式,多用“太奶奶捏面人”这种生活化的类比,把核心逻辑讲透,同时把实操中真正会踩的坑、参数怎么调、代码怎么跑,都摊开来说。
提示:NeRF不是“一键生成三维模型”的魔法按钮。它更像是一个“从多张照片里学会场景长什么样”的拟合过程,训练需要时间,数据质量决定上限。
2. NeRF到底在拟合什么:核心原理拆解
2.1 一个函数,五个输入,四个输出
NeRF的核心可以浓缩成一句话:它用一个多层感知机(MLP)来近似一个五维函数。
这个函数的输入是五个数:
- 空间位置x, y, z(三个数):表示你在三维空间中的哪个点。
- 观察方向θ, φ(两个数):表示你从哪个方向去看这个点。
输出是四个数:
- 颜色R, G, B(三个数):这个点往这个方向看过去是什么颜色。
- 体积密度σ(一个数):这个点有多“实”,是空气、烟雾,还是实心物体。
你可以把整个三维场景想象成一团巨大的、半透明的彩色雾气。每个位置都有一定的“浓度”(密度)和“颜色”。当你从某个方向看过去,光线穿过这团雾气,沿途不断累积颜色,最终落到你眼睛里,就形成了画面。NeRF要学的,就是这团雾气的分布规律。
为什么输入要加观察方向?因为很多材质是“各向异性”的。比如你拿一块金属,从正面看是亮的,从侧面看可能变暗;你拿一片树叶,逆光和顺光看到的颜色完全不一样。如果只输入位置,网络只能学到“这个点是什么颜色”,学不到“从这个方向看是什么颜色”。加上方向,网络才能表达反光、高光、半透明这些效果。
2.2 为什么不用体素或点云,非要用神经网络
这里要解释一个关键选择:为什么NeRF不直接把场景存成体素网格或者点云,非要塞进一个神经网络里?
体素网格的问题在于“分辨率爆炸”。你想表达一个1米见方的场景,如果每个方向切1000份,那就是10亿个体素。每个体素存颜色和密度,内存直接爆掉。而且体素是离散的,你放大看就是一个个小方块,边缘全是锯齿。
点云的问题是“没有表面”。点云只有点,点与点之间是空的。你从远处看还行,凑近看,点与点之间的缝隙就露出来了。而且点云很难表达半透明、体积雾这类效果。
神经网络的好处是连续且紧凑。它不存离散的格子,而是学一个连续函数。你给它任意坐标,它都能算出一个值,不管这个坐标是不是在“格子”上。而且网络参数是共享的,表达一个复杂场景可能只需要几MB的权重文件,比存体素省了成千上万倍。代价是,每次查询都要跑一遍网络,计算量大,训练慢。这就是为什么NeRF早期只能在实验室里跑,后来才有各种加速方案。
2.3 体积渲染:光线是怎么“累积”出颜色的
理解了“雾气”的比喻,体积渲染就很好懂了。假设你从相机位置发射一条光线,穿过这团雾气。光线沿途会经过很多点,每个点都有密度σ和颜色c。
- 如果某个点密度很高,说明这里是个实心物体,光线到这里基本就被挡住了,后面的点贡献很小。
- 如果密度很低,说明这里是空气,光线直接穿过去,这个点的颜色贡献也很小。
- 如果密度中等,说明是半透明材质,光线会“吸收一部分、透过一部分”,颜色是沿途累积的结果。
数学上,这个过程用积分表示:最终颜色等于沿途每个点的颜色乘以“光线到达这个点时还剩多少能量”再乘以“这个点的密度”。这个“还剩多少能量”就是透射率,随着穿过高密度区域而衰减。
实际计算时,不可能对连续光线做积分,所以NeRF采用分层采样:在光线上随机取一批点,算它们的密度和颜色,然后用离散求和近似积分。这里有个关键技巧叫“重要性采样”——先粗采样一遍,看看哪些区域密度高,再在这些区域附近细采样。这样既省算力,又不漏掉细节。
注意:采样策略直接决定训练速度和最终画质。粗采样点太少,会漏掉薄结构(比如铁丝网、树叶);细采样点太多,显存扛不住。后面讲实操时会具体说怎么调。
3. 从零理解NeRF的训练流程:数据、网络、损失
3.1 输入数据:多角度照片加相机位姿
NeRF的输入不是单张照片,而是同一个静态场景的多角度照片,加上每张照片拍摄时相机的精确位置和朝向。通常需要几十到上百张照片,覆盖场景的各个角度。
相机位姿怎么来?一般用运动恢复结构(SfM)工具从照片里自动算出来。这一步非常关键,位姿不准,后面全白搭。我试过用手机随手拍一圈,结果SfM算出来的位姿飘得厉害,训练出来的模型全是糊的。后来老老实实把相机固定在小三脚架上,每转15度拍一张,位姿就稳多了。
实操心得:拍照片时尽量保持光照一致,不要自动曝光忽明忽暗。场景里最好有一些纹理丰富的物体,纯白墙面、玻璃桌面这种“无纹理区域”会让SfM很难受。
3.2 网络结构:位置编码是灵魂
原始NeRF的MLP并不深,大概8层全连接,每层256维。但直接输入x,y,z坐标效果很差,因为神经网络天生偏向学习低频函数,你让它拟合高频细节(比如边缘、纹理),它学不动。
解决办法是位置编码:把每个坐标值映射到一组高频正弦波上。比如把x映射成[sin(2^0πx), cos(2^0πx), sin(2^1πx), cos(2^1πx), ...]。这样网络就能轻松表达高频变化。这个技巧是NeRF画质好的关键之一,没有它,输出就是一团模糊的色块。
方向向量也做类似编码,但频率低一些,因为视角相关效果通常变化更平滑。
3.3 损失函数:只比较像素颜色
训练时,从一张已知位姿的照片里随机选一批像素,沿着对应光线做体积渲染,算出预测颜色,然后和真实像素颜色算均方误差。就这么简单。
没有三维监督,没有深度图,没有语义标签。网络完全靠“从不同角度看同一个点应该颜色一致”这个约束,自己学会三维结构。这也是NeRF最让人惊讶的地方:它不需要任何三维标注,只靠二维照片和相机位姿,就能反推出三维场景。
当然,这也意味着如果某个区域在所有照片里都没被拍到,网络就学不到那里的结构,新视角看过去可能就是一团糊。所以拍摄时覆盖要尽量全。
4. 实操落地:跑通一个NeRF项目要经历哪些环节
4.1 环境准备与依赖安装
跑NeRF对显卡有一定要求。原始版本在1080Ti上训练一个场景大概要一两天,现在有各种加速版本,比如Instant-NGP、Plenoxels、TensoRF,能把时间压到几分钟甚至几十秒。我建议新手先从Instant-NGP入手,它对显存友好,速度也快。
环境方面,核心依赖是PyTorch或JAX,加上CUDA。如果你用Windows,建议直接上WSL2,省去很多编译麻烦。显卡驱动、CUDA版本、PyTorch版本三者要匹配,这是最容易卡住的地方。
# 以某加速方案为例,创建环境 conda create -n nerf python=3.9 conda activate nerf pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install tiny-cuda-nn注意:不要盲目装最新版CUDA。先查清楚你用的NeRF实现支持哪个CUDA版本,再倒推着装驱动。我见过太多人卡在“CUDA版本不匹配”上浪费一整天。
4.2 数据采集与预处理
数据采集的要点前面提过:多角度、光照稳定、纹理丰富。拍完之后,用SfM工具算出相机内参和外参。输出通常是一个transforms.json文件,里面记录了每张照片的路径、相机矩阵、焦距等信息。
预处理阶段要检查几件事:
- 照片数量是否足够(建议至少30张,复杂场景要100张以上)。
- 位姿是否合理(可以在三维可视化工具里看一眼相机轨迹,应该是平滑环绕,不能乱跳)。
- 图像分辨率是否统一(不统一的话要resize,但注意保持长宽比)。
如果SfM失败,常见原因是照片之间重叠太少、纹理太弱、或者有大量运动模糊。解决办法是重新拍,增加重叠区域,或者手动加一些标记点。
4.3 训练参数怎么调
训练NeRF有几个关键参数:
| 参数 | 作用 | 常见取值 | 调整建议 |
|---|---|---|---|
| 学习率 | 控制网络更新步长 | 5e-4 | 太大震荡,太小收敛慢 |
| 批大小 | 每次采多少条光线 | 1024~4096 | 显存够就加大,训练更稳 |
| 粗采样点数 | 每条光线粗采样数量 | 64 | 太少漏细节,太多费显存 |
| 细采样点数 | 重要性采样数量 | 128 | 通常比粗采样多一倍 |
| 位置编码频率 | 正弦波最高频率 | 2^10 | 越高细节越锐,但可能过拟合 |
| 训练步数 | 总迭代次数 | 20万~50万 | 看损失曲线,不再下降即可停 |
我个人的经验是,学习率先用5e-4跑几万步,然后降到5e-5再跑几万步,这样最后收敛得更精细。另外,如果发现输出有很多“漂浮物”(场景里不存在的絮状物),通常是采样不足或训练不够,可以增加细采样点数,或者多跑几万步。
4.4 渲染与导出
训练完之后,你可以从任意新视角渲染图像。渲染时同样要做体积渲染,但不需要计算损失,所以可以只做细采样,速度比训练快很多。
如果想导出三维模型(比如mesh),需要从密度场里提取等值面,常用Marching Cubes算法。但要注意,NeRF的密度场是“软”的,提取出来的mesh往往比较粗糙,边缘不够锐利。如果目标是高质量mesh,可能需要结合其他方法做后处理。
实操心得:渲染视频时,相机轨迹要平滑,不要突然加速或转向。我一般用贝塞尔曲线生成轨迹,然后逐帧渲染,最后用ffmpeg合成视频。帧率建议30fps,每帧渲染时间控制在几秒以内,否则一个10秒视频要渲染好几个小时。
5. 常见问题与排查技巧实录
5.1 训练出来全是糊的,怎么办
这是最常见的问题。排查顺序如下:
- 检查相机位姿:把相机轨迹可视化出来,看看是不是乱跳。如果位姿不准,后面怎么调都没用。
- 检查位置编码:确认代码里确实加了位置编码,且频率设置合理。有些简化实现会漏掉这一步。
- 检查学习率:学习率太大,网络会震荡;太小,收敛太慢。可以先跑一个短训练,看损失是否稳定下降。
- 检查数据覆盖:如果某些角度没拍到,那些角度渲染出来就是糊的,这是数据问题,不是算法问题。
5.2 显存不够用怎么优化
NeRF训练很吃显存,尤其是高分辨率图像和大量采样点时。优化手段包括:
- 降低批大小(但太小会导致梯度噪声大)。
- 降低图像分辨率(训练时用半分辨率,渲染时再上采样)。
- 使用混合精度训练(fp16),能省不少显存。
- 换用加速方案,比如基于哈希网格的方法,显存占用比原始MLP小很多。
5.3 场景里有透明物体,效果很差
透明物体(玻璃、水面)是NeRF的难点,因为光线穿过透明物体后方向会改变,而原始NeRF假设光线是直线传播的。解决办法有几种:一是用更复杂的渲染模型,显式建模折射;二是用多视角一致性约束,让网络自己学;三是干脆避开透明物体,或者用不透明材质替代。
5.4 训练速度太慢,有没有加速方案
有,而且很多。原始NeRF慢是因为每条光线要跑两次网络(粗采样+细采样),每次都要查询MLP。加速思路主要有三类:
- 换表示:用体素网格、哈希网格、张量分解代替MLP,查询速度快几个数量级。
- 换采样:用更聪明的采样策略,减少查询次数。
- 换硬件:用更好的显卡,或者多卡并行。
目前社区里比较流行的加速方案,训练一个场景从几小时到几分钟不等,画质各有取舍。选哪个取决于你的需求:要极致画质就慢一点,要快速预览就快一点。
6. 我个人在实际操作中的几点体会
NeRF这个方向,从论文到落地,中间隔着一堆工程细节。我踩过最大的坑是低估了数据采集的重要性。一开始觉得随便拍几张就能跑,结果位姿算不准,训练出来全是鬼影。后来老老实实搭了个简易拍摄台,用固定轨道环绕拍摄,效果立刻上来了。
另一个体会是,不要迷信原始实现。原始NeRF代码是研究导向的,工程优化很少。社区里有很多改进版,在速度、显存、画质上都有提升。多看看别人的实现,能省很多时间。
最后分享一个小技巧:如果你只是想快速验证一个场景能不能用NeRF重建,可以先用低分辨率、少采样点跑一个快速版本,看看大致结构对不对。如果快速版本都糊得不行,那大概率是数据问题,不用浪费时间调参数了。等快速版本结构对了,再上高分辨率、多采样点精调,这样效率最高。
这个方向后续还可以往动态场景、大尺度场景、少视角重建等方向扩展,每一个方向都有不少坑等着填。但核心思路不变:用一个函数去拟合“从任意位置、任意方向看过去是什么样”,剩下的都是工程问题。