☰
Godot 4体积雾完全解析:从Froxel原理到性能优化实战
2026/10/24 1:28:27 网站建设 项目流程

Godot 4.0 正式发布后,我把官方仓库 godot-demo-projects 里能跑的 demo 基本都过了一遍。大多数演示给我的感觉是“功能说明”,唯独 3d/volumetric_fog 这个体积雾演示让我停下来多看了几分钟:暮色森林里,一束束光线透过树干斜斜打进薄雾,雾不是贴在地面上的一层白,而是真真切切占满了整个空间,光柱从暗到亮再到散开的层次非常自然。我当时的第一反应是:这到底是怎么算出来的?这些参数又该去哪里调?后来我把项目里的 Environment 资源、FogVolume 节点一个个翻过去,又在空场景里从零建了一整套体积雾效果,才算把 Godot 4 的体积雾系统摸透。

这篇文章就把这段折腾的过程完整记录下来:从技术原理讲起,把官方 demo 使用的全局体积雾和局部体积雾参数逐个拆开,再给出一套可以直接抄作业的搭建步骤,最后是性能优化和排坑经验。如果你正在用 Godot 4 做场景氛围,又对体积雾那一长串属性感到头疼,这篇应该能帮你省不少时间。

1. 先搞清楚体积雾到底解决了什么问题

1.1 官方Demo里的雾,和以往用过的雾根本不是一个东西

如果之前用过 Godot 3 或者更早的引擎,对“雾”的理解大概率是两种形态:环境雾和半透明面片加噪声贴图模拟的雾气。环境雾说白了就是一个颜色渐变,把远处的物体往雾色里混合,它没有任何空间形状,光打上去也不会在雾里形成光束。粒子雾更像是贴画,一张张 billboard 转来转去,只适合做局部点缀,做不出“一整片空间里都有悬浮颗粒”的沉浸感。

Godot 4 的体积雾完全不一样。它把雾当作一个真正存在于场景里的三维密度场:某个位置该有多少雾、能被哪盏灯照亮、光穿过之后衰减成什么颜色,都按照近似物理的方式参与计算。官方 demo 里那种暮光穿透森林的效果,正是标准体积光照的产物——光轴的出现,是因为树干遮挡了直射光,雾里于是有了明暗边界;地表的薄雾贴着地面慢慢扩散,则是因为高度衰减参数在起作用。

所以在进入具体参数之前,最好先建立这个认知:体积雾不是“加一层半透明滤镜”,而是“在三维空间里计算光照”。带着这个理解去看后面所有参数,一眼就能明白为什么那样调、为什么有些参数组合会让画面脏成一片。这篇博客适合三类读者:想在项目里加体积雾氛围但不知道怎么下手的;已经加了雾但发现又脏又卡、不知道问题出在哪的;以及纯粹想搞清楚官方 demo 背后原理的引擎爱好者。

1.2 Froxel的白话拆解:把视锥体切成积木块

Godot 4 的体积雾基于 Froxel 技术。Froxel 这个词是 Frustum(视锥体)和 Voxel(体素)的拼接,含义很直白:把相机的视锥体空间按一定间隔切成一个三维网格,每一个小格子就是一个 Froxel。官方文档里那个 volumetric_fog_froxil_size 参数,你可以把它理解成“这个网格的精度档位”,取值有 32、64、128、256、512,大多数场景用 64 或者 128 已经足够。

每一帧渲染时,引擎的处理流程大致是这样的:先在视锥体范围内的每个 Froxel 里采样雾密度,这里的输入包括全局环境的密度场加上场景里所有 FogVolume 的贡献;然后计算这个格子受到了哪些光源的影响,把光照结果和雾的颜色、密度一起写进一张三维纹理。到了真正绘制画面的时候,每个像素沿着视线方向对这张三维纹理做多次采样,逐层累积起来,最后得到屏幕上那个带光柱、带层次感的雾。

