想象你深夜走进一间空旷的美术馆,展厅中央静静立着一尊雕塑,四周一片漆黑,你手里只有一支探照灯。你绕着雕塑缓缓走动,每到一处便举起灯照一下——背面的阴影、侧面的轮廓、正面的高光,每一次都只能看到一帧侧影。你拍下十几张这样的照片,然后所有的灯全部熄灭。这时,你能否仅凭脑海中这些零散的侧影,画出从任何一个新角度望过去“应该”看到的样子? NeRF(神经辐射场)就是为回答这个问题而生的“空间记忆师”。它从多张不同角度的照片出发,学会一个极其朴素却强大的规律:房间里的每一个立体小点,从哪个方向看是什么颜色、它有多“实心”、能否挡住光线。一旦学会,无论你站在哪里举起探照灯,它都能在脑海里“算”出那张照片,甚至能算出你从未真实拍过的角度。这套技术正在悄悄重塑三维视觉——从电影特效的虚拟场景,到自动驾驶的街景重建,再到数字人的可交互形象,背后都有它的身影。这篇文章,我们就用这支探照灯,把NeRF从里到外照个通透。

岗位关联

对AI从业者而言,读懂NeRF意味着掌握“隐式神经表示 + 可微渲染”这一新范式,无论是做图形学研究、三维视觉落地,还是突破传统渲染管线瓶颈,它都是绕不开的基石。

目录
  1. 可视化演示
  2. 光线采样:探照灯的扫描路径
  3. 位置编码:给每个坐标盖邮戳
  4. 体渲染积分:彩色玻璃层层叠
  5. 网络结构:记录空间的小本本
  6. 分层采样:先看轮廓再看细节
  7. 损失设计:照片对照着改
  8. 与传统渲染对比:手绘画师与写生画家
  9. 工程实现:让NeRF走出实验室
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

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

NeRF整体架构:相机\u2192射线\u2192MLP\u2192体渲染 CameraImage PlaneMLP F\u03b8Ray r(t)=o+tdCamera

光线采样:探照灯的扫描路径

你举着探照灯检查这间黑暗的美术馆,最笨也最可靠的办法,是让灯光沿一条直线扫过去,然后在这条直线上一个点一个点地确认:“这里有没有东西?这里是什么颜色?”NeRF 的第一步正是如此——它把每个像素想象成一支探照灯,从相机位置发射出一条光线,然后沿着这条光线取出一串三维检查点,挨个询问场景的信息。这些检查点,就是“采样点”。

这些光线在数学上是一条射线。相机位置记作起点 \(\mathbf{o}\),像素指向场景的方向记作 \(\mathbf{d}\),而 \(t\) 则是你沿着光线走出去的距离,像一把从零开始丈量的尺子。美术馆的墙不会无限远,所以我们要把检查范围限制在“近平面”和“远平面”之间,只扫描房间里真正可能有雕塑的那一段空间。

在原始实现中,每条像素光线会在近远平面之间均匀地取 64 个点,并且每个点还会附带上一个微小的随机抖动。这个抖动看似不起眼,却至关重要——如果所有光线都在完全相同的位置上取样,重建出的场景就会出现像百叶窗一样的条纹伪影。采样范围的选择也需要经验:框得太窄会漏掉物体,框得太宽则会让大量算力浪费在空荡荡的空气上。

所以,采样点就是探照灯扫描路径上的一串路标。路标选得够密、够准,后续的观察与重建才能既省力又清晰。

r(t)=o+t d,t∈[tnear, tfar]\displaystyle \mathbf{r}(t) = \mathbf{o} + t\,\mathbf{d}, \qquad t \in [t_{\text{near}},\ t_{\text{far}}]
  1. 光线起点\(\mathbf{o}\) 是相机(探照灯)所在的位置,每条光线都从这一点出发。
    o=(xcam, ycam, zcam)\mathbf{o} = (x_{\text{cam}},\ y_{\text{cam}},\ z_{\text{cam}})
  2. 方向向量\(\mathbf{d}\) 表示这条光线射向场景的方向,由像素在图像上的位置决定,通常被归一化为单位长度。
    d=ppixel−o∥ppixel−o∥\mathbf{d} = \frac{\mathbf{p}_{\text{pixel}} - \mathbf{o}}{\|\mathbf{p}_{\text{pixel}} - \mathbf{o}\|}
  3. 距离参数\(t\) 是沿光线前进的路程,改变 \(t\) 就得到光线上不同的三维点。
    t=0⇒相机处,t=tfar⇒最远处t = 0 \Rightarrow \text{相机处}, \quad t = t_{\text{far}} \Rightarrow \text{最远处}
  4. 扫描范围\(t_{\text{near}}\) 与 \(t_{\text{far}}\) 圈定了采样区间,只在这个区间内均匀取点。
    ti=tnear+iN−1 (tfar−tnear)t_i = t_{\text{near}} + \frac{i}{N-1}\,(t_{\text{far}} - t_{\text{near}})

