项目地址:
GitHub - NJU-3DV/Relightable3DGaussian: [ECCV2024] 可重新照明的 3D 高斯:使用 BRDF 分解和光线追踪的实时点云重新照明
可优化参数
gaussians.training_setup(opt)
if is_pbr:: direct_env_light.training_setup(opt)
光照解耦
update_visibility
分块处理高斯
对每个高斯体,采样 sample_num 个入射方向,计算每个方向的可见性(是否被遮挡)、方向向量、该方向在球面上的面积权重,并将这些结果保存下来
fibonacci_sphere_sampling斐波那契球面采样
输出
- incident_dirs:每个点/法线的 sample_num 个采样方向(单位向量),用于模拟不同角度的入射光。
- normals 是 [N, 3],则输出 incident_dirs 是 [N, sample_num, 3]
- incident_areas:每个采样方向的面积权重(都相同),用于积分时加权。
- incident_dirs形状[N, sample_num, 1]
在 fibonacci_sphere_sampling 代码中,采样方向是围绕每个高斯体的法线方向生成的:
- 首先在 z 轴方向上生成均匀分布的球面采样(fibonacci sphere)。
- 然后通过旋转,把这些方向对齐到每个高斯体的法线方向(rotation_between_z(normals))。
- 如果 random_rotate=True,还会加上一个随机旋转,使得每个高斯体的采样方向分布更加均匀、随机。
rotation_matrix = rotation_between_z(normals)
incident_dirs = rotation_matrix @ z_samples
- 这里 rotation_matrix 是针对每个高斯体的法线单独计算的。
- 所以 incident_dirs 是每个高斯体独立的一组方向。
render(is_training=true)
render_pkg = render_fn(viewpoint_cam, gaussians, pipe, background,opt=opt, is_training=True, dict_params=pbr_kwargs, iteration=iteration)pbr_kwargs['sample_num'] = pipe.sample_num#64pbr_kwargs["env_light"] = direct_env_light
高斯增加优化属性
- _base_color:表面基础色(反射率/漫反射色),PBR渲染的核心属性。
- _roughness:表面粗糙度,影响高光和反射的模糊程度。
- _incidents_dc/incidents_rest:每个高斯体的入射光照分布(用球谐函数表示),用于高效地近似全局光照。
- _visibility_dc/visibility_rest:每个高斯体的可见性分布(同样用球谐函数表示),用于阴影和遮挡的近似。
first_iter = gaussians.create_from_ckpt(args.checkpoint, restore_optimizer=True)
初始化这些属性为0
环境光照(IBL - Image-Based Lighting)
- 模拟全局光照:环境贴图存储了来自各个方向的环境光强度与颜色,为物体表面提供非直接光照(如漫反射、镜面反射)。
- PBR 材质依赖:在基于物理的渲染中,环境贴图是计算材质反射、折射的基础输入(如金属高光、玻璃透射)
PBR最终像素颜色和渲染方程Rendering equation
参考彻底看懂PBR/BRDF方程 - 张亚坤的文章 - 知乎
https://zhuanlan.zhihu.com/p/158025828
base_color:点云的“固有色”。
pbr:这个点在当前光照、视角、材质等条件下,真实应该呈现出来的颜色。
pbr
:最终像素颜色(Lambert 漫反射 + GGX 镜面反射)
输出结果brdf_color和extra_results
extra_results = {"incident_dirs": incident_dirs,"incident_lights": incident_lights,"local_incident_lights": local_incident_lights,"global_incident_lights": global_incident_lights,"incident_visibility": incident_visibility,"diffuse_light": diffuse_light,"specular": specular,}
基于物理的渲染方程(PBR):
其中:
f_d = base_color[:, None] / np.pi
f_s = GGX_specular(normals, viewdirs, incident_dirs, roughness, fresnel=0.04)
- Lo:出射光(最终颜色)
- fdiffuse:漫反射 BRDF
- fspecular:镜面反射 BRDF
- Li:入射光强度
- n⋅ωi:余弦项(法线与入射光方向的点积)
diffuse_light = transport.mean(dim=-2):: 表示表面接收到的总光照强度
rasterizer结果
变量名 | 说明 | 典型形状 |
---|---|---|
num_rendered | 渲染的高斯点总数 | 标量/int |
num_contrib | 每像素有贡献的高斯点数 | [H, W] |
rendered_image | 渲染出的RGB图像 | [3, H, W] |
rendered_opacity | 渲染出的不透明度图 | [1, H, W] |
rendered_depth | 渲染出的深度图 | [1, H, W] |
rendered_feature | 渲染出的特征图(多通道) | [C, H, W] |
rendered_pseudo_normal | 渲染出的伪法线图 | [3, H, W] |
rendered_surface_xyz | 渲染出的表面3D坐标 | [3, H, W] |
weights | 高斯点对像素的贡献权重 | [N, H, W] |
radii | 高斯点在屏幕上的半径 | [N] |
render输出results
feature和rendered_image、rendered_pbr
if is_training:features = torch.cat([depths, depths2, brdf_color, normal, base_color, roughness, extra_results["diffuse_light"], extra_results["incident_visibility"].mean(-2)], dim=-1)else:features = torch.cat([depths, depths2, brdf_color, normal, base_color, roughness,extra_results["diffuse_light"], extra_results["specular"], extra_results["incident_lights"].mean(-2),extra_results["local_incident_lights"].mean(-2),extra_results["global_incident_lights"].mean(-2),extra_results["incident_visibility"].mean(-2)], dim=-1)
在gaussian_renderer\neilf.py里的render_view
在gaussian_renderer\r3dg_rasterization.py中
num_rendered, num_contrib, color, opacity, depth, feature, normal, surface_xyz, weights, radii, geomBuffer, binningBuffer, imgBuffer = _C.rasterize_gaussians(*args)
在r3dg-rasterization\ext.cpp中
在r3dg-rasterization\rasterize_points.cu中
return std::make_tuple(
rendered, n_contrib, out_color, out_opacity, out_depth, out_feature, out_normal, out_surface_xyz, out_weights, radii, geomBuffer, binningBuffer, imgBuffer);
与 下图一一对应
num_rendered, num_contrib, color, opacity, depth, feature, normal, surface_xyz, weights, radii, geomBuffer, binningBuffer, imgBuffer = _C.rasterize_gaussians(*args)
在r3dg-rasterization\cuda_rasterizer\forward.cu中
rendered_image:是通过球谐函数(Spherical Harmonics, SH)渲染得到的图像,即上图中的out_color
rendered_pbr:是通过物理基础渲染(Physically Based Rendering, PBR)得到的图像,即上图中的out_feature(对应render_feature)中的一部分
pbr = rendered_pbr
rendered_pbr = pbr * rendered_opacity + (1 - rendered_opacity) * bg_color[:, None, None]
out_feature(对应render_feature)由features加权得出,rendered_pbr对应由brdf_color加权
if is_training:features = torch.cat([depths, depths2, brdf_color, normal, base_color, roughness, extra_results["diffuse_light"], extra_results["incident_visibility"].mean(-2)], dim=-1)else:features = torch.cat([depths, depths2, brdf_color, normal, base_color, roughness,extra_results["diffuse_light"], extra_results["specular"], extra_results["incident_lights"].mean(-2),extra_results["local_incident_lights"].mean(-2),extra_results["global_incident_lights"].mean(-2),extra_results["incident_visibility"].mean(-2)], dim=-1)
重照明relighting.py
文件路径变量
scene_config_file
路径:{args.config}/transform.json
作用:场景的空间变换配置文件,通常描述每个子场景或物体的变换矩阵(如平移、旋转、缩放)。
traject_config_file
路径:{args.config}/trajectory.json
作用:相机轨迹配置文件,通常描述渲染时相机的运动路径、每一帧的相机参数等。
light_config_file
路径:{args.config}/light_transform.json
作用:光源轨迹或光照变换配置文件,描述光源的位置、方向、变化等(如果有动态光照)。
为什么可以实现
if iteration % args.save_interval == 0 or iteration == args.iterations:#5000print("\n[ITER {}] Saving Gaussians".format(iteration))scene.save(iteration)
在运行script\run_nerf.sh后 保存高斯各属性在output/NeRF_Syn/hotdog/neilf/point_cloud/iteration_40000/point_cloud.ply类似路径,在configs\nerf_syn_light\transform.json中保存各场景路径和transform矩阵
在scene_composition中加载ply文件,
relighting.py 的流程:读取高斯体模型(如 .ply 文件);读取环境光照(如 .hdr 文件;读取相机参数;用渲染器(如 render_fn_dict['neilf'])直接渲染图片。
只要有一个已经拟合好的3D场景(高斯体参数),可以随时改变光照条件(比如换环境贴图),然后用渲染器重新渲染出不同光照下的图片。这就是重照明。因为场景的几何和材质参数已经在训练阶段学好了,渲染时只需要前向推理(forward),不需要再优化参数。
`scene_composition`
功能
scene_composition
的作用是:
将多个场景(或点云)的高斯体模型合成为一个整体高斯体模型,并做必要的初始化。
具体流程如下:
-
遍历
scene_dict
,为每个子场景加载高斯体点云(.ply
文件),并应用相应的变换(transform
),把每个子场景的点云(高斯体)通过指定的 4x4 变换矩阵变换到全局坐标系,。 -
把所有加载好的高斯体模型合成为一个大模型(调用
GaussianModel.create_from_gaussians
)。 -
对合成后的模型做一些初始化(如可见性、入射光参数等)。
-
返回合成后的高斯体模型。
-
scene_dict: dict
-
字典格式,key 是场景名,value 是一个字典,包含:
-
"path"
:点云文件路径(.ply) -
"transform"
:4x4 的变换矩阵(list 或 array)
-
-
-
dataset: ModelParams
-
数据集参数对象,至少包含
sh_degree
等高斯体模型初始化所需参数。
-
-
gaussians_composite: GaussianModel
-
合成后的高斯体模型对象,包含所有子场景的点云和参数,已经应用了各自的变换,并做了初始化。
-
-
加载每个子场景的高斯体模型并变换
for scene in scene_dict:gaussians = GaussianModel(dataset.sh_degree, render_type="neilf")gaussians.load_ply(scene_dict[scene]["path"])torch_transform = torch.tensor(scene_dict[scene]["transform"], device="cuda").reshape(4, 4)gaussians.set_transform(transform=torch_transform)gaussians_list.append(gaussians)
-
合成所有高斯体模型
gaussians_composite = GaussianModel.create_from_gaussians(gaussians_list, dataset)
-
初始化可见性和入射光参数
n = gaussians_composite.get_xyz.shape[0] gaussians_composite._visibility_rest = (torch.nn.Parameter(torch.cat([gaussians_composite._visibility_rest.data,torch.zeros(n, 5 ** 2 - 4 ** 2, 1, device="cuda", dtype=torch.float32)],dim=1).requires_grad_(True))) gaussians_composite._incidents_dc.data[:] = 0 gaussians_composite._incidents_rest.data[:] = 0
-
返回合成后的模型
return gaussians_composite
-
功能:合成多个高斯体点云为一个整体,并初始化相关参数。
-
输入:场景字典(含路径和变换)、数据集参数。
-
输出:合成后的高斯体模型对象(
GaussianModel
)。
render(is_training=false)
render_kwargs = {"pc": gaussians_composite,"pipe": pipe,"bg_color": background,"is_training": False,"dict_params": {"env_light": light,"sample_num": args.sample_num,#384},"bake": args.bake}
with torch.no_grad():render_pkg = render_fn(viewpoint_camera=custom_cam, **render_kwargs)
在 render_view
函数中,is_training
参数为 True 或 False 时,渲染流程和输出内容有明显区别,is_training
为 True 时,流程更高效、特征更精简、用于训练和损失计算;为 False 时,流程更全面、特征更丰富、用于推理、评估和可视化。
主要体现在以下几个方面:
is_training=True
- 直接对所有点一次性调用
rendering_equation
,效率高,适合训练时的批量处理。 - 代码片段:
if is_training:brdf_color, extra_results = rendering_equation(base_color, roughness, normal.detach(), viewdirs, incidents,direct_light_env_light, visibility_precompute=pc._visibility_tracing, incident_dirs_precompute=pc._incident_dirs, incident_areas_precompute=pc._incident_areas)
- 直接对所有点一次性调用
is_training=False
- 为了节省显存,采用分块(chunk)处理,每次只处理一部分点,适合测试/推理时大规模渲染。
- 代码片段:
else:chunk_size = 100000brdf_color = []extra_results = []for i in range(0, means3D.shape[0], chunk_size):_brdf_color, _extra_results = rendering_equation(...)brdf_color.append(_brdf_color)extra_results.append(_extra_results)brdf_color = torch.cat(brdf_color, dim=0)extra_results = {k: torch.cat([_extra_results[k] for _extra_results in extra_results], dim=0) for k in extra_results[0]}torch.cuda.empty_cache()
-
is_training=True
-
拼接的特征较少,只包含训练所需的内容。
- 代码片段:
features = torch.cat([depths, depths2, brdf_color, normal, base_color, roughness, extra_results["diffuse_light"], extra_results["incident_visibility"].mean(-2)], dim=-1)
-
-
is_training=False
-
拼接的特征更丰富,包含更多渲染细节,便于评估和可视化。
- 代码片段:
features = torch.cat([depths, depths2, brdf_color, normal, base_color, roughness,extra_results["diffuse_light"], extra_results["specular"], extra_results["incident_lights"].mean(-2),extra_results["local_incident_lights"].mean(-2),extra_results["global_incident_lights"].mean(-2),extra_results["incident_visibility"].mean(-2)], dim=-1)
-
-
is_training=True
-
rendered_feature
只拆分出训练所需的特征(如 base_color、roughness、diffuse、visibility)。
-
-
is_training=False
-
rendered_feature
拆分出更多特征(如 specular、lights、local_lights、global_lights 等),便于评估和可视化。
-
-
is_training=False
-
还会输出
render_env
、pbr_env
、env_only
等环境光相关的渲染结果,便于评估不同光照下的表现。
-
-
is_training=True
-
这些环境光相关的输出不会被计算,节省计算资源。
-
-
is_training=True
-
会进一步调用
calculate_loss
计算损失和训练日志。
-
-
is_training=False
-
不计算损失,只输出渲染结果。
-
is_training | 主要用途 | BRDF计算 | 特征拼接 | 输出内容 | 损失计算 | 评估/可视化 |
---|---|---|---|---|---|---|
True | 训练 | 一次性 | 精简 | 精简 | 有 | 少 |
False | 推理/评估 | 分块 | 丰富 | 丰富 | 无 | 多 |
render输出results
评估
- train.py 的评估是为了训练过程中的监控和调优,通常和训练流程绑定。
- eval_nvs.py 是专门为独立评测和论文展示设计的,方便单独运行和批量评测。