在多视角三维重建里,很多系统最终交付的是一张带颜色的网格或点云。但这个颜色并不是材质本身,而是材质在特定光照和视角下的综合结果。如果场景里有一盏灯在金属表面留下高光,重建结果往往会把这块亮斑直接烘焙到纹理中,一旦换到新光照环境,高光不会跟着视线移动,反而变成一块发白的污渍。解决这个问题的关键是反射率分解,也就是把表面出射光拆成漫反射部分和高光部分。NeRD在这条路上引入了神经反射场的思路,让漫反射反照率与视角相关高光可以在没有显式网格的情况下被分离出来。

反射率分解为什么不能只靠颜色
从物理上看,漫反射和镜面反射遵循完全不同的规律。漫反射可以粗略理解为光进入表面浅层后发生多次散射,再以接近均匀的角度分布射出,因此从不同方向观察时颜色变化很小。它主要由表面反照率决定,也就是物体本来的颜色。镜面反射则发生在空气与材质的分界面上,入射光的一部分直接按反射方向弹出。对光滑表面,这个反射能量集中在一个很窄的角度范围内,观察者只有在特定位置才能看到高光;对粗糙表面,高光会被散射到更宽的范围,看起来更柔和。BRDF用函数描述这种方向相关行为,通常写作入射方向和出射方向的四维函数。
普通相机拍摄的图像没有直接记录哪部分能量来自漫反射,哪部分来自高光。像素值只是两者的线性叠加。若没有额外约束,仅凭几张照片做逆渲染会遇到严重的歧义:一个白色亮斑可能是浅色漫反射表面,也可能是深色光滑表面的镜面高光。过去的方法往往依赖受控光源、交叉偏振镜或者大量采样来降低歧义。NeRD的核心贡献之一,就是把这种物理先验写进网络结构,让模型在没有复杂采集设备的条件下也能完成稳定分解。
对下游应用来说,漫反射和高光分离的直接价值非常明显。重光照时漫反射项需要保留,而高光项要根据新光源位置重新计算;材质编辑时如果只改反照率而高光里仍混着旧颜色,结果就会不真实。因此分离质量直接决定三维资产能否跨光照复用。
NeRD的神经反射场结构
NeRD并没有先重建一个完整网格再做逐面片优化,而是采用类似神经辐射场的隐式场景表示。模型接收一个空间坐标和一个观察方向,先通过MLP提取该点的几何特征,再预测该点的漫反射反照率、镜面粗糙度等材质参数。法线不直接由材质网络输出,而是通过体积密度关于空间坐标的梯度计算得到。这样法线与几何保持一致性,不会出现材质网络随意输出一个法线导致高光方向错误的情况。
材质预测部分通常会把反照率限制在0到1之间,粗糙度也通过Sigmoid压缩到合理区间。镜面高光采用微表面BRDF模型计算,比如GGX分布。与直接预测颜色的NeRF变体相比,NeRD的中间表示具有明确物理含义,因此即使训练视角有限,也更容易泛化到新光照。
import torch
import torch.nn as nn
class NeRDMaterial(nn.Module):
def __init__(self, pos_dim=63, dir_dim=27, hidden=256):
super().__init__()
self.geo_net = nn.Sequential(
nn.Linear(pos_dim, hidden),
nn.ReLU(),
nn.Linear(hidden, hidden),
nn.ReLU(),
nn.Linear(hidden, hidden),
nn.ReLU(),
)
self.mat_head = nn.Sequential(
nn.Linear(hidden, hidden),
nn.ReLU(),
nn.Linear(hidden, 4),
)
self.dir_net = nn.Sequential(
nn.Linear(dir_dim + hidden, hidden),
nn.ReLU(),
nn.Linear(hidden, hidden),
)
def forward(self, pos_enc, dir_enc):
geo_feat = self.geo_net(pos_enc)
mat_raw = self.mat_head(geo_feat)
albedo = torch.sigmoid(mat_raw[:, :3])
roughness = torch.sigmoid(mat_raw[:, 3:4])
view_feat = self.dir_net(torch.cat([dir_enc, geo_feat], dim=-1))
return albedo, roughness, view_feat, geo_feat
上面的代码只是网络主干示意。NeRD在输入层使用位置编码,让MLP更容易表达高频几何和材质变化。geo_net输出的特征会同时用于体积密度预测和材质预测,促使共享表示既尊重表面位置,也尊重表面属性。dir_net则把观察方向与几何特征融合,为后续视角相关高光计算提供基础。
这个结构的关键好处是,反照率不接收观察方向输入,因此它天然被约束为视角无关量;而高光相关计算必须依赖观察方向。通过这种输入设计,网络在训练开始前就已经被诱导成正确的分解形式,后续光度损失只需要进一步细化数值。
渲染方程中的显式分离
NeRD最终的像素颜色需要经过体积渲染。沿一条光线采样若干空间点,先由密度场计算每个点的占据权重,然后对各个点的出射辐射加权求和。出射辐射由漫反射项和镜面反射项组成。漫反射项可以近似为反照率除以圆周率再乘以入射辐照度。在自然光照或简单点光源假设下,入射辐照度可以用球面谐波或者直接对光源方向积分得到。镜面反射项则使用微表面BRDF,包含法线、视角方向、光源方向和粗糙度。
下面的计算示意展示了一个点光源场景中的颜色合成。漫反射部分只与法线和光源方向有关,与观察方向无关;镜面部分同时依赖法线、光源方向和观察方向。
import torch
def safe_normalize(x, eps=1e-8):
return x / (torch.norm(x, dim=-1, keepdim=True) + eps)
def ggx_specular(normal, view_dir, light_dir, roughness):
half = safe_normalize(view_dir + light_dir)
n_dot_h = torch.clamp(torch.sum(normal * half, dim=-1, keepdim=True), min=0.0)
n_dot_v = torch.clamp(torch.sum(normal * view_dir, dim=-1, keepdim=True), min=0.0)
n_dot_l = torch.clamp(torch.sum(normal * light_dir, dim=-1, keepdim=True), min=0.0)
alpha = roughness * roughness
alpha2 = alpha * alpha
denom = n_dot_h * n_dot_h * (alpha2 - 1.0) + 1.0
d = alpha2 / (3.1415926 * denom * denom + 1e-8)
f = 0.04 + 0.96 * (1.0 - n_dot_v) ** 5
g = 0.5 / (n_dot_v * (1.0 - roughness) + roughness)
specular = d * f * g / (4.0 * n_dot_v * n_dot_l + 1e-8)
return specular
def shading(albedo, normal, view_dir, light_dir, roughness, light_intensity):
diffuse = albedo / 3.1415926
diffuse_term = diffuse * torch.clamp(torch.sum(normal * light_dir, dim=-1, keepdim=True), min=0.0)
specular_term = ggx_specular(normal, view_dir, light_dir, roughness)
color = light_intensity * (diffuse_term + specular_term)
return color
实际训练时,NeRD通常会把场景光照表示为一组可学习的球面高斯或一个环境图,而不是简单点光源。这样能处理真实拍摄中来自窗户、天空和室内灯的复杂光照。无论光照表示如何变化,漫反射与高光的分离逻辑保持一致:漫反射分支对观察方向不敏感,高光分支则显式建模视角相关反射。
在体积渲染阶段,每个采样点计算一次颜色和密度,再按透明度混合。由于高光项在单个点上可能数值很大,网络需要学会把大亮度分配给高光分支而不是反照率。否则输出颜色虽然也能拟合训练图像,但反照率贴图会被高光污染。
训练损失与正则化约束
仅靠像素颜色重建损失并不总能保证分离干净。比如一个纯白色表面在固定光照下某处亮度很高,网络可以把该点反照率设为0.8、高光设为0.2,也可以把反照率设为1.0、高光设为0.0,两者渲染结果相同。为了消除这类歧义,NeRD引入了几类正则项。
第一项是反照率平滑损失,鼓励相邻表面点的漫反射颜色变化缓慢。真实材质中反照率通常具有局部连续性,而高光可以随曲率快速变化。第二项是粗糙度先验,防止网络把所有表面都预测成极端镜面或极端漫反射。第三项有时会对高光残差施加稀疏约束,只允许少数区域出现强高光,而不是整个场景平均分摊高光能量。部分实现还会对法线方向与密度梯度方向做一致性约束,保证材质和几何不脱节。
def total_loss(render_pred, pixel_gt, albedo, roughness, normal, grad_normal):
color_loss = torch.mean((render_pred - pixel_gt) ** 2)
albedo_smooth = torch.mean((albedo[1:] - albedo[:-1]) ** 2)
normal_consist = torch.mean((normal - grad_normal) ** 2)
roughness_reg = torch.mean((roughness - 0.5) ** 2)
loss = color_loss + 0.01 * albedo_smooth + 0.05 * normal_consist + 0.001 * roughness_reg
return loss
这些权重需要根据场景规模调整。如果平滑项过强,反照率会丢失纹理细节;如果粗糙度正则过强,金属和塑料的差异会被抹平。这种平衡是NeRD类方法在实际部署时的主要调参工作。
与直接使用NeRF颜色场的方法相比,NeRD的损失函数要求网络在中间层就满足物理结构,因此对训练视角数量的要求更低。即使某些高光只在极少数输入视图中出现,只要几何和材质先验足够,网络也能推断出合理的漫反射基底。
实际效果与限制
经过训练后,NeRD可以输出每个空间点的反照率、粗糙度和法线。把反照率单独可视化时,高光亮斑会消失,剩下干净的基底颜色。把粗糙度降低会让高光范围变窄、材质看起来更光滑;旋转光源时,高光会根据新光照方向重新出现,而漫反射阴影也随之更新。这种编辑能力在增强现实、电影虚拟资产和游戏材质生成中非常实用。
不过NeRD也有明显限制。首先是输入要求多视角图像覆盖足够的观察方向和光照变化,如果所有照片都来自同一角度或同一光照,高光和漫反射仍然难以区分。其次,对于非常复杂的半透明材质、各向异性材质或多次散射明显的材质,当前模型假设的BRDF形式不够灵活。最后,体积渲染的训练和推理成本比传统网格纹理高得多,移动端实时运行还需要进一步蒸馏或烘焙。
即便如此,NeRD代表的方向很清晰:与其让网络黑箱预测颜色,不如把物理反射模型嵌入神经渲染框架。这样既能利用深度学习的表达能力处理真实图像,又能保留可解释、可编辑的材质参数。漫反射与高光分离不再依赖实验室级采集,普通多视角照片也能成为材质重建的输入来源。