位置编码:给每个坐标盖邮戳

现在你已经在光线上布满了检查点,可还有一个尴尬的问题:如果直接把三维坐标丢给神经网络,它会“脸盲”。两个紧挨在一起的采样点,坐标数值几乎一样,网络根本分不清谁是谁,更别提表达雕塑表面那些精细的纹理了。这就像快递员送件时地址只写到“3 楼”——他分不清 302 和 303,必须把门牌号用各种格式写到快递单上,才能精确送达。

研究发现,普通的 MLP 对低频变化非常敏感,对高频细节却相当迟钝,这个现象叫“频谱偏置”。位置编码的解法很直接:在把坐标喂给网络之前,先把它映射到一个布满“正弦波”的高维空间。这相当于给每个三维坐标盖上了一枚独一无二的邮戳——不同位置的坐标经过不同频率的正弦、余弦变换后,彼此之间产生了显著的差异,网络一眼就能认出“这是 302 房间,那不是 303”。

原始论文中,位置坐标使用 10 个频率段,而观察方向只用了 4 个频率段。为什么方向要用更少的频率?因为颜色随视角的变化通常是平滑的,而空间位置上的纹理细节则剧烈得多。频率段 \(L\) 就像镜头倍数——倍数越高,能捕捉的细节越细,但也越容易把噪声一并放大。

位置编码这枚“邮戳”,让网络第一次看清了雕塑上每一道细小的刻痕。没有它,NeRF 只会画出一团模糊的色块;有了它,连石像眼角的纹路都能被复原出来。

γ(p)=(p, sin⁡(20πp), cos⁡(20πp), …, sin⁡(2Lπp), cos⁡(2Lπp))\displaystyle \gamma(p) = \Big(p,\ \sin(2^{0}\pi p),\ \cos(2^{0}\pi p),\ \dots,\ \sin(2^{L}\pi p),\ \cos(2^{L}\pi p)\Big)
  1. 原始坐标\(p\) 是采样点的某一个坐标分量,取值范围在整个场景空间内。
    p∈[xmin⁡, xmax⁡]p \in [x_{\min},\ x_{\max}]
  2. 频率倍数\(2^{k}\) 控制正弦波振动的快慢,\(k\) 越大,波形变化越剧烈。
    k=0,1,2,…,Lk = 0, 1, 2, \dots, L
  3. 正余弦投射把 \(2^{k}\pi p\) 同时送入正弦与余弦,得到一对相位不同的特征。
    sin⁡(2kπp),cos⁡(2kπp)\sin(2^{k}\pi p),\quad \cos(2^{k}\pi p)
  4. 向量拼接把所有频率下的正余弦结果连同原始坐标拼成一个大向量,作为网络的输入。
    γ(p)∈R2L+1 (单坐标)\gamma(p) \in \mathbb{R}^{2L+1} \ (\text{单坐标})

体渲染积分:彩色玻璃层层叠

你站在一座彩色玻璃窗前,窗上叠着好几层颜色深浅不一的玻璃。最前面一层是淡蓝色,后面一层是红色,再后面是绿色——你看到的最终颜色,绝不是任何单层玻璃的颜色,而是所有玻璃层层叠加、彼此遮挡之后的结果。NeRF 的“体渲染”做的正是这件叠玻璃的事:一条光线上有很多采样点,每个点都有密度和颜色,我们要把它们按顺序“叠”起来,算出最终进入眼睛的颜色。

光线每穿过一个点,都会被这个点的密度“挡住”一部分。我们把光线能活着走到某个点、还没被前面物体全部挡住的概率,称为透射率 \(T(t)\)。如果前面某个点密度极大,几乎不透明,那么后面点的颜色再鲜艳也传不出来——就像叠在最前面的黑色玻璃,直接把后面所有的颜色都遮死了。密度 \(\sigma\) 负责描述“实心程度”,颜色 \(\mathbf{c}\) 负责描述“这个点本身长什么样”。

技术实现时,连续的积分被离散化成逐点累加:每个采样点贡献“当前透射率 × 该点遮挡比例 × 该点颜色”,一路累加过去。这个累加过程必须可微,因为训练时损失函数的梯度要沿着这条颜色链一路传回网络,告诉每个采样点“你的密度该调大一点还是调小一点”。

往深里说,体渲染积分就是那句俗话的数学版本:“你看到的颜色,是前面所有玻璃叠出来的总和。”NeRF 的全部学问,最终都收敛到这个积分式上。