这里要强调一个工程上的关键点:Godot 4 实现的是单次散射近似。意思是引擎只计算“光从光源到某个体素,再从该体素散射到相机”这一条路径,光在雾里多次弹跳的情况没有完整模拟。所以你基本看不到体积雾投射出非常明显的自阴影,但换来的是实时渲染能接受的性能。这也是体积雾开着依然能跑、而不是像离线渲染那样一帧等好几分钟的根本原因。

拿果冻来类比这个流程挺合适:你就当场景里塞了一大块透明果冻,引擎把果冻切成一排排一格格的立方体,然后往每个格子里注入雾量和灯光颜色,最后人眼透过这些格子看世界,所有格子的颜色叠加起来,就是最终画面。官方 demo 的另一个价值也在这里——它把抽象的理论变得肉眼可见:你试着把 froxil_size 降到 32,或者把 voxel_size 撑大,画面里马上会出现明显的网格感,那就是这些格子存在的证据。

1.3 官方Demo为什么值得当学习样本

说实话,godot-demo-projects 里大部分示例项目都是“为了演示功能而搭的最小场景”,但 volumetric_fog 这个演示恰好在一个相对完整的场景里跑通了全套体积雾链路:Environment 上的全局密度场负责铺底,方向光负责制造光轴,场景里的 FogVolume 节点负责局部浓雾。一口气把这些配置全部打开,你会立刻理解全局体积雾和局部体积雾是怎么配合工作的,而不是看一眼文档就忘。

我自己测试时有个习惯性操作:把 Environment 里的 Volumetric Fog 开关暂时关掉,对比同一个视角的画面。关闭前画面有明显的明暗层次和大气纵深,关闭后就是一个干净但略显干瘪的三维场景。这个 A/B 对比能帮你快速建立“体积雾到底贡献了多少画面成分”的判断,后面做性能取舍、跟美术沟通需求的时候,心里都会更有底。

2. 官方Demo背后的关键参数拆解

2.1 Environment上那一排volumetric_fog属性怎么理解

打开 WorldEnvironment 节点里的 Environment 资源,勾选 Volumetric Fog 之后,面板会多出一长串以 volumetric_fog 开头的属性。第一次看到确实有点吓人,但按功能可以分成三类:颜色与密度、高度分布、性能与范围。我先把最常用的一组列出来:

属性默认值作用
volumetric_fog_enabled关体积雾总开关
volumetric_fog_density0.05基础雾密度,越大雾越浓
volumetric_fog_albedo白色雾的固有颜色,相当于雾把光照回人眼的基础色调
volumetric_fog_emission黑色雾的自发光颜色,不为黑时雾自身会发光
volumetric_fog_height_falloff2.0高度衰减速度,越大则雾随高度消失越快
volumetric_fog_height_density0.0地面基础高度密度,让雾从底部开始更浓
volumetric_fog_sky_affects1.0天空光对雾颜色的影响权重
volumetric_fog_gi_inject1.0全局光照注入到雾里的强度
volumetric_fog_froxil_size64视锥方向上的网格精度档位
volumetric_fog_voxel_size0.5单个雾体素的世界尺寸(米)
volumetric_fog_length64体积雾从相机延伸的最远距离(米)

表格里密度和颜色那几项比较好理解,真正容易搞混的是 height_falloff 和 height_density 这一对。height_falloff 控制的是“雾随着高度上升而衰减的速度”,值越大,雾越只存在于地面附近;height_density 则是“地面本身的基础浓度”,它会往整个场景的底部叠加一层额外密度。想做出贴地流动的晨雾,我一般把 falloff 调到 4 到 6,把 height_density 放在 0.2 到 0.5 之间,效果比单纯拉高全局 density 自然得多。

