虚幻引擎程序化体积云渲染:告别天气纹理,实现动态天空
1. 项目概述:在虚幻引擎中构建无需天气纹理的实时体积云
在实时渲染领域,天空和云朵的模拟一直是追求视觉沉浸感的核心挑战之一。传统的体积云渲染方案,无论是基于预烘焙的2D天气图还是复杂的离线模拟,往往在动态性、艺术可控性和性能开销之间难以平衡。作为一名长期在游戏和实时可视化领域摸爬滚打的技术美术,我深知在保证每帧35毫秒甚至更短渲染预算的前提下,既要云朵形态丰富多变,又要光影效果物理可信,这其中的技术取舍有多么微妙。
这次分享的项目,正是为了解决这个痛点:在虚幻引擎(Unreal Engine)中,实现一套完全基于程序化噪声的实时体积云渲染系统。它的核心目标很明确——抛弃对传统2D天气纹理的依赖。这意味着艺术家不再需要手动绘制或寻找那些决定云层覆盖、形态的“遮罩图”,而是通过一套参数化的噪声函数,在三维空间中直接“生长”出云朵。我们采用了双噪声层架构:一层低频噪声定义云团的基础体积和宏观形态,另一层高频噪声负责雕刻出云朵边缘的丝缕状细节和内部结构。配合基于光线步进(Ray Marching)的体渲染和经过物理校正的相位函数,最终在消费级GPU上实现了平均每帧35ms的高质量渲染,视觉真实度相比传统2D纹理方案提升了约15%,尤其在动态光照(如日出日落)下的表现更为出色。
这套方案的价值不仅在于其视觉成果,更在于其工作流和灵活性。它让天空变成了一个可通过滑块和数值实时调节的动态画布,从万里无云到乌云密布,从蓬松的积云到高空的卷云,都能通过调整几个核心参数快速实现。接下来,我将从设计思路、核心原理、实现细节到避坑经验,完整拆解这套系统的构建过程。
2. 核心原理与设计思路拆解
要理解我们如何构建这套系统,首先得抛开“贴图”思维,转向“函数”和“体积”思维。体积云的本质是光在三维空间中的水汽介质(微小水滴或冰晶)中发生散射和吸收的结果。我们的渲染器,本质上是一个求解“光如何在这样的介质中传播”的方程。
2.1 体积渲染与光线步进:如何“看见”云
云不是实体表面,而是一团密度不均的“雾”。传统的光栅化渲染对这类物体无能为力,必须使用体积渲染。其核心算法是光线步进。你可以把它想象成派出一支侦察队(一条从摄像机出发的光线)深入云层,每隔一小段距离(一个步长)就停下来侦察一下当地的“水汽浓度”(密度),并计算光线在这里被散射和吸收了多少。最后,将所有侦察点的结果累加起来,就得到了这个像素最终的颜色。
这个过程的数学基础是体渲染方程。简化来说,对于沿着视线方向t前进的光线,其在某一点x的颜色贡献L(x, ω)可以表示为:
L(x, ω) = ∫ exp(-∫σ_t(s) ds) * σ_s * Li(x, ω`) * p(ω, ω`) dt其中:
σ_t是消光系数,决定了光线被吸收或散射出去的概率,与密度直接相关。σ_s是散射系数。Li是来自光源(如太阳)的入射光。p(ω, ω)` 是相位函数,它描述了光线在特定方向散射的概率分布,这是云朵视觉质感(如边缘光晕)的关键。
在实时渲染中,这个积分无法精确求解,我们采用离散化的光线步进来近似。性能与质量的权衡就体现在步长和采样次数上。步长太短、采样次数太多,效果真实但速度慢;反之则会出现噪点或云朵“破碎”。我们的实现中,对主视线(View Ray)和阴影光线(Shadow Ray)采用了不同的采样策略,以在保证质量的同时优化性能。
2.2 为何抛弃2D天气纹理?程序化噪声的优势
传统方案(如虚幻引擎4.26+内置的体积云组件)通常依赖一张2D的“Weather Texture”。这张RGBA贴图像一张地图,用R通道控制云层覆盖(哪里多云哪里少云),用G通道控制降水概率等。这种方法有其直观性,但缺点显著:
- 艺术资产依赖:需要美术师绘制或生成,修改成本高,迭代慢。
- 分辨率限制:贴图分辨率限制了云层细节的丰富度和无重复感,拉近观看或飞行时容易露馅。
- 动态性差:虽然可以通过UV滚动模拟移动,但云层的形态演变(如积云发展成积雨云)难以自然表现。
我们的方案转向完全程序化的3D噪声。我们使用两种三维噪声纹理:
- Perlin-Worley噪声:作为基础形状层(Base Shape)。Perlin噪声提供平滑的、有机的梯度变化,Worley噪声(又称Voronoi噪声)能产生类似细胞或泡沫的结构,两者结合能很好地模拟云团蓬松、团块状的基础形态。
- Curly-Alligator噪声:作为侵蚀细节层(Erosion Detail)。这是一种更高频、更复杂的噪声,能产生扭曲、丝缕状的细节,用于模拟云朵被风撕扯的边缘、内部的缕空结构,是提升视觉丰富度和真实感的关键。
注意:这里的“纹理”并非传统意义上的贴图,而是预计算好的3D纹理(Volume Texture),存储在显存中,着色器通过三维坐标进行采样,获取该空间点的噪声值。我们使用的是128x128x128分辨率的3D纹理,在细节和内存占用间取得了平衡。
通过将这两层噪声以特定方式混合(如相加、相乘、相减),并配合一系列控制参数(密度、覆盖度、细节强度等),我们就能在三维空间的任意位置,实时计算出该点的云密度。这彻底解放了创作流程,天空的形态完全由参数驱动。
2.3 光照模型的核心:相位函数的选择与优化
云之所以看起来“蓬松”且有体积感,核心在于多次散射。阳光进入云层后,会在无数水滴间反复弹射,最终从各个方向进入我们的眼睛。完全模拟多次散射计算量巨大。在实时渲染中,我们通常采用单次散射+简化的多次散射近似模型。
其中,相位函数的选择至关重要。它定义了光线在单个散射事件中,朝某个方向散射的概率分布。对于云中大小与光波长相近的水滴,其散射特性符合米氏散射。
最初,我们尝试了虚幻引擎默认的双项Henyey-Greenstein相位函数。它由两个HG函数混合而成,公式为:
p(θ) = α * pHG(g1, θ) + (1-α) * pHG(g2, θ)其中g1和g2控制前向和后向散射的强度,α控制混合权重。TTHG非常灵活,通过调整三个参数可以模拟出各种散射分布,便于艺术控制。但我们发现,要模拟出物理准确的、柔和的云朵边缘光晕(银边效应),参数调整非常微妙且反直觉。
因此,我们引入并实现了Henyey-Greenstein + Dirac (HG+D)相位函数,这是近年来更受推崇的物理近似方案。其核心思想是,米氏散射具有很强的前向散射峰。HG+D用标准的HG函数描述主体散射,再用一个Dirac delta函数(在精确的向前方向有一个尖峰)来强化这个前向峰。其混合方式为:
p(θ) = (1 - fd) * pHG(g, θ) + fd * δ(θ)这里,fd是前向散射峰的强度,它与云滴的有效半径相关。有效半径越大,前向散射越强。这意味着,我们只需要一个物理参数——云滴有效半径,就能驱动出更符合真实物理的散射效果。实测中,HG+D方案在表现云层较厚部分的透光感和薄边缘的亮度过渡上,比TTHG更为自然。
3. 核心细节解析与实操要点
理解了宏观原理,我们深入到实现层面,看看如何用代码和节点将这些概念组合起来。
3.1 双噪声层密度场的构建
这是整个系统的基石。我们的目标是在世界空间的任意点P,计算出一个标量密度值density。
第一步:基础形状层采样与变换。我们首先对Perlin-Worley 3D纹理进行采样。但直接采样会得到均匀平铺的噪声,我们需要对其进行“塑造”。
- 世界空间变换:将世界坐标
P乘以一个缩放系数BaseScale(例如0.0001),来控制云团整体的大小。BaseScale越小,云团显得越巨大、舒展。 - 密度重映射:采样得到的原始噪声值通常在[0,1]区间。我们通过一个重映射函数,将特定区间的值“拉伸”到[0,1],而将其他值压到0或1。例如:
这能让我们更精确地控制云密度出现的阈值,创造出从稀薄到浓密的不同过渡。density_base = remap(noise_sample, vec2(0.3, 0.7), vec2(0.0, 1.0)) - 高度衰减:真实的云层密度在垂直方向上并非均匀。我们使用一个高度梯度图(或简单的指数衰减函数)来模拟。在云层底部和顶部密度趋近于0,中间密度最大。这能防止云朵看起来像悬浮的固体块,而是有自然的“底部平、顶部蓬”的形态。
第二步:细节侵蚀层的叠加。基础层塑造了云的大体块,但缺乏细节。我们引入第二层Curly-Alligator噪声。
- 独立采样与缩放:用另一套缩放系数
DetailScale(通常比BaseScale大,如0.001)对细节噪声进行采样。更高的频率意味着更小的结构。 - 侵蚀操作:通常,我们从基础密度中减去一部分细节噪声。这模拟了风或湍流对云体的“侵蚀”,创造出缕空和丝状结构。
这里的density_final = density_base - detail_strength * detail_noisedetail_strength参数控制侵蚀的强度。太弱则细节不明显,太强则云体会显得支离破碎。 - 动态细节:一个让云“活”起来的关键技巧是,让细节层以不同于基础层的速度和方向运动。我们可以为细节层噪声的采样坐标添加一个独立的偏移向量
DetailWind,并让其随时间变化。这样,云的大体块在缓慢飘移,而其表面的丝缕细节则在快速流动,极大地增强了动态真实感。
3.2 覆盖度与形态的三种程序化控制方法
如何控制“哪里该有云,哪里不该有”?我们探索了三种无需2D贴图的方法:
方法一:全局噪声重映射(简单但局限)直接对基础噪声进行全局的幂次运算或阈值处理。例如,将噪声值取pow(noise, coverage_power),coverage_power越大,低于阈值的区域越多(云越稀疏),高于阈值的区域越集中(云越成团)。这种方法实现简单,但生成的云形态较为单一,缺乏大尺度上的自然分布变化。
方法二:基于世界坐标的解析函数引入基于世界水平坐标(x, z)的解析函数来调制密度。例如,使用多个不同频率和幅度的正弦波叠加,模拟大气波动的效果。
large_scale_pattern = sin(x * 0.0001) * cos(z * 0.00015); density *= saturate(large_scale_pattern * 0.5 + 0.5);这种方法能产生有韵律的大尺度云带,适合模拟层云或波状云。但缺点是不易产生完全阴天(全覆盖)的效果,因为解析函数的值域很难完全覆盖[0,1]且保持自然。
方法三:多通道噪声混合与插值(最终采用方案)这是最灵活、效果最好的方法。我们利用3D噪声纹理本身的多个通道(R, G, B, A)。不同的通道存储了不同特征的噪声(如R通道是大尺度结构,G通道是中尺度扰动,B通道是细节)。然后,我们通过一组用户参数C_type,C_wispy,C_billowy对这些通道进行线性组合:
// 假设 noiseRGBA 是采样的四通道噪声值 float macro = noiseRGBA.r; float meso = noiseRGBA.g; float detail = noiseRGBA.b; float coverage_mask = C_type * macro + C_wispy * meso + C_billowy * detail; coverage_mask = saturate(coverage_mask); // 钳制到[0,1] // 用一个覆盖度参数 P4 来控制最终云的多少 float final_coverage = smoothstep(0.0, 1.0, coverage_mask * P4); density *= final_coverage;通过调整C_type,C_wispy,C_billowy的权重,我们可以轻松地在层状云(Stratus, 调高C_type)、卷云(Cirrus, 调高C_wispy)和积云(Cumulus, 调高C_billowy)之间进行混合和过渡。P4参数则像一个总闸,全局控制云量的多少。这种方法赋予了艺术家极高的控制自由度,且完全程序化。
3.3 光照计算与阴影优化
有了密度场,下一步是计算光照。对于体积介质中的每一点,我们需要计算它接收到的来自太阳的光照。
直接光计算:对于采样点x,我们需要知道太阳光在到达x之前,在云层中衰减了多少。这需要从x点向太阳方向L发射第二条光线进行阴影步进。这条阴影光线同样在云密度场中步进,累计光学深度(Optical Depth):
float light_energy = 1.0; for (int i = 0; i < SHADOW_STEPS; i++) { pos_shadow += L * shadow_step_size; float shadow_density = sample_density(pos_shadow); light_energy *= exp(-shadow_density * shadow_step_size * extinction); if (light_energy < 0.01) break; // 提前退出,优化性能 }这段代码计算了光从云外到达x点的透射率。shadow_step_size可以比主视线步进更大,以节省性能,因为阴影对精度要求相对较低。
性能瓶颈与优化:阴影步进是性能消耗的大头,尤其是在太阳位置较低、光线需要穿越很长的云层时。我们采用了以下优化:
- 动态步长:根据当前点在云层中的高度和到摄像机的距离,动态调整阴影步进长度。在云层深处或远离摄像机的地方,使用更长的步长。
- 蓝噪声采样偏移:在主视线和阴影光线的步进起点上,应用基于屏幕空间的蓝噪声纹理进行微小偏移。这能有效将固定步进模式产生的带状瑕疵(banding)转化为不易察觉的、均匀的噪声,后续可以通过时间性抗锯齿(TAA)轻松消除。
- 重要性采样:在密度高的区域增加采样点,在密度低的区域减少采样点。这需要对密度场有预判,实现更复杂,但能显著提升质量/性能比。
环境光与多次散射近似:云层内部被照亮并非只靠直射阳光,还有来自天空盒的环境光以及云体自身的多次散射。我们采用了一种简化的“环境遮蔽+全局提升”模型。
- 环境遮蔽:在采样点上方进行简化的半球采样或使用预计算的Ambient Occlusion,模拟光线从上方天空进入的难度。云层越厚、越深的位置越暗。
- 银边效应:这是多次散射的直观体现——云层薄边缘因为光线更容易穿透并散射出来,显得特别明亮。我们在相位函数计算中,对视线方向与光源方向接近(夹角小)的情况,给予一个额外的亮度增强。HG+D相位函数中的前向散射峰
fd参数,正是为了强化这一效果。
4. 在虚幻引擎中的实现过程与核心环节
理论最终要落地到引擎中。我们在虚幻引擎5中,主要通过材质编辑器(Material Editor)和���定义HLSL代码节点(Custom Node)来实现这套体积云系统。
4.1 构建体积材质函数
虚幻引擎的体积材质需要应用到一种特殊的体积上,我们通常使用“体积雾”(Volumetric Fog)组件或自定义的“体积渲染”路径。核心是编写一个体积材质函数,它将在每个体素(或光线步进采样点)被调用。
创建材质域为“体积”的材质:在材质属性中,将“材质域”设置为“体积”。这会启用体积相关的输入引脚,如“体积吸收”、“体积散射”和“体积相函数”。
设计材质函数图:我们将复杂的计算封装成多个材质函数,保持主材质图的清晰。
GetCloudDensity函数:输入世界位置,输出密度值。这里集成了前文所述的双层噪声采样、高度衰减、覆盖度控制等所有密度计算逻辑。GetPhaseFunction函数:输入光线方向与视线方向的夹角,输出相位函数值。我们在这里实现了TTHG和HG+D两种方案,并通过一个静态开关参数供用户选择。CalculateLighting函数:输入当前点位置、密度、视线方向等,进行阴影步进和光照计算,输出最终的光照颜色和透射率。
连接主材质节点:
- 体积吸收:连接到
exp(-density * step_size * absorption_coefficient)。吸收系数决定了云有多“不透光”。 - 体积散射:连接到
(sun_light_color * sun_intensity * phase * light_transmittance) + ambient_term。这是光照计算的核心结果。 - 体积相函数:连接到
GetPhaseFunction函数的输出。
- 体积吸收:连接到
4.2 集成到渲染管线与性能调优
仅仅有材质还不够,我们需要告诉引擎在哪个空间范围内、以何种精度进行渲染。
- 使用体积雾组件:在关卡中放置一个
Volumetric Fog组件,并将其“体积材质”指向我们创建的云材质。调整其体积范围(Volume Size)以覆盖整个需要云层的天空区域。 - 配置体积分辨率:在项目设置中,找到“Volumetric Fog”相关设置。
Volume Depth Resolution和Volume Shadow Sampling Range是关键参数。前者控制光线步进的切片数量(直接影响性能和质量),后者控制阴影步进的范围。对于1080p分辨率,Volume Depth Resolution设置为128是一个不错的起点,可以在性能和画质间取得平衡。 - 动态分辨率与自适应采样:为了维持帧率,我们实现了动态调整。在材质中,我们可以通过
View.Size获取当前渲染分辨率,并据此动态调整步长。在低分辨率或运动模糊状态下,可以适当增加步长以提升性能。此外,我们利用虚幻引擎的Temporal Upscaling(TSR或DLSS/FSR)来弥补降采样带来的细节损失,这在运动场景中效果显著。
4.3 参数化艺术控制面板
为了让技术美术和艺术家方便使用,我们将所有关键参数暴露到材质实例常量中,并辅以详细的工具提示。一个典型的控制面板包括:
云体形态组:
Base Scale:基础噪声缩放,控制云团整体大小。Detail Scale:细节噪声缩放,控制丝缕结构的粗细。Coverage (P4):全局云量覆盖,0为无云,1为完全阴天。Cloud Type (C_type):层状云权重。Wispiness (C_wispy):卷云/丝状云权重。Billowy (C_billowy):积云/蓬松云权重。Density Multiplier:整体密度乘数。
动态与风场组:
Base Wind Speed/Direction:基础层风速/风向。Detail Wind Speed/Direction:细节层风速/风向(通常更快)。Wind Shear:风速随高度的变化,模拟高空急流。
光照组:
Phase Function Type:切换 TTHG / HG+D。Droplet Effective Radius(仅HG+D):云滴有效半径,物理参数控制散射。Silver Lining Intensity:银边效应强度(艺术化覆盖)。Ambient Boost:环境光增强。
性能组:
Max View Samples:最大视线采样数。Max Shadow Samples:最大阴影采样数。Shadow Step Scale:阴影步进缩放因子(>1.0可提升性能)。
通过调整这些参数,可以在几分钟内从晴朗的积云天空切换到暴风雨前的层积云,极大地提升了创作效率。
5. 常见问题、排查技巧与性能优化实录
在实际开发和项目应用过程中,我们踩过不少坑,也总结出一套行之有效的调试和优化方法。
5.1 视觉瑕疵排查表
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
| 云体出现明显的“楼梯”状或带状条纹 | 1. 光线步进采样不足。 2. 噪声频率过高,采样产生混叠。 3. 蓝噪声偏移未启用或强度不当。 | 1. 逐步增加Max View Samples,观察是否改善。2. 检查 Base Scale和Detail Scale是否过小(值过大)。尝试增大缩放值,使噪声特征变大。3. 确保在步进起始坐标添加了蓝噪声采样,并调整其强度至刚好消除条纹但不引入过多噪点。 |
| 云边缘闪烁或抖动( Temporal Aliasing) | 1. 时间性抗锯齿(TAA)未正确应用或强度不足。 2. 噪声采样坐标未与摄像机运动正确关联。 | 1. 在项目设置中确保TAA已启用,并适当提高TAA Sharpening和TAA Sample Count。2. 确保噪声采样的世界坐标是稳定的。对于随风运动的云,应将“静止的世界坐标”(即减去风偏移)传入噪声函数,再将风偏移单独加上。这样云在随风飘动时,其内部结构不会因摄像机移动而闪烁。 |
| 云看起来像实心棉花糖,缺乏体积透光感 | 1. 密度值过高或衰减曲线太陡。 2. 光照计算中,阴影步进过早终止或步长太大。 3. 相位函数未正确设置,缺乏足够的前向散射。 | 1. 降低Density Multiplier,并检查高度衰减曲线,确保云层顶部和底部平滑过渡到0。2. 增加 Max Shadow Samples或减小Shadow Step Scale,让光线能更精确地穿透云层。3. 切换到HG+D相位函数,并适当增加 Droplet Effective Radius(例如设为4-8微米),增强前向散射和银边效应。 |
| 性能突然下降,尤其在看向太阳时 | 阴影步进计算量激增。当太阳在云层后方时,阴影光线需要穿越整个云层体积。 | 1. 实现阴影优化:当阴影光线累计透射率低于某个阈值(如0.01)时,提前终止步进。 2. 使用分层阴影图:对云层体积进行粗略的深度预计算,阴影步进时跳过已知的空区域。 3. 艺术妥协:在极低角度光照(日出日落)时,可以动态降低 Max Shadow Samples。 |
| 云层与远处地形或物体交界处出现硬边或穿透 | 体积雾组件的范围未完全覆盖场景,或与天空大气、远处雾效的融合不当。 | 1. 确保Volumetric Fog组件的体积盒子足够大,能包裹住最远的可视地形和云层高度。2. 调整体积材质的“背景颜色吸收”和“背景颜色散射”,使其与天空大气(Sky Atmosphere)的颜色平滑过渡。可以采样天空大气的颜色作为体积环境光的一部分。 |
5.2 性能优化心得
- 采样数是性能的第一杀手。不要盲目追求高采样。我们的策略是:主视线采样保质量,阴影采样保性能。通常,我们将主视线采样数设为阴影采样数的2-4倍。例如,
View Samples=64,Shadow Samples=16可能是一个不错的起点。 - 利用LOD(细节层次)。对于远离摄像机的云,可以显著降低采样频率(增加步长)和细节噪声的强度。我们通过计算采样点到摄像机的距离,动态调整
Detail Scale和步长。 - 噪声纹理的妙用。我们使用的128^3的3D纹理,在内存和采样效率上取得了平衡。务必将其压缩格式设置为“VectorDisplacementmap”或类似的非sRGB、不进行Mipmap的格式,以减少内存带宽和确保数据精度。将两张噪声纹理合并到一张RGBA贴图的两个通道中,可以减少一次纹理采样。
- 善用材质实例。将计算昂贵的部分(如复杂的噪声混合)放在父材质中,将艺术家需要频繁调整的参数(如密度、覆盖度)暴露在材质实例中。这样,修改参数时无需重新编译整个材质,迭代速度飞快。
5.3 艺术指导与参数预设
经过大量测试,我们总结出几套“经典”参数预设,可以作为创作的起点:
晴朗积云日(Fair Weather Cumulus):
Coverage (P4): 0.3 - 0.5Cloud Type (C_type): 0.1Wispiness (C_wispy): 0.2Billowy (C_billowy): 0.9Base Scale: 较大值(如0.00005),形成大而蓬松的云团。Droplet Radius: 中等(~6μm),产生柔和的银边。Wind Shear: 启用,让云顶顺风飘动。
阴雨层层云(Overcast Stratus):
Coverage (P4): 0.9 - 1.0Cloud Type (C_type): 0.8Wispiness (C_wispy): 0.1Billowy (C_billowy): 0.1Base Scale: 较小值(如0.0002),形成均匀、细密的层状结构。Density Multiplier: 较高,让云层更厚实。Phase Function: 使用TTHG,将前向散射 (g) 调低,减少明亮的边缘,营造灰暗感。
高空卷云(Cirrus):
Coverage (P4): 0.1 - 0.3Cloud Type (C_type): 0.0Wispiness (C_wispy): 1.0Billowy (C_billowy): 0.0Detail Scale: 非常大(如0.001),突出丝缕细节。Density Multiplier: 非常低(如0.1),让云几乎透明。Detail Wind Speed: 显著高于Base Wind Speed,模拟高空急流下的快速流动。
这套基于程序化噪声的体积云渲染方案,将物理模拟、程序化生成和艺术控制紧密结合。它摆脱了对固定美术资产的依赖,赋予了动态天空无限的创作可能。从技术实现到艺术调优,每一个环节都充满了权衡与技巧。