C(r)=∫tneartfarT(t) σ(r(t)) c(r(t)) dt,T(t)=exp⁡ ⁣(−∫tneartσ(r(s)) ds)\displaystyle C(\mathbf{r}) = \int_{t_{\text{near}}}^{t_{\text{far}}} T(t)\,\sigma(\mathbf{r}(t))\,\mathbf{c}(\mathbf{r}(t))\,dt, \qquad T(t) = \exp\!\left(-\int_{t_{\text{near}}}^{t} \sigma(\mathbf{r}(s))\,ds\right)
  1. 密度场\(\sigma(\mathbf{r}(t))\) 表示光线在位置 \(\mathbf{r}(t)\) 处遇到的密度,值越大越不透明。
    σ≥0\sigma \geq 0
  2. 颜色场\(\mathbf{c}(\mathbf{r}(t))\) 是该位置自带的 RGB 颜色。
    c∈[0,1]3\mathbf{c} \in [0,1]^3
  3. 透射率\(T(t)\) 是光线从起点走到 \(t\) 处仍未被完全遮挡的概率,由密度的累积积分经指数衰减得到。
    T(t)=exp⁡(−∫tneartσ(s) ds)T(t) = \exp(-\int_{t_{\text{near}}}^{t} \sigma(s)\,ds)
  4. 积分合成把“到达概率 × 遮挡概率 × 颜色”在整段光线上积分,就是最终像素颜色。
    C(r)=∫T⋅σ⋅c dtC(\mathbf{r}) = \int T \cdot \sigma \cdot \mathbf{c}\,dt

网络结构:记录空间的小本本

现在你已经把一条条光线变成了一个个彩色像素,但这些像素背后的“空间知识”存放在哪里?NeRF 的回答是一本“空间小本本”——一个全连接神经网络。你告诉它一个采样点的五维信息(三维位置加二维观察方向),它翻一翻小本本,吐出四个数字:一个密度值 \(\sigma\) 和三个颜色通道。这本小本本不是照片集,而是把整个美术馆的“物质分布”压缩到了网络的权重里。

为什么要五维输入而不是三维?因为颜色和观察角度有关。你从正面看雕塑,会看到高光;从侧面看,高光就消失了。密度不一样——雕塑的“实心程度”是客观的,不随你的视线变化。所以作者做了巧妙的设计:位置先单独经过 8 层全连接层,提取出密度;等到第五层之后,才把观察方向拼接进去,用于预测颜色。这样一来,密度天然地与视角无关,符合物理直觉。

网络最后一层会对输出做约束:密度经过 ReLU 激活保证非负(密度不可能为负),颜色经过 sigmoid 激活压到 0 到 1 之间(颜色亮度不能超界)。整个网络就像一个可微分的大函数,输入任意一个空间位置和观察方向,都能得到该处的密度与颜色——这就是“神经辐射场”中“场”字的含义。

有了这本小本本,探照灯不再需要真的照到雕塑——无论你提出哪个角度的查询,它都能像查字典一样,把答案精确地写给你。

FΘ(x, d)  →  (σ, c)\displaystyle F_{\Theta}(\mathbf{x},\ \mathbf{d}) \;\to\; (\sigma,\ \mathbf{c})
  1. 空间坐标\(\mathbf{x} = (x,y,z)\) 是采样点的三维坐标,经过位置编码后作为主输入。
    x∈R3\mathbf{x} \in \mathbb{R}^{3}
  2. 观察方向\(\mathbf{d}=(\theta,\phi)\) 是光线的方向,决定颜色中视角相关的高光成分。
    d∈R2\mathbf{d} \in \mathbb{R}^{2}
  3. 网络参数\(\Theta\) 是所有可学习的权重和偏置,相当于小本本上的全部笔记。
    Θ={W1,b1,W2,b2,… }\Theta = \{W_1, b_1, W_2, b_2, \dots\}
  4. 输出\(\sigma\) 是密度标量,\(\mathbf{c}\) 是 RGB 颜色向量。
    (σ, c)∈R4(\sigma,\ \mathbf{c}) \in \mathbb{R}^{4}

分层采样:先看轮廓再看细节

一支探照灯在美术馆里扫过时,聪明的观察者不会把灯光均匀地撒满整个房间。他会先快速扫一遍,找到雕塑大概在哪个区域,然后让灯光集中在那片区域,仔细端详每一个细节。NeRF 的“分层采样”策略正是这套思路,只不过把“扫一眼”和“仔细看”交给了两个不同的网络。

第一个网络叫“粗网络”,它在每条光线上均匀地取 64 个点,快速估算出场景的大致轮廓。粗网络输出的密度和颜色经过体渲染后,会留下每个采样点的权重 \(w_i\)——这个权重直观地反映了“这条光线最终看到的颜色,有多大比例是来自这个点”。把所有权重归一化,就得到一个概率分布:概率高的地方,说明雕塑的实体集中在那里。

第二个网络叫“细网络”,它按照粗网络给出的概率分布在同一条光线上再取 128 个点。注意,这次不是均匀取,而是“哪里概率高就往哪里多取”。两个网络在训练时一损俱损、一荣俱荣:粗网络负责探路,细网络负责精雕细琢,最终渲染时只用细网络的采样结果。这就像先用手掌摸一遍雕塑确定大致形状,再用指尖沿着刻痕细细描过每一道纹路。