emission 这个属性是很多人容易踩误区的地方。它不是给雾加贴图,而是让雾本身成为光源。做“魔法烟雾”“毒气区域”这类设定时给一个青色或者绿色,雾会自发光,氛围很足。但做正常的场景雾我强烈建议保持黑色,让雾完全依赖场景光照来显色,否则雾会显得很平、像一层蒙板。

sky_affects 和 gi_inject 属于进阶项。sky_affects 负责让天空颜色参与雾的散射计算,户外场景默认 1.0 通常没问题;但在室内,如果天空很亮而室内很暗,雾会被天光照得发蓝发白,很不自然,这时候应该往下调。gi_inject 负责把整个场景的全局光照结果(VoxelGI、SDFGI 这些间接光)注入到雾里,让雾在阴影区域也能被“照亮”。这两项是暗场景里雾发黑的共同元凶,后面排查章节我会再详细说。

性能三件套 froxil_size、voxel_size、length 也都在 Environment 里。froxil_size 是深度方向上的精度档位,voxel_size 是每个格子占多少世界单位,length 是雾参与计算的最远距离。举一个直观的例子:voxel_size 取 0.5、length 取 64 时,相机深度方向大约会划分出 128 层格子,这个量级在桌面 GPU 上没问题,但在移动端就需要把 length 缩到 32 甚至更小。这三个参数是体积雾性能的核心杠杆,后面优化章节会展开讲。

2.2 局部体积雾:FogVolume 与 FogMaterial

全局体积雾负责整体大气感,但很多场景需要“某一坨区域特别多雾”:井口冒出的白汽、洞窟里的潮湿雾团、河面上的晨雾。Godot 4 用 FogVolume 节点解决这件事。它像一个可缩放的光照区域一样挂在场景里,可以选择 Box、Sphere、Ellipsoid、Cone、Cylinder 这些形状,作用范围也就是雾出现的范围,超出这个范围雾立刻消失,过渡非常清晰。

FogVolume 需要配合 FogMaterial 使用。FogMaterial 里同样有 density、albedo、emission、height_falloff 这些属性,含义和 Environment 里的全局参数一致,但只作用于当前这个 FogVolume。比较特别的是材质里还有一组动画参数:启用 animated 之后,雾的密度会用噪声随时间扰动,配合 animation_speed 可以实现缓慢翻滚的效果;detail_spread 控制噪声在空间里的重复频率,数值越小噪点块越大越像云,数值越大细节越碎越接近均匀薄雾。

这里有一个我踩过的坑:FogVolume 形状越大,同时 animated 又开着时,性能消耗会比想象中高很多,因为每个体素都要额外计算一次噪声。做河面晨雾这种大范围 Volume 时,我的做法是只对贴着水面的区域加一个很薄的 Box Volume,而不是直接罩一个巨型 Ellipsoid。视觉上差别不大,性能却经常能差出一倍以上。

2.3 参数之间的联动关系

体积雾的参数不是独立变量,调一个往往会影响另外几个的表现。比如 density 调高之后雾变浓,光穿过雾时的衰减也跟着变快,光柱会变得更窄、更亮,但与此同时场景会整体发白。如果 Albedo 又选了明亮的颜色,画面很快就灰成一片。所以我的调参顺序永远是:先定密度,再定高度分布,然后加光,最后调颜色。反过来调会很容易陷入“这边补好了,那边又过了”的循环。

另一个非常典型的联动是 sky_affects 和 albedo。在户外开放场景里,雾的颜色很大程度上由天空决定,所以 albedo 保持白色就行,色调方向交给天空;但在室内场景里天空几乎照不到,albedo 就成了主要调色手段。很多人发现室内雾发蓝发灰,原因就是 sky_affects 还开着 1.0,而这类房间根本没有天空,雾拿到了一个错误的天空颜色做混合。

3. 实操:从零搭一个体积雾场景

3.1 场景搭建:最小可运行配置

