三维高斯溅射技术通过数以百万计的微小高斯椭球体来重构三维空间,在视角合成方面表现出极高的逼真度。然而,在处理大规模户外场景时,如果仅依靠高斯椭球体本身的颜色和透明度进行渲染,往往会丢失距离感。远处的物体与近处物体在色彩饱和度和对比度上缺乏差异,导致整个场景看起来像是一个缩微模型,而非真实世界的广阔空间。大气散射与雾效的引入,正是为了解决这一空间感缺失的问题,通过模拟光线在大气中的传播衰减,为场景注入真实的纵深感。

为什么3DGS场景需要大气散射与雾效?
在真实世界中,光线在穿过大气层时会被空气分子、水蒸气和微小颗粒散射。这种物理现象导致远处的物体看起来颜色更淡、对比度更低,且整体色调偏向天空的背景色。这种现象被称为大气透视。在3DGS的默认渲染管线中,每个高斯椭球体都是独立计算的,它们并不会自动感知到光线在到达相机前所经历的长距离大气衰减。
缺乏雾效的3DGS场景在表现远景时会出现几个明显问题。首先是深度感知错误,观察者很难准确判断远处山脉或建筑的真实距离。其次是渲染瑕疵暴露,在视角发生偏移时,远处的稀疏高斯点容易出现突变或摩尔纹效应,而雾效可以作为一种自然的遮罩,平滑这些视觉瑕疵。最后,雾效能够极大地提升场景的氛围感,无论是清晨的薄雾还是黄昏的尘埃,都能让画面更具艺术表现力。
因此,在3DGS的光栅化渲染阶段引入大气散射计算,不仅是视觉效果的增强,更是物理真实性的必要补充。我们需要在片元着色器中,根据当前像素的深度值,混合原始颜色与大气散射颜色,从而模拟出光线在介质中的透射与散射过程。
基于深度信息的均匀雾效实现方案
实现雾效最基础的方案是线性深度雾。这种方案的核心思想是获取当前像素在视图空间下的深度值,然后根据该深度值在原始物体颜色和雾颜色之间进行线性插值。在3DGS的渲染管线中,通常是在处理完所有高斯椭球体的混合操作后,于后处理阶段或片元着色阶段加入这一步操作。
具体实现时,我们需要在着色器中获取片元的深度信息。由于3DGS采用的是基于瓦的光栅化技术,我们可以通过当前片元的视图空间坐标计算其与相机的距离。设定一个雾的起始距离和结束距离,在这个区间内,雾的浓度从零线性增加到一。这种计算方式极其高效,对渲染性能的影响微乎其微。
下面是一个在GLSL中实现基础线性雾效的代码示例。在这个示例中,我们假设已经获取了片元的视图空间深度,并将其转化为雾的混合因子。
// 基础线性雾效计算
uniform vec3 fogColor;
uniform float fogNear;
uniform float fogFar;
// fragDepth 为当前片元在视图空间的深度值
float computeLinearFogFactor(float fragDepth) {
// 限制深度在近远平面之间
float dist = clamp(fragDepth, fogNear, fogFar);
// 计算线性插值因子
float fogFactor = (dist - fogNear) / (fogFar - fogNear);
return clamp(fogFactor, 0.0, 1.0);
}
// 在主渲染函数中应用
vec3 applyLinearFog(vec3 originalColor, float fragDepth) {
float factor = computeLinearFogFactor(fragDepth);
return mix(originalColor, fogColor, factor);
}
虽然线性雾实现简单且计算开销极低,但其视觉效果相对生硬。真实世界中的雾气浓度并非线性增长,而是呈现出指数衰减的规律。因此,线性雾通常只用于快速预览或特定的风格化渲染场景中,若要追求写实,则需要更复杂的模型。
进阶:引入指数衰减与光线散射模型
为了获得更真实的视觉效果,我们需要引入指数平方雾效模型。这种模型基于物理光学原理,认为光线在穿过均匀介质时,其强度按照指数规律衰减。指数雾效能够更好地模拟薄雾到浓雾的过渡,使得远近景物的层次感更加自然。在着色器实现上,我们只需将线性插值改为指数函数计算即可。
更进一步,我们可以实现简单的大气散射效果。真实的大气散射不仅会衰减物体原本的颜色,还会将太阳光或天空光散射到观察者的视线中。这意味着雾的颜色不应该是单一的固定值,而是应该根据视线方向与光源方向的夹角发生变化。当视线朝向太阳时,雾的颜色应该更亮且偏暖色;背向太阳时则偏冷色。
实现这种方向性散射,我们需要在着色器中引入光源方向向量。通过计算视线方向与光源方向的点积,并将其作为输入计算相位函数,可以得到散射光的强度。结合指数衰减的透射率,最终的颜色由透射后的物体颜色和散射进入视线的环境光共同组成。
// 指数雾效与方向性散射
uniform vec3 sunDirection; // 归一化的太阳方向
uniform vec3 sunColor; // 太阳散射颜色
uniform float fogDensity; // 雾气密度
float computeExponentialFog(float fragDepth) {
// 指数平方衰减
float fogFactor = 1.0 - exp(-pow(fragDepth * fogDensity, 2.0));
return clamp(fogFactor, 0.0, 1.0);
}
vec3 applyAdvancedFog(vec3 originalColor, float fragDepth, vec3 viewDir) {
// 计算基础透射率
float transmittance = 1.0 - computeExponentialFog(fragDepth);
// 计算方向性散射 (简单的相位函数近似)
float sunAmount = max(dot(viewDir, sunDirection), 0.0);
// 利用幂函数模拟太阳光晕的聚集效果
vec3 scatteredLight = sunColor * pow(sunAmount, 8.0);
// 混合基础雾色和太阳散射光
vec3 totalFogColor = mix(fogColor, scatteredLight, 0.5);
// 最终颜色 = 透射的物体颜色 + 散射进入视线的光
return originalColor * transmittance + totalFogColor * (1.0 - transmittance);
}
通过上述代码,雾的颜色不再是静态的,而是会随着相机的视角和光源的位置动态变化。当用户在3DGS场景中漫游时,朝向光源方向看去会看到耀眼的光晕,而背光面则呈现出深沉的冷色调,这种动态的光照反应极大地提升了三维场景的沉浸感。
着色器参数调优与性能考量
在3DGS中集成大气散射与雾效时,参数调优是决定最终画面质量的关键。雾的密度参数需要根据场景的实际尺度进行精细调整。如果场景是以米为单位的大规模地形,密度参数通常需要设置得很小,以避免近处物体被过度雾化。同时,雾的颜色和光源颜色应当与场景的天空盒或环境贴图保持一致,否则会产生明显的割裂感。
性能方面,虽然指数运算和点积计算在现代GPU上开销不大,但在3DGS这种以数百万高斯点为基础的渲染管线中,任何额外的片元着色器指令都会被放大。为了优化性能,建议将雾效计算放在屏幕空间的后处理阶段进行,而不是在每个高斯椭球体的混合阶段计算。这样可以确保雾效计算只针对屏幕上的每个像素执行一次,而不是针对重叠的透明高斯体执行多次。
此外,在实现深度获取时,需要注意3DGS的光栅化过程可能会修改深度缓冲区的写入策略。如果采用后处理方案,需要确保在雾效计算前,场景的深度图已经被正确写入并可供读取。通过合理利用渲染管线的各个阶段,我们可以在不破坏3DGS原有高效率渲染的前提下,为其披上一层逼真且富有诗意的大气外衣。