分层采样是在用“先粗看再细看”的直觉,换取同样的采样预算下更清晰的图像。这是 NeRF 在算力受限时代能跑起来的关键一招。

w^i=Ti(1−e−σiδi)∑jTj(1−e−σjδj)\displaystyle \hat{w}_i = \frac{T_i\left(1 - e^{-\sigma_i \delta_i}\right)}{\displaystyle \sum_{j} T_j\left(1 - e^{-\sigma_j \delta_j}\right)}
  1. 采样点权重分母中的 \(T_i\) 是透射率,\(\sigma_i\) 是该点密度,\(\delta_i\) 是相邻采样点的间距。
    wi=Ti(1−e−σiδi)w_i = T_i\left(1 - e^{-\sigma_i \delta_i}\right)
  2. 归一化把所有采样点的 \(w_i\) 求和,再逐点相除,把权重变成总和为 1 的概率分布。
    w^i=wi∑jwj\hat{w}_i = \frac{w_i}{\sum_j w_j}
  3. 重采样按 \(\hat{w}_i\) 这个分布从所有采样点中进行随机抽取,概率高的点会被抽到更多次。
    细采样点∼w^i\text{细采样点} \sim \hat{w}_i
  4. 两阶段配合粗网络均匀采样,细网络按权重聚焦,完成由粗到精的递进。
    Nc=64,Nf=128N_c = 64,\qquad N_f = 128

损失设计:照片对照着改

想象一位学画的新手,他每画完一张雕塑写生,师傅都会把真实照片和他的画并排放在一起,一笔一笔地对照:“这里颜色偏深了,那里轮廓画粗了。”NeRF 的训练就是这样一个“一对一辅导”的过程——只不过“师傅”就是真实照片,“学生”就是粗网络和细网络。

损失函数选的是最朴素的 MSE(均方误差):把 NeRF 渲染出的像素颜色,与真实照片上对应像素的颜色相减、平方、再求和。这个数字刻画了“合成照片”与“真实照片”之间的差距。差距越小,说明网络对场景的理解越准确。值得注意的是,粗网络和细网络都要参与计算损失——粗网络的输出也要拿去和真实照片比较,这样它才能学会“哪里该多撒点”,为细网络铺好路。

整个损失能够工作的前提,是“渲染”这个操作本身必须可微。传统渲染管线把几何投影到屏幕后就直接丢弃了中间量,梯度无处可传;而 NeRF 的体渲染积分每一步都是平滑的加减乘除,误差信号可以沿着光线一路倒推回网络参数。这也是“可微渲染”这个新范式最迷人之处。

师徒对照式的训练看似笨拙,却极其稳定。MSE 不会给任何像素“开后门”,每一处偏色都会被忠实记录,逼着网络一点点修正自己对整个空间的认知。

L=∑r∈R∥ C^(r)−C(r) ∥22\displaystyle \mathcal{L} = \sum_{\mathbf{r} \in \mathcal{R}} \Big\|\,\hat{C}(\mathbf{r}) - C(\mathbf{r})\,\Big\|_2^2
  1. 渲染像素\(\hat{C}(\mathbf{r})\) 是 NeRF 对光线 \(\mathbf{r}\) 渲染出的预测颜色。
    C^(r)=∫T(t) σ(t) c(t) dt\hat{C}(\mathbf{r}) = \int T(t)\,\sigma(t)\,\mathbf{c}(t)\,dt
  2. 真实像素\(C(\mathbf{r})\) 是同一光线上真实照片对应的颜色,作为监督信号。
    C(r)∈[0,1]3C(\mathbf{r}) \in [0,1]^3
  3. 逐条光线误差两者相减取平方,衡量这条光线预测偏了多少。
    ∥C^−C∥22=(R^−R)2+(G^−G)2+(B^−B)2\|\hat{C} - C\|_2^2 = (\hat{R}-R)^2 + (\hat{G}-G)^2 + (\hat{B}-B)^2
  4. 汇总训练集对所有光线求和,得到整个批次的损失。
    L=∑r∥C^(r)−C(r)∥22\mathcal{L} = \sum_{\mathbf{r}} \|\hat{C}(\mathbf{r}) - C(\mathbf{r})\|_2^2

与传统渲染对比:手绘画师与写生画家

传统三维渲染像是“手工画家”:必须先请模型师用三角网格搭出雕塑的骨架,再请材质师贴上一层“皮肤”,灯光师打好每一盏灯,渲染器才能把画面画出来。任何一个环节缺失,画就画不出来——没有网格,一切免谈。而 NeRF 像一位“写生画家”:它完全不需要看网格,只凭多角度的照片,直接在脑海里构建一个连续的场景函数,然后挥毫泼墨,画出一个从未真实存在的角度。