说再多也不如自己跑一遍。我在一个空项目里从零搭了一套体积雾验证场景,过程非常简单,但覆盖了 80% 的常见需求。步骤基本是这四步:新建一个 3D 场景,加入 WorldEnvironment 节点并新建 Environment 资源;在 Environment 里打开 Volumetric Fog,基础密度先设成 0.05;加一盏 DirectionalLight3D,旋转角度让光线从侧面打过来;最后随便放几个方块、柱子当遮挡物。

为什么一定要放遮挡物?因为体积雾光柱的本质,是“光经过遮挡物边缘之后,在雾中形成的亮暗分界”。没有遮挡物时,整个雾被均匀照亮,画面只是蒙了一层白,根本看不到光束结构。官方 demo 里那些树起的作用就是截断光路:树干把光一块块挡住,视觉上立刻出现了光束感。想复现官方 demo 的效果,场景里必须要有明显的高低错落物体。

相机的设置也值得说一句。volumetric_fog_length 默认是 64 米,意思是离相机超过 64 米的区域不会参与体积雾计算。如果你的场景很大,不调 length 就会发现远处完全没有雾;反过来,length 拉得太大,近处的雾会被稀释,整体浓度变低。实际项目里我习惯把 length 设成“相机在最远处需要雾的有效距离”,而不是整个关卡范围。

3.2 调参顺序:先有雾,再有光柱,最后调色

我推荐的调试顺序分三步。第一步调全局密度和高度分布:density 从 0.05 开始,每增加 0.02 就观察画面里的“压气感”,直到雾能明显看出厚度和层次;接着把 height_falloff 调到 3 到 5 之间,height_density 给一个 0.3 左右的值,让雾集中在地表附近。这一步不要碰颜色,只观察密度分布是否自然,最大限度减少变量。

第二步调光,这是光柱出现的关键步骤。方向光的角度要从侧面或低角度射入,角度越平,光线穿过遮挡物后在雾里拉出的亮暗缝隙越长,光束感越明显。如果场景里始终看不到光柱,先检查是不是光的角度太高,而不是怀疑雾本身有问题。同时把光的强度适当调高一些,因为雾在吸收光的同时也是靠光显形的,光不足时雾会显得很脏、发灰。

第三步才轮到调颜色。用 albedo 修正雾的基础色,用 emission 做特殊效果,用 sky_affects 平衡天空的影响。我常用的“安全起点”数值是:density 0.05、height_falloff 4.0、height_density 0.4、albedo 用暖白(0.95, 0.93, 0.9)、sky_affects 0.8、voxel_size 0.5、froxil_size 64、length 48。以这组参数为起点微调,基本不会一上来就糊成一片或者黑成一团。

3.3 局部雾实战:给场景加一坨会动的雾

全局雾铺底之后,我又加了一个 FogVolume 做局部浓雾:新建 FogVolume 节点,形状选 Box,把 Scale 调整到覆盖场景角落的一块区域;给节点挂一个 FogMaterial,density 设成 0.2,比全局雾高不少,然后把 animated 勾上、animation_speed 调到 0.5。这样那个角落的雾会缓慢翻滚,跟全局的静态薄雾形成动静对比,画面一下子生动很多。

这里有一个操作细节值得注意:FogVolume 的 Transform 缩放会影响雾的覆盖范围,但如果把它压成一个极薄的盒子,边缘会出现明显的噪点感,原因在于体素尺寸限制了雾密度变化的最小尺度。我的经验是,局部雾体积切向和深度方向都不要小于 voxel_size 的三倍,否则边缘会“切”得特别生硬。真需要非常薄的雾层,宁可把 Box 压扁并配合高度衰减,也不要让它只有十几厘米厚。

3.4 天空和全局光照的配合

官方 demo 场景里能看到雾被天空和环境光一起“托”起来,并不是只有直射光在起作用。如果你的场景用的是 ProceduralSky,并且想保留这种大气感,建议让 sky_affects 保持在 0.5 到 1.0 之间。天空亮度越高,雾被照亮的范围越大,但这种光没有方向性,会让雾显得软,缺少立体感——所以纯靠天空的雾看久了会觉得有点闷。

