3D Gaussian Splatting凭借实时渲染和高质量重建能力,已经成为三维场景表示领域的热门方案。但很多实际应用场景里,仅仅还原原始扫描数据是不够的——用户希望像操作游戏引擎里的光源一样,自由调整环境光的强度、色温,甚至重新布置阴影。这篇文章就聚焦3DGS的光照编辑问题,拆解环境光照和阴影两个维度的修改方法,并给出可以直接嵌入管线中的代码思路。

先明确一点:3DGS本身并不是一个物理光照模型。它用数百万个各向异性的高斯椭球拼出场景,每个高斯点携带位置、协方差、不透明度和颜色属性。颜色属性通常用球谐函数(Spherical Harmonics)展开,常见的是三阶SH,每个高斯点存储48个或更多的系数。这些系数编码了视角相关的辐射度,也就是说,从不同方向看这个高斯点,颜色会按照SH基函数的组合发生变化。环境光照信息就藏在这些系数里,尤其是零阶和一阶系数,它们决定了基础亮度和大致的方向性明暗。
理解了这个存储机制,环境光照的编辑就有了明确的操作对象。与其在渲染端做图像后处理,不如直接在SH系数层面做数学变换。后处理的问题在于它只能做全局调整,无法区分场景中不同材质对光照变化的响应差异,而修改SH系数则能保留每个高斯点独有的反射特性。不过SH系数与真实物理光照之间没有简单的线性对应关系,直接改系数容易带来伪影,需要谨慎设计变换策略。
环境光照编辑:从SH系数出发的两种思路
第一种思路最直接,就是给所有高斯点的低阶SH系数施加统一的偏移或缩放。如果只想让场景整体变亮,可以把零阶系数(对应常数项,控制各向同性的基础颜色)乘以一个大于1的因子。这个操作等价于提升环境光的漫反射水平,高光部分基本不受影响。想让场景偏暖色调,可以在R通道的零阶系数上增加一个正值,同时小幅下调B通道。但对于高阶系数,这种全局偏移会破坏视角相关的细节,因为不同高斯点的高阶系数差异很大,统一修改会让某些区域的镜面反射变得不自然。
一个改进方案是按照高斯点的法线方向分区处理。虽然3DGS没有显式法线,但可以通过协方差矩阵的最小特征向量近似得到每个高斯点的朝向。获得法线后,就可以把环境光编辑建模成一个简单的光照计算:新颜色等于原颜色乘以一个依赖法线与光源方向点积的权重。这种方法在处理单一方向的环境光变化时效果不错,比如模拟太阳从东侧照射改为从西侧照射。但法线估计本身存在噪声,尤其是对于细薄结构和透明物体,误差会在编辑后被放大。
第二种思路更贴近物理,叫作环境光分解与重合成。先对场景做一次预计算,把每个高斯点的SH系数投影到一组预定义的环境光基函数上,得到每个高斯点对各类光照条件的响应系数。编辑时,用户指定新的环境贴图或调整光照参数,系统根据响应系数快速合成新的SH系数。这种做法的优点是可以支持非常复杂的环境光照变化,比如从室内灯光切换到户外自然光,而不仅仅是亮度或色温的微调。缺点是预计算耗时较长,并且响应系数矩阵需要占用额外显存。
阴影修改:可见性重建与重着色的技术路线
阴影在3DGS中属于隐式信息。原始训练得到的高斯点颜色里已经烘焙了静态阴影,场景重建完成后,阴影就是颜色的一部分,并没有独立的阴影贴图或光源投影数据结构。要修改阴影,本质上是重新计算哪些高斯点应该处于阴影中,然后调整它们的外观。
一条可行的技术路线是借助深度信息重建可见性。从新光源位置渲染一张深度图,然后把每个高斯点投影到光源空间,比较其深度与深度图中对应位置的值。如果高斯点深度大于深度图采样值,说明它被遮挡,处于阴影中;否则它直接受光。得到可见性标记后,对阴影中的高斯点降低亮度或改变色调,就能模拟阴影变化。这个流程和传统阴影映射(Shadow Mapping)有相似之处,但需要注意高斯点的体积特性——一个高斯点可能部分被遮挡,只做二值判断会产生硬边界,需要对可见性做软过渡处理。
另一种方法不依赖显式的可见性计算,而是采用重着色策略。预先训练一个轻量网络,输入高斯点的几何特征(位置、法线、协方差)和当前光照参数,输出该点应该呈现的颜色。编辑阴影时,改变光照参数,网络自动推断新的着色结果,包括阴影和半影区域。这条路线的优势是阴影过渡更柔和,但它需要额外的训练数据和计算资源,并且在处理高频阴影细节时容易产生模糊。
实际工程中,混合方案往往更实用。对于静态场景中用户明确指定光源方向的简单情况,深度图方法足够快且可控。对于动态光照或要求阴影物理正确性的场景,再引入重着色网络。需要强调的是,无论哪种方法,修改阴影后都应当同步调整对应区域的SH系数,否则后续渲染时原始烘焙的阴影信息会重新出现,造成视觉冲突。
代码示例:实现一个基础的环境光强度调节器
下面这段代码演示了如何读取3DGS场景数据,对零阶SH系数做全局强度调整。代码假设场景数据已经加载为PyTorch张量,每个高斯点的SH系数存储在形状为[N, 48]的数组中,前3个值对应RGB三个通道的零阶系数。
import torch
def adjust_environment_light(sh_coeffs, intensity_scale=1.5, warm_bias=0.0):
"""
调整3DGS场景的环境光强度与色温。
参数:
sh_coeffs: 形状为[N, 48]的张量,N为高斯点数量,
前3个通道对应RGB零阶SH系数。
intensity_scale: 整体亮度缩放因子,大于1变亮,小于1变暗。
warm_bias: 暖色偏移量,正值增加红色、减少蓝色。
返回:
调整后的SH系数张量。
"""
adjusted = sh_coeffs.clone()
# 零阶系数索引:直流分量位于SH展开的首位,RGB各占一个
# 对于三阶SH,总系数为16个基函数 x 3通道 = 48
# 零阶基函数只有一个,对应索引0, 16, 32
dc_indices = [0, 16, 32]
# 应用亮度缩放
adjusted[:, dc_indices] = adjusted[:, dc_indices] * intensity_scale
# 应用暖色偏移
if warm_bias != 0.0:
# R通道零阶系数增加
adjusted[:, 0] += warm_bias
# B通道零阶系数减少
adjusted[:, 32] -= warm_bias * 0.6
# 确保数值在合理范围内
adjusted = torch.clamp(adjusted, min=0.0, max=10.0)
return adjusted
# 使用示例
# 假设已有高斯点SH系数张量 sh_data,形状为[N, 48]
# new_sh = adjust_environment_light(sh_data, intensity_scale=1.8, warm_bias=0.15)
这个调节器只操作零阶系数,因此对场景的影响偏向全局光照。如果希望模拟方向性光源变化,还需要修改一阶系数。一阶SH有三个基函数,对应RGB三通道各占3个系数,索引范围分别是1到3、17到19、33到35。对一阶系数的调整可以改变光照的方向性分布,但直接修改这些系数容易破坏颜色通道间的平衡,实践中通常配合法线信息做加权修正。
阴影修改的代码实现更为复杂,下面给出一个基于深度图判断可见性的简化版本。这个例子假设已经获得了从新光源位置渲染的深度图,以及场景中每个高斯点的世界坐标。
import torch
import torch.nn.functional as F
def mark_shadowed_points(gaussian_positions, light_view_matrix, light_depth_map, light_size):
"""
判断每个高斯点是否处于新光源的阴影中。
参数:
gaussian_positions: 形状为[N, 3]的张量,高斯点世界坐标。
light_view_matrix: 4x4光源空间变换矩阵。
light_depth_map: 形状为[1, H, W]的张量,光源深度图。
light_size: 元组(H, W),深度图尺寸。
返回:
形状为[N]的布尔张量,True表示该点在阴影中。
"""
N = gaussian_positions.shape[0]
ones = torch.ones(N, 1, device=gaussian_positions.device)
pos_homo = torch.cat([gaussian_positions, ones], dim=1) # [N, 4]
# 变换到光源裁剪空间
light_clip = pos_homo @ light_view_matrix.T # [N, 4]
light_ndc = light_clip[:, :3] / light_clip[:, 3:4].clamp(min=1e-6)
# 转换到深度图像素坐标
u = ((light_ndc[:, 0] + 1) * 0.5) * (light_size[1] - 1)
v = ((light_ndc[:, 1] + 1) * 0.5) * (light_size[0] - 1)
point_depth = light_ndc[:, 2] # 高斯点在光源空间的深度
# 采样深度图
grid = torch.stack([u, v], dim=1).unsqueeze(0).unsqueeze(0) # [1, 1, N, 2]
grid = grid * 2 / (torch.tensor(light_size[::-1], device=grid.device) - 1) - 1
sampled_depth = F.grid_sample(light_depth_map.unsqueeze(0), grid,
mode='bilinear', padding_mode='zeros',
align_corners=True).squeeze()
# 深度比较:点深度大于表面深度则被遮挡
shadowed = point_depth > sampled_depth + 1e-3
return shadowed
上述代码返回布尔标记后,就可以对阴影中的高斯点做颜色衰减。实际使用中,直接改变高斯点的SH系数比在渲染后做像素级阴影叠加更稳定,因为渲染后的阴影叠加会与镜头相关的反射效果产生冲突。另外,为了获得柔和的阴影边缘,可以对可见性标记做一次高斯模糊,或者根据深度差计算一个0到1之间的软阴影因子,而不是使用硬布尔值。
实际编辑中的常见问题与规避策略
环境光照编辑的第一个常见问题是颜色溢出。当强度缩放因子过大时,原本接近饱和的区域会迅速过曝,丢失纹理细节。这是因为SH系数直接决定了高斯点的颜色输出,没有经过色调映射的缓冲。解决方法是做缩放时采用对数域操作,或者设置一个保护区间,对数值接近上下限的系数施加递减的缩放率。另一个问题是一致性破坏——如果场景中一部分高斯点来自反光表面,环境光改变后它们的高阶系数没有对应调整,就会出现闪光点。对此需要在编辑前对高斯点做材质分类,给金属类和镜面类表面单独设计系数变换规则。
阴影修改方面,最常见的错误是深度比较精度不足。3DGS场景的高斯点数量通常很大,不少点位于被遮挡的边缘地带,这些位置的深度比较非常敏感,偏差几个毫米就会导致标记翻转。解决手段包括提高深度图分辨率、在光源空间使用更紧的投影矩阵、以及对高斯点做收缩处理——将高斯点沿其主轴方向向中心缩进一定比例再进行深度比较。另一个难以处理的情况是半透明物体和参与介质,它们的阴影本来就包含透射光成分,简单的二值遮挡判断会丢失这部分效果,需要引入透射率权重来修正。
还有一个工程层面的问题值得注意:修改光照后,高斯点颜色分布的统计特性会变化,原始训练时用到的自适应密度控制参数可能不再适用。如果编辑幅度较大,建议执行一次轻量的微调步骤,只更新颜色相关参数而冻结几何结构,这样可以在保持几何稳定性的同时让光照编辑结果更平滑。
总的来说,3DGS的光照编辑还远没有达到传统渲染管线那种精确可控的程度,但通过SH系数变换、环境光分解重合成、深度图可见性重建这几条技术路径,已经能够覆盖大量实际需求。随着3DGS向物理渲染方向演进,光照编辑的精度和灵活性还会继续提升。