具体到技术层面:光栅化把三角形逐个投影到屏幕上,速度快得惊人,但前提是你必须拥有完整的三角形网格;光线追踪则让光线的路径像台球一样在几何表面弹跳,效果极其逼真,但每弹跳一次都要做一次射线与三角形的求交运算。两者都是“先有几何,再谈着色”。NeRF 把几何彻底藏了起来——它不再显式存储三角形,而是用神经网络的权重隐式地编码几何与外观,体渲染积分本身就是“着色”过程。

下面这条渲染方程,是传统计算机图形学的基石:它描述了某个点发出的光等于自发光的亮度,加上所有入射光经过材质反射后贡献的总和。传统方法要解这个方程,必须已知几何法线、BRDF 材质参数、光源分布——每一个都要人手动建模。而 NeRF 的巧妙之处在于,它让网络把所有这些物理量“一锅炖”进权重里,你只管输入坐标、输出颜色。

如果说传统渲染是“有米才能下锅”的手工艺,那 NeRF 就是“看菜下饭”的写生派——素材(数据)足够多,它就能凭空复原出完整的空间味道。

Lo(x,ωo)=Le(x,ωo)+∫Ωfr(x,ωi,ωo) Li(x,ωi) (n⋅ωi) dωi\displaystyle L_o(\mathbf{x}, \omega_o) = L_e(\mathbf{x}, \omega_o) + \int_{\Omega} f_r(\mathbf{x}, \omega_i, \omega_o)\,L_i(\mathbf{x}, \omega_i)\,(\mathbf{n}\cdot\omega_i)\,d\omega_i
  1. 出射辐射\(L_o\) 是观察方向 \(\omega_o\) 上最终看到的颜色亮度。
    Lo(x,ωo)L_o(\mathbf{x}, \omega_o)
  2. 自发光项\(L_e\) 是该点自身发出的光,比如灯泡的表面。
    Le(x,ωo)L_e(\mathbf{x}, \omega_o)
  3. BRDF 反射项\(f_r\) 描述材质如何把入射光反射到出射方向,\(\mathbf{n}\cdot\omega_i\) 是光线入射角的余弦衰减。
    fr(x,ωi,ωo) Li(x,ωi) (n⋅ωi)f_r(\mathbf{x}, \omega_i, \omega_o)\,L_i(\mathbf{x}, \omega_i)\,(\mathbf{n}\cdot\omega_i)
  4. 半球积分对法线上方整个半球所有方向的入射光累加。
    ∫Ω… dωi\int_{\Omega} \dots\,d\omega_i

工程实现:让NeRF走出实验室

从论文公式到能跑的工程代码,中间还隔着许多“后勤工作”。就像探照灯在美术馆里能否照得准,不仅取决于灯本身,还取决于你站的位置、灯光的朝向、甚至电池够不够用。NeRF 落地时同样要处理好三件事:数据、训练和加速。

数据准备是第一步。NeRF 需要每个相机拍摄时的精确位置和朝向,这个信息通常不会记录在照片文件里,而是用 COLMAP 这样的传统三维重建工具从照片中估计出来。同时还要估算场景的近远平面范围——太大,采样点浪费在空处;太小,雕塑被裁掉。训练阶段的技巧同样关键:官方实现用 Adam 优化器配合学习率衰减,粗采样 64 点、细采样 128 点,批大小约 1024 条光线,通常要训练几十万步才能收敛。

对于工程落地,纯 NeRF 的慢是出了名的——训练数小时、渲染一张图也要几秒。后来的加速方案各显神通:有的提前用低分辨率粗渲染剔除空白区域的采样点;有的把位置编码换成可学习的哈希表(如 instant-NGP),训练速度提升上百倍;还有的将整个场景替换成更小更快的表示方式。理解这些加速手段之前,先吃透基础 NeRF 的每一个环节,才是捷径。

工程实现的过程,就是把探照灯从“实验室里能转”调校到“野外也能打”的过程。数据、采样、优化、加速——每一环都值得反复打磨。

θ  ←  θ−α ∇θL\displaystyle \theta \;\leftarrow\; \theta - \alpha\,\nabla_{\theta}\mathcal{L}
  1. 网络参数\(\theta\) 是所有可学习权重的集合,即“空间小本本”的全部内容。
    θ={W1,b1,W2,b2,… }\theta = \{W_1, b_1, W_2, b_2, \dots\}
  2. 损失梯度\(\nabla_{\theta}\mathcal{L}\) 是损失函数对每个参数的偏导,指出“哪个方向能让误差降得更快”。
    ∇θL=∂L∂θ\nabla_{\theta}\mathcal{L} = \frac{\partial \mathcal{L}}{\partial \theta}
  3. 学习率\(\alpha\) 是每次更新的步长,太大会震荡,太小会蜗牛爬。
    α∈(10−4, 10−3)\alpha \in (10^{-4},\ 10^{-3})
  4. 参数更新沿梯度反方向迈一步,损失就小一点,如此反复迭代。
    θnew=θold−α ∇θL\theta_{\text{new}} = \theta_{\text{old}} - \alpha\,\nabla_{\theta}\mathcal{L}