如果场景开着 VoxelGI 或 SDFGI,那么 gi_inject 直接决定这些间接光能照亮多少雾。我在暗室内做测试时发现,gi_inject 为 0 时,雾基本是纯黑的,除非有灯光直接照到;调到 0.5 以上之后,雾才出现“被环境烘托”的柔和亮度。注意 gi_inject 不是越高越好,过高的间接光注入会让雾失去明暗对比,整片空间变成灰蒙蒙的。建议从 0.3 到 0.6 之间开始试。

4. 性能优化:体积雾是拿GPU资源换氛围

4.1 开销到底烧在哪里

体积雾的开销逻辑和普通后处理很不一样。普通后处理只对屏幕上的像素做一次计算,体积雾却要先在三维空间里做一次完整的光照计算,再在屏幕空间做一轮采样。以 froxil_size 为 64、视野深度 64 米、voxel_size 为 0.5 为例,相机视锥里大约会划分出 128 层、每层 64 乘 64 的格子,一共五十多万个体素,每个体素都要采样密度、判断光源范围、计算光照衰减。帧率掉得厉害时,往往就卡在这里。

第二个开销点是最终画面合成时的光线步进采样。每个像素沿视线方向在三维纹理里采样多次,采样次数越多、雾层越厚,计算量就越大。因此同一套参数之下,画面里雾越多帧率越差;镜头对着基本没有雾的天空时,帧率反而会高一些。这个规律在性能排查时会很有用。

4.2 实测有效的降耗组合

把常用的降耗手段按性价比排个序,第一位是缩短 volumetric_fog_length。从 64 缩到 32,性能立竿见影地变好,而视觉上大多数情况不受影响,因为远处的雾本来就容易被相机裁剪和深度方向上的精度稀释掩盖。第二位是降低 froxil_size,从 128 降到 64,近距离的质量损失比较明显,但镜头一拉开就很难察觉。第三位才是加大 voxel_size,从 0.25 改成 0.5,这个最容易看到颗粒感,所以我一般放在最后用。

档位froxil_sizevoxel_sizelength适用场景
高画质1280.2564高端 PC 静态展示、电影化镜头
均衡640.548大多数桌面游戏
性能优先640.532中低端设备、高频战斗场景

除了这三个参数,还有两个容易忽略的降耗点。一是动态光源数量:场景里每一盏可能照到雾的光都会参与体积雾光照计算,光源越多开销越大。把光源范围调小、能关阴影就关阴影,能省下不少 GPU 时间。二是移动端:目前移动端跑体积雾需要严格控制负载,我的建议是只开全局雾,关掉所有 FogVolume 的 animated,或者直接做一个基于画质档位的开关,低画质下禁用整个 Volumetric Fog。

4.3 编辑器里量化验证性能

Godot 4 的编辑器里可以直接量化性能:运行项目后打开调试器的 Profiler 标签页,关注 CPU 和 GPU 帧耗时;更直观的做法是把 FPS 和帧耗时显示在运行窗口上。我调整参数时习惯于每改一个参数跑一遍固定视角,记录帧耗时,而不是凭肉眼感觉。肉眼很容易被画面欺骗,尤其是体积雾这种“氛围很足但帧率很低”的效果,一定要用数据说话。

再分享一个我经常用的测试方法:把 Environment 里的 volumetric_fog_enabled 做成一个可以在运行时切换的布尔值,先用这个开关对比视觉效果,再用 Profiler 对比帧耗时。这套 A/B 测试法能帮你快速判断“这个场景的体积雾提升是否值得那百分之三五十的帧率开销”,省得在参数里来回试错。

5. 常见问题与排查技巧实录

5.1 雾是黑的,或者完全看不见