AI 应用场景

电影特效与虚拟拍摄

阶段:新视角合成(模型推理)

解决的问题:传统电影特效需要美工逐帧搭建三维场景、绑定材质灯光,成本极高;如果实拍镜头角度不够,后期合成时难以补齐。NeRF 从实拍照片中隐式学习场景,可在任意虚拟视角重新渲染,让“补拍”不再需要回到现场。

具体案例:某特效公司拍摄一段雕塑环绕镜头时,现场只采集了 20 张照片,随后用 NeRF 重建辐射场,导演在虚拟摄影棚里把机位推进到雕塑基座附近,渲染出从未真实拍摄过的低角度镜头,画面与实拍素材无缝融合。

自动驾驶的街景重建

阶段:场景隐式建模(训练 + 渲染)

解决的问题:自动驾驶需要高精度的三维街景,传统激光雷达扫描价格昂贵,且对玻璃、反光表面束手无策。NeRF 能用普通车载摄像头视频重建连续场景,对反光与半透明物体也有不错的表达能力。

具体案例:某智能驾驶团队用一辆装有 6 个环视摄像头的数据采集车,在路口绕行两圈,采集约 300 帧画面,离线训练一个小型 NeRF,重建出路口花坛、路灯和过往车辆的连续辐射场,用来补充仿真环境中难以手工搭建的复杂路况。

数字人与商品展示

阶段:神经辐射场拟合(模型训练)

解决的问题:电商平台想让用户 360° 查看商品,传统方式要搭建旋转拍摄台或用工业级扫描仪;NeRF 只需手机围绕商品拍一段视频,就能生成可交互的三维展示,成本下降一个数量级。

具体案例:一家鞋类电商发起“光影鞋展”活动,工作人员用手机在 20 秒内绕新款球鞋拍摄一周,后台训练轻量 NeRF,用户在网页上拖拽鼠标,就能从任意角度欣赏鞋面皮革在不同光照下的反光细节。

Python 演示

python110 行
"""micro-NeRF: 2D continuous scene learning via volume rendering"""import numpy as np PE_L, HID, NS, NP, EP = 4, 32, 24, 5, 300CAM = [0.0, 1.4, 2.8, 4.2]NEW = 5.6R_CT = 1.6TS = np.linspace(0.2, 2.6, NS)  def scene(p):    x, y = p[..., 0] - 0.15, p[..., 1]    r = np.sqrt((x / 0.35) ** 2 + (y / 0.20) ** 2)    dens = np.where(r < 1, 4.0 * (1 - r), 0.0)    rgb = np.broadcast_to(np.array([0.2, 0.8, 0.3]), p.shape[:-1] + (3,))    return dens, rgb  def pe(p):    enc = [p]    for k in 2.0 ** np.arange(PE_L):        enc += [np.sin(k * np.pi * p), np.cos(k * np.pi * p)]    return np.concatenate(enc, axis=-1)  def make_rays(angles, n_pix):    out = []    for a in angles:        c = np.array([R_CT * np.cos(a), R_CT * np.sin(a)])        fwd = -c / R_CT        right = np.array([-fwd[1], fwd[0]])        for i in range(n_pix):            s = (i - (n_pix - 1) / 2) / n_pix * 1.4            d = c + right * s - fwd * 0.4 - c            out.append((c, d / np.linalg.norm(d)))    return out  def render(rays, func):    dt = TS[1] - TS[0]    pix = []    for o, d in rays:        p = o[None, :] + TS[:, None] * d[None, :]        dens, col = func(p)        a = 1 - np.exp(-dens * dt)        T = np.concatenate([[1], np.cumprod(1 - a)[:-1]])        pix.append(((T * a)[:, None] * col).sum(0))    return np.array(pix)  class Net:    def __init__(s, nin):        s.w1 = np.random.randn(nin, HID) * 0.3        s.b1 = np.zeros(HID)        s.w2 = np.random.randn(HID, 4) * 0.3        s.b2 = np.zeros(4)     def fwd(s, x):        s.x = x        s.h = np.maximum(0, x @ s.w1 + s.b1)        s.raw = s.h @ s.w2 + s.b2        s.dens = np.maximum(0, s.raw[:, 0])        s.col = 1 / (1 + np.exp(-s.raw[:, 1:]))        return s.dens, s.col     def bwd(s, gd, gc, lr):        n = len(s.x)        g = np.zeros_like(s.raw)        g[:, 0] = gd * (s.raw[:, 0] > 0)        g[:, 1:] = gc * s.col * (1 - s.col)        s.w2 -= lr * s.h.T @ g / n        s.b2 -= lr * g.sum(0) / n        gh = (g @ s.w2.T) * (s.h > 0)        s.w1 -= lr * s.x.T @ gh / n        s.b1 -= lr * gh.sum(0) / n  def main():    np.random.seed(42)    rays = make_rays(CAM, NP)    gt = render(rays, scene)    gtn = render(make_rays([NEW], NP), scene)    net = Net(pe(np.zeros((1, 2))).shape[1])    dt = TS[1] - TS[0]    for ep in range(EP):        loss = 0.0        for k, (o, d) in enumerate(rays):            p = o[None, :] + TS[:, None] * d[None, :]            dens, col = net.fwd(p)            a = 1 - np.exp(-dens * dt)            T = np.concatenate([[1], np.cumprod(1 - a)[:-1]])            w = T * a            pr = (w[:, None] * col).sum(0)            gl = 2 * (pr - gt[k]) / 3            gw = col @ gl            S = 0.0            ga = np.zeros(NS)            for i in range(NS - 1, -1, -1):                ga[i] = gw[i] * T[i] - S / (1 - a[i] + 1e-8)                S += gw[i] * w[i]            net.bwd(ga * dt * (1 - a), w[:, None] * gl[None, :], 0.6)            loss += ((pr - gt[k]) ** 2).sum()        if ep % 50 == 0:            print("epoch", ep, "loss", round(loss, 5))    new = render(make_rays([NEW], NP), lambda p: net.fwd(p))    print("new view MSE:", round(float(np.mean((new - gtn) ** 2)), 6))  if __name__ == "__main__":    main()
目的:用纯 NumPy 实现一个微型 NeRF——从 4 个视角的“照片”中学习一个二维连续场景,然后在全新视角下渲染并对比误差。
  1. for k in 2.0 ** np.arange(PE_L):生成 4 个不同频率的倍数,用于构造位置编码的傅里叶特征。
  2. enc += [np.sin(k * np.pi * p), np.cos(k * np.pi * p)]把坐标映射到不同频率的正弦与余弦,实现“盖邮戳”。
  3. T = np.concatenate([[1], np.cumprod(1 - a)[:-1]])逐点计算透射率,前面点越不透明,后面点能见度越低。
  4. pix.append(((T * a)[:, None] * col).sum(0))体渲染合成:把“透射率 × 遮挡率 × 颜色”沿光线求和。
  5. gl = 2 * (pr - gt[k]) / 3对 MSE 损失求梯度,指向预测与真实差异的方向。
  6. ga[i] = gw[i] * T[i] - S / (1 - a[i] + 1e-8)体渲染反向传播:把误差从合成像素沿光线逐点回传。
  7. net.bwd(ga * dt * (1 - a), w[:, None] * gl[None, :], 0.6)把光线上每个点的梯度送进网络,更新权重。

常见误区

误区位置编码的频率段 L 设得越大,能表达的细节越多,性能一定越好,所以直接拉到最大。

正解这是一个典型的“过犹不及”。频率越高,网络越容易把训练照片中的采样噪声、光线不一致当成真实细节学习进去,导致合成的新视角出现高频噪点和闪烁。原始论文中 L=10 是反复调出来的平衡点:既能表达纹理细节,又不至于过拟合。实际使用时应该根据场景复杂度做消融实验。

为何常见:人们容易把“特征维度更大 = 容量更大 = 效果更好”的直觉套用到位置编码上,忽略了频率引入的是“高频噪声放大”的风险。

误区为了让渲染更清晰,就把每条光线的采样点从 64 加到 512,认为点越多积分越准。

正解均匀增加采样点确实能提高积分精度,但收益急剧递减,而训练时间几乎线性增长。NeRF 的真正王牌是分层采样:先用粗网络判断“哪里值得看”,再针对性加密。同样预算下,分层采样的效果远好于盲目均匀加点。如果渲染仍有雾感,优先检查的是网络是否收敛、场景范围是否合理,而不是单纯堆采样数。

为何常见:数值积分直觉告诉人们“步长越小越精确”,但忽略了计算代价和 NeRF 两阶段采样的精妙设计。

误区NeRF 的名字里带“辐射”,渲染时又发射光线,所以本质就是传统光线追踪加了个神经网络。

正解传统光线追踪的前提是场景有显式几何(三角形网格),光线在表面反射、折射,需要做射线与三角形求交。NeRF 没有显式几何,它把整个场景编码成连续函数,光线穿过的是“密度场”而不是“表面”,颜色靠体渲染积分累加。两者共享“光线”这个词,哲学却完全不同:一个是物理模拟,一个是数据拟合。

为何常见:两者都涉及“光线”“辐射”等术语,视觉流程上都有从相机发射光线的阶段,容易让人混淆。

误区NeRF 训练完了,就应该像传统游戏引擎一样 60 帧每秒跑起来。

正解标准 NeRF 即使在推理阶段,每条光线也要采样 128 个点、各自过一遍 8 层 MLP,渲染一张 800×800 的图像在 GPU 上仍需数秒。真正实现实时渲染的是后来的一系列加速工作:plenoxels 用体素网格替换 MLP,instant-NGP 用哈希编码加速特征查找;这些方案与原始 NeRF 已经有很大差距。