遇到这种情况,按顺序检查四件事基本能解决。第一,Environment 里的 Volumetric Fog 开关是否打开了;第二,场景里是否有光源,方向光或点光都行,完全依赖环境光的场景体积雾很容易黑;第三,gi_inject 是否被设成 0,暗室场景如果把间接光注入关掉,雾当然没有亮度来源;第四,density 或者 FogVolume 的 density 是不是太低,0.01 和 0.05 在视觉上的差别已经非常明显。

还有一个隐蔽原因:相机的 far 裁剪值小于 volumetric_fog_length。相机远端小于 64 米时,体积雾会在裁剪面附近直接消失,看起来就好像雾没生效,其实只是超出了可视范围。把相机 far 调大,或者把 length 设得和相机 far 匹配,问题立刻消失。

5.2 画面里有颗粒感、马赛克和断层

这个问题的根源几乎都集中在性能三件套上。froxil_size 太小会让深度方向的采样太稀疏,雾会在某个距离上突然跳变,形成断层;voxel_size 太大则让雾在较大的空间块内颜色均匀,边界处看起来像马赛克。如果你已经用均衡档(64 / 0.5 / 48)还是看到颗粒,那么多半是场景里的 FogVolume 形状太薄、太小,边缘刚好切在体素边界上。

处理方法也直接:先把 voxel_size 降到 0.25,看颗粒是不是立刻消失;如果消失,说明是精度不足,再试着把 froxil_size 升到 128。如果颗粒固定出现在某个小区域,检查那附近是否有 FogVolume,把它范围稍微放大一点,或者先关掉 animated,排除噪声采样在低精度体素里产生的额外干扰。

5.3 远处雾糊成一片,失去层次

这种问题在开放场景里很常见。根本原因是 length 设得太大,远处所有格子都堆满了雾,密度被拉满,远处的物体轮廓自然就糊了。正确处理是缩短 length,而不是降低全局 density。另外 sky_affects 太高也会让远处的雾直接采集成天空颜色,形成一整片死白,这时候可以试试把 sky_affects 降到 0.6 左右,恢复远景的层次。

5.4 体积雾和透明物体渲染顺序冲突

体积雾是后处理阶段叠加上去的,半透明物体(粒子、玻璃、水面)的渲染排序和雾并不是完全一致的。实际表现常见两种:粒子被雾完全盖住,看不出雾中透亮的细节;玻璃后面的雾看起来像在玻璃前面。这是引擎里做体积雾的已知限制,不是参数设错了。我的习惯是让重要的半透明特效尽量离相机近一点,或者对玻璃这类物体重新考虑是否一定要用透明材质——不少场景里用不透明材质配合贴图模拟透明,反而能避开这个问题。

5.5 FogVolume 动画效果闪烁

animated 体积雾偶尔会出现类似水面反射的高频闪烁,通常是因为 animation_speed 设得太高、detail_spread 设得太小,导致噪声函数在体素尺度上变化过快。体素网格本来就是离散采样的,高频噪声很容易在相邻帧之间跳动。解决方法是把 animation_speed 降到 0.3 以下,或者调大 detail_spread,让噪声变化更平滑。如果画面仍然闪烁,还可以考虑加大 voxel_size,用更粗的体素换掉高频抖动。

最后分享一个我自己几次实战下来的体会。体积雾这个效果的上限确实高,但坑也比普通后处理多得多。官方 demo 是一个很好的起点,可是如果你只是在它基础上改几个数值,不理解 Froxel 的工作机制,遇到问题一样会懵。我的建议是:抽出半小时,把 Environment 里那排 volumetric_fog 属性每个都故意调到极端值看看画面变化,等你对每个参数的作用有了肌肉记忆,再回来搭自己的场景,思路会开阔很多。这种“先做破坏性实验,再认真设计参数”的方式,在渲染相关功能上真的非常管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询