为何常见:人们容易把“训练完成”和“实时可用”画等号,忽略了 NeRF 的连续场查询本就昂贵,需要专用加速手段才能逼近实时。

面试题

Q1. 射线方程 r(t)=o+td 中,符号 o 表示什么?

单选题 · easy

  • A.光线方向矢量
  • B.光线起点位置← 正确答案
  • C.场景背景颜色
  • D.像素曝光时间

答案:B。o 是相机或像素光线的起点,即探照灯的位置;d 才是方向矢量。t 是沿光线的距离参数,颜色和曝光时间与射线方程无关。

Q2. NeRF 中使用位置编码的主要目的是什么?

单选题 · easy

  • A.降低网络参数量
  • B.增强高频细节表达← 正确答案
  • C.减少训练数据需求
  • D.替代体渲染积分

答案:B。MLP 有频谱偏置,倾向于拟合低频变化,位置编码先用不同频率的正弦余弦把坐标映射到高维空间,让网络能捕捉雕塑表面刻痕这类高频细节。它不改变网络参数量,也不能替代渲染积分。

Q3. 体渲染公式中 σ(t) 表示采样点的什么属性?

单选题 · easy

  • A.体积密度← 正确答案
  • B.表面法线
  • C.光源强度
  • D.纹理坐标

答案:A。σ 是体积密度,描述该点“实心程度”和挡光能力。法线、光源强度、纹理坐标都不是辐射场直接输出的量。

Q4. 粗采样网络输出的权重 w_i 在分层采样中的作用是什么?

单选题 · mid

  • A.渲染最终背景色
  • B.指导细网络重采样← 正确答案
  • C.计算正则化惩罚
  • D.替换位置编码

答案:B。粗网络输出的权重归一化后形成概率分布,细网络按这个分布集中采样,实现“先看轮廓再看细节”。权重本身不直接参与最终渲染,也不用于正则化。

Q5. NeRF 训练时采用的损失函数是?

单选题 · mid

  • A.交叉熵损失
  • B.均方误差损失← 正确答案
  • C.感知特征损失
  • D.对抗生成损失

答案:B。NeRF 用渲染像素与真实像素的 MSE 作为损失,逐像素比较颜色差异。交叉熵用于分类,感知损失依赖预训练网络,对抗损失属于 GAN 体系,都不是原版 NeRF 的选择。

Q6. 细网络的采样点主要来自哪里?

单选题 · mid

  • A.均匀随机分布
  • B.粗网络的权重分布← 正确答案
  • C.场景包围盒角落
  • D.固定的稀疏网格

答案:B。细网络按照粗网络归一化后的权重(概率分布)进行重要性采样,密度高的区域会获得更多采样点,实现算力聚焦。均匀分布是粗采样的方式,包围盒角落与稀疏网格都不是答案。

Q7. 位置编码频率 L 设置过大时,最可能出现什么问题?

单选题 · hard

  • A.过拟合高频噪声← 正确答案
  • B.训练速度大幅提升
  • C.网络不再需要激活
  • D.颜色通道自动扩展

答案:A。过高的频率会让网络把照片中的噪声、光照不一致等高频干扰当成真实细节记忆,导致渲染新视角时出现噪点和闪烁。训练速度不会变快,网络结构不会改变,颜色通道也不会因频率变化。

Q8. 与传统光栅化渲染相比,NeRF 的显著特点有哪些?

多选题 · mid

  • A.渲染过程完全可微← 正确答案
  • B.场景表示为连续场← 正确答案
  • C.无需显式三角网格← 正确答案
  • D.天然支持实时渲染

答案:ABC。NeRF 用连续辐射场替代显式几何,体渲染全程可微,让梯度能流回网络,这是它可训练的前提。但标准 NeRF 推理速度慢,离实时渲染很远,D 错误。

Q9. 下列哪些属于 NeRF 训练流程中的关键技巧?

多选题 · hard

  • A.粗网络均匀采样← 正确答案
  • B.细网络按 PDF 重采样← 正确答案
  • C.位置编码输入网络← 正确答案
  • D.直接回归像素坐标

答案:ABC。分层采样(粗均匀 + 细聚焦)和位置编码都是 NeRF 的基石。NeRF 学习的是空间坐标到密度和颜色的映射,不是直接回归像素坐标,D 错误。

Q10. 关于体渲染公式 C=∫T(t)σ(t)c(t)dt,下列说法正确的有?

多选题 · extreme

  • A.T(t) 表示透射率← 正确答案
  • B.σ(t) 表示体积密度← 正确答案
  • C.c(t) 表示该点颜色← 正确答案
  • D.T(t) 表示场景法线

答案:ABC。透射率 T(t) 是光线走到 t 处未被遮挡的概率,σ(t) 是密度,c(t) 是颜色,三者共同决定合成像素。法线在体渲染公式中并没有直接出现,D 错误。