Unity风格化云朵Shader实现:从噪声叠加到动态天气系统
1. 项目概述:为什么我们需要一个风格化的云朵Shader?
在Unity项目里,尤其是那些追求独特视觉风格的游戏或交互应用中,天空盒往往是奠定整体氛围基调的关键。一个写实的天空盒固然不错,但对于卡通渲染、低多边形风格、或是带有强烈艺术感的独立游戏来说,写实的云朵反而会显得格格不入。这就是“风格化渲染”的价值所在——它不追求物理上的绝对真实,而是追求艺术表达上的和谐与美感。
“UnlitClouds”这个项目,从名字就能看出它的核心:一个不依赖复杂光照计算的、专注于表现风格化云朵的Shader。对于很多开发者,特别是独立开发者或技术美术(TA)新手来说,这是一个极具吸引力的起点。市面上的云渲染方案,无论是基于体积雾(Volumetric Fog)的实时体积云,还是使用3D噪声函数模拟的复杂Shader,其性能和实现门槛都相当高。而UnlitClouds的思路则非常直接:它通常基于片元着色器(Fragment Shader),利用多层2D噪声纹理(如Perlin Noise或Worley Noise)进行叠加、扭曲和混合,通过简单的颜色和透明度控制,在屏幕空间或模型表面(比如一个面片或球体)渲染出具有动态效果的风格化云层。
它的核心价值在于轻量、可控、易定制。你不需要理解复杂的体积光照散射(Rayleigh/Mie Scattering)模型,也不用担心性能被拖垮。通过调整几个直观的参数——比如噪声的缩放(Scale)、滚动速度(Speed)、颜色渐变(Gradient)和边缘柔和度——你就能快速创造出从《塞尔达传说:旷野之息》那种清新的卡通云朵,到带有蒸汽波(Vaporwave)美学色彩的梦幻云霞。这对于快速原型开发、风格化项目,甚至是作为更复杂体积云效果的性能后备方案(如移动端),都极具实用意义。
2. 核心思路与方案设计:拆解一个风格化云Shader的骨架
要理解UnlitClouds,我们可以把它想象成在画布上“画”云。这个画布可以是一个覆盖整个天空的球体(Skybox),也可以是场景中漂浮的多个面片(Billboard)。Shader的工作,就是告诉GPU如何在这个画布上,通过计算生成云朵的图案。
2.1 核心算法:多层噪声叠加
这是所有程序化云朵(包括风格化)的基石。单一层的噪声纹理看起来过于规整和“假”。真实的云(即使是风格化的)其边缘应该是破碎、不规则且有层次感的。
- 基础噪声层:使用一张或多次采样Perlin噪声纹理,定义云朵大致的形状和分布。通过调整UV的缩放(
_CloudScale),可以控制云朵的尺寸,值越小,云朵显得越庞大、稀疏;值越大,云朵越细碎、密集。 - 细节噪声层:使用另一张缩放比例更大(即更“高频”)的噪声纹理,叠加在基础层之上。这一层通常会和基础层进行某种形式的混合(如相乘
multiply或屏幕混合screen),用于添加云朵内部的细节和纹理,让云看起来不那么“平”。 - 扭曲与动态:为了让云“动”起来,我们需要让噪声“流动”。最常见的方法是让噪声纹理的UV坐标随时间(
_Time.y)偏移。更高级的做法是使用一张流动图(Flow Map)或另一层低频噪声来扭曲高频噪声的UV坐标,模拟云层被风吹拂时那种缓慢变形、流动的感觉,这比简单的平移要自然得多。
注意:噪声纹理的选择至关重要。Unity自带的
Noise/Perlin 2D节点在Shader Graph中很方便,但有时自定义的Worley噪声(细胞噪声)能产生更符合某些艺术风格的、块状或絮状的云朵形态。在实际项目中,我通常会准备几套不同的噪声纹理组合,根据项目风格进行A/B测试。
2.2 着色与透明度:塑造云的“体感”
云是半透明的,而且不同部位的厚度不同,透光性也不同。在Unlit(无光照)模型中,我们通过颜色和透明度(Alpha)来模拟这种体感。
- 基于噪声值的遮罩:我们将多层混合后的最终噪声值,视为一个“密度”图。值越高的地方,代表云越“厚”越“实”;值越低的地方,代表云越“薄”越“透”,甚至完全消失。
- 重映射与平滑:原始的噪声值范围通常在[0,1]。我们可以通过一个
Remap节点,将其映射到新的范围,比如[0.2, 0.8]。然后,通过一个Smoothstep函数来处理这个值。Smoothstep是风格化渲染的“魔法函数”之一,它能将输入值进行平滑的阶梯化处理。例如,Smoothstep(_EdgeSoftness, 1.0, noiseValue)。这里,_EdgeSoftness控制着从“无云”到“有云”过渡区域的柔和程度。值越小,云朵边缘越锐利;值越大,云朵边缘越模糊、越蓬松。这个操作直接决定了云朵的视觉风格是偏向硬朗的卡通剪影,还是柔软的棉花糖。 - 颜色与透明度输出:
- 颜色(Color):通常不是简单的纯白色。我们可以根据噪声值(或一个独立的时间变量)来采样一个颜色渐变纹理(
Gradient)。例如,让云朵底部颜色偏灰蓝(模拟阴影或环境光遮蔽),顶部偏亮白。对于夕阳场景,可以采样从橙红到紫红的渐变。 - 透明度(Alpha):经过
Smoothstep处理后的噪声值,直接作为透明度输出。值高的地方不透明,值低的地方透明。为了实现更逼真的半透明边缘,有时会将这个值进行幂运算(Power),让透明度变化更非线性。
- 颜色(Color):通常不是简单的纯白色。我们可以根据噪声值(或一个独立的时间变量)来采样一个颜色渐变纹理(
2.3 方案选型:Shader Graph vs 手写HLSL
这是实现UnlitClouds时面临的首要选择。
Shader Graph(可视化着色器编辑器):
- 优势:直观、快速迭代、无需深厚Shader编程基础。节点式的操作让参数调整和逻辑可视化,非常适合技术美术和初学者快速搭建和调试效果。Unity官方支持良好,社区资源丰富。
- 劣势:生成的代码可能不够优化,对于极其复杂的逻辑(如循环、自定义函数)支持有限,调试深层问题不如代码直接。
- 适用场景:快速原型、风格相对固定、团队中TA主导视觉效果的项目。对于UnlitClouds这种逻辑相对线性的Shader,Shader Graph完全能够胜任,且效率很高。
手写HLSL/CG代码:
- 优势:性能控制精准,灵活性极高,可以实现任何复杂算法。便于代码复用、模块化管理,适合构建自己的Shader库。
- 劣势:学习曲线陡峭,调试困难,迭代速度慢。
- 适用场景:对性能有极致要求(如移动端)、需要高度定制化算法、或作为团队核心渲染管线的一部分。
我的建议:对于大多数寻求风格化云朵解决方案的开发者,从Shader Graph开始是绝佳选择。你可以先通过节点连接快速实现核心效果,理解数据流动。如果后期发现性能瓶颈或需要更复杂功能,再将其中关键部分用Custom Function节点替换为手写HLSL代码。UnlitClouds项目本身,很可能就是一个用Shader Graph构建的、参数暴露完善的.unitypackage或示例场景,开箱即用。
3. 关键参数深度解析与实操调优
拿到一个UnlitClouds Shader后,面对一堆参数可能会不知所措。下面我以常见的参数为例,拆解它们的具体影响和联动调节技巧。
3.1 控制云朵形态的核心参数
| 参数名(示例) | 功能描述 | 调节技巧与视觉影响 |
|---|---|---|
_BaseScale/_DetailScale | 控制基础噪声层和细节噪声层的UV缩放。 | _BaseScale:这是云朵的“大局观”。调小(如0.1),云朵巨大、绵延,适合远景天空盒;调大(如1.0),云朵细碎、密集,适合近景云团。_DetailScale:通常设为_BaseScale的2-4倍,用于添加小尺度细节。两者差异越大,云朵的层次感越强。 |
_CloudSpeed | 控制噪声UV偏移的速度,制造云层飘动动画。 | 通常是一个二维向量(Vector2),对应UV的X和Y方向。值很小,如(0.01, 0.005)。技巧:让X和Y速度略有不同,避免过于机械的直线运动。可以连接一个基于时间的正弦波到速度上,制造风速变化的感觉。 |
_DensityThreshold | 密度阈值。低于此值的噪声区域将被视为“无云”(完全透明)。 | 这是控制云量多少的最直接参数。提高阈值,云朵变少、变稀疏;降低阈值,云朵变多、连成一片。通常配合_EdgeSoftness一起调节。 |
_EdgeSoftness | 控制云朵边缘的柔和度/羽化程度。 | 这是风格化的关键!设为0附近,云朵边缘锐利,像剪纸或卡通画;设为0.3以上,边缘非常柔和,像水彩或雾气。实操心得:在Shader Graph中,这个值通常作为Smoothstep函数的第一个输入参数。你可以将其暴露为一个Slider(0, 0.5)。 |
3.2 控制颜色与外观的参数
| 参数名(示例) | 功能描述 | 调节技巧与视觉影响 |
|---|---|---|
_ColorGradient | 一个渐变纹理(Texture2D)或Gradient资产,用于根据云密度映射颜色。 | 这是实现艺术风格化的利器。例如: 1.卡通风格:使用2-3个色阶的硬过渡渐变。 2.梦幻风格:使用从淡紫到粉红再到亮黄的柔和渐变。 3.阴沉风格:使用从深灰到中灰的渐变。注意:在Shader Graph中采样Gradient Texture时,需要将噪声值(或重映射后的值)从[0,1]映射到UV的U方向(通常水平方向)。 |
_Brightness/_Contrast | 整体亮度和对比度调节。 | 在最终颜色输出前进行调节。提高对比度可以让云朵的亮部和暗部区分更明显,体感更强。这比单纯调节颜色倍增更有效。 |
_Opacity | 全局透明度乘数。 | 用于快速调整整个云层的淡入淡出效果,常用于场景切换或天气变化。它应乘在由噪声计算出的局部透明度之上。 |
3.3 高级效果:深度交融与天空盒适配
一个常见的需求是让云朵和场景中的物体(如山、建筑)产生正确的遮挡关系,即远处的云应该在山后面。
- 深度测试(Depth Test):在Shader的渲染设置中,确保深度测试(ZTest)设置为
LEqual(默认)。这样,云朵面片只有在比场景物体更近时才会被绘制。 - 深度交融(Depth Fade):这是实现云朵边缘与场景物体平滑融合的关键技巧。Shader Graph中有一个
Depth Fade节点。它的原理是,计算当前片元与深度缓冲区中对应位置场景物体的距离差。当云朵非常靠近一个物体表面时,让它的透明度逐渐降低为0,从而产生“融入”背景的效果,避免生硬的穿插。- 参数
_FadeDistance:控制交融的范围。值越大,云朵在物体周围“消散”的范围越广。 - 实操:将
Depth Fade节点的输出值(一个0到1的系数,1代表完全可见,0代表完全消融)与之前计算出的云朵透明度相乘,作为最终的Alpha输出。
- 参数
天空盒集成:如果你把UnlitClouds Shader用在天空盒材质上,需要注意天空盒默认是渲染在无限远处的,深度测试可能不按预期工作。一种更灵活的做法是:不直接修改天空盒材质,而是创建一个独立的、巨大的半球体或球体模型,贴上UnlitClouds材质,并放置在摄像机远裁剪面之内。这样可以更方便地控制云层的高度、旋转和动态效果,也更容易与场景中的其他雾效、后期处理(如调色)配合。
4. 在Unity中实现UnlitClouds的完整流程
假设我们使用Shader Graph来创建一个最基础的风格化云朵Shader,并应用到天空球体上。
4.1 资源准备与场景搭建
- 创建Shader Graph:在Project窗口中右键 -> Create -> Shader -> Universal Render Pipeline -> Unlit Shader Graph(如果你的项目是URP)。命名为
SG_UnlitClouds。 - 准备噪声纹理:你可以使用Shader Graph自带的Procedural Noise节点,但为了更好的控制和性能,我建议使用预烘焙的2D噪声纹理。可以去一些免费资源网站(如OpenGameArt)搜索“Perlin Noise Texture”或“Cloud Noise”,下载几张灰度图。导入Unity后,确保它们的Wrap Mode设置为
Repeat,Filter Mode设置为Bilinear或Trilinear,以便无缝平铺和平滑采样。 - 创建材质:右键刚才创建的Shader Graph -> Create -> Material。命名为
Mat_StylizedClouds。 - 创建天空球体:在Hierarchy中创建一个Sphere(球体),Scale放大到(500,500,500)左右,使其远大于你的场景。将其Mesh Renderer的材质指定为
Mat_StylizedClouds。关键步骤:由于球体内外都能看到,我们需要只渲染背面(否则会看到云在球体内侧)。创建一个新的Shader Graph,或修改现有Shader:- 在Graph Inspector中,找到
Surface Options->Render Face,设置为Back(只渲染背面)。 - 或者,更简单的方法:在材质球上,勾选
Double Sided(双面渲染),并确保球体法线朝外(默认就是)。
- 在Graph Inspector中,找到
4.2 Shader Graph节点搭建详解
打开SG_UnlitClouds。我们将构建一个基础的双层噪声云。
创建属性(Properties):
_BaseNoiseTex(Texture2D): 基础噪声纹理。_DetailNoiseTex(Texture2D): 细节噪声纹理。_BaseScale(Vector1): 默认值0.05。_DetailScale(Vector1): 默认值0.2。_CloudSpeed(Vector2): 默认值(0.01, 0.005)。_DensityThreshold(Vector1): Slider(0, 1),默认值0.2。_EdgeSoftness(Vector1): Slider(0, 0.5),默认值0.1。_ColorGradient(Texture2D): 一张1x256的渐变纹理。_Opacity(Vector1): Slider(0, 1),默认值1。
构建主图(Master Graph):
- UV与滚动:使用
Time节点获取_Time.y,与_CloudSpeed相乘,得到一个二维的偏移向量。将其与UV节点的输出相加,得到滚动的UV。分别乘以_BaseScale和_DetailScale,得到两套缩放后的UV。 - 采样噪声:用两套UV分别去采样
_BaseNoiseTex和_DetailNoiseTex,得到两个灰度值(R通道即可)。 - 噪声混合:将
_DetailNoise乘以一个较小的系数(如0.3),然后与_BaseNoise相加(Add节点)。这会让细节噪声作为扰动叠加到基础噪声上。也可以使用Multiply,效果更强烈。 - 密度与平滑:将混合后的噪声值输入到一个
Remap节点,从(0,1)重映射到(-0.5, 1.5)。这个范围可以手动调整,目的是为了后续的Smoothstep有更大的操作空间。然后将重映射后的值输入Smoothstep节点,第一个输入端口(Edge1)连接_DensityThreshold,第二个输入端口(Edge2)连接_DensityThreshold加上_EdgeSoftness。输出结果我们称为cloudMask,这是一个在[0,1]范围内的、边缘平滑的云遮罩。 - 颜色映射:将
cloudMask作为U坐标(通常需要乘以一个小的系数如0.99,避免采样到纹理边缘),V坐标设为0.5,去采样_ColorGradient纹理,得到颜色。 - 透明度计算:将
cloudMask与_Opacity相乘,得到最终的Alpha值。 - 连接主节点:将计算出的颜色连接到
Master Stack的Color端口,Alpha值连接到Alpha端口。在Master Stack的Surface配置中,将Blend Mode设置为Alpha(SrcAlpha,OneMinusSrcAlpha),这样云朵才能正确半透明。
- UV与滚动:使用
4.3 材质调试与场景适配
- 实时调试:将材质赋给天空球体后,在Scene视图或Game视图就能看到效果。选中材质球,在Inspector中实时拖动各个参数滑块,观察变化。这是Shader Graph最大的优势。
- 解决常见视觉问题:
- 云朵有接缝:检查噪声纹理的导入设置,Wrap Mode必须是
Repeat。检查UV计算中是否有不连续的操作。 - 云朵边缘闪烁(Z-fighting):这是因为云朵面片和远处背景(或天空盒)深度值过于接近。可以轻微地将球体向摄像机方向移动一点,或者为Shader添加一个很小的
Depth Offset(在Master Stack的Advanced设置中)。 - 云朵太“平”:尝试添加“视差(Parallax)”效果。简单做法是,根据视角向量和噪声值,轻微地偏移UV采样位置,模拟云层的体积感。但这会增加一些复杂度。
- 性能考虑:如果云层覆盖全屏且持续滚动,对移动端可能构成压力。可以考虑:1) 降低噪声纹理分辨率;2) 减少噪声层数(从两层减为一层);3) 在远处使用更低精度的LOD材质。
- 云朵有接缝:检查噪声纹理的导入设置,Wrap Mode必须是
5. 进阶技巧与性能优化实战
当基础效果满足后,我们可以通过一些技巧让云朵更生动,并确保它在不同平台上都能流畅运行。
5.1 实现动态天气与昼夜变化
一个静态的云层是乏味的。我们可以通过脚本动态控制Shader参数,来模拟天气变化。
// CloudController.cs using UnityEngine; public class CloudController : MonoBehaviour { public Material cloudMaterial; // 指向云朵材质 public float weatherChangeDuration = 10.0f; // 天气变化时长 private float targetDensity; private float currentDensity; private float timer; void Start() { if (cloudMaterial == null) cloudMaterial = GetComponent<MeshRenderer>().material; currentDensity = cloudMaterial.GetFloat("_DensityThreshold"); targetDensity = Random.Range(0.1f, 0.6f); // 随机一个目标云量 } void Update() { timer += Time.deltaTime; float lerpFactor = timer / weatherChangeDuration; // 平滑过渡云量 float newDensity = Mathf.Lerp(currentDensity, targetDensity, lerpFactor); cloudMaterial.SetFloat("_DensityThreshold", newDensity); // 也可以同时改变颜色渐变、速度等 // cloudMaterial.SetColor("_TintColor", Color.Lerp(dayColor, nightColor, lerpFactor)); if (lerpFactor >= 1.0f) { // 一次变化结束,设定新的目标 currentDensity = targetDensity; targetDensity = Random.Range(0.1f, 0.6f); timer = 0f; } } }将这个脚本挂载到天空球体上,并拖入材质引用。它会让云量在设定的范围内周期性随机变化,模拟云层聚散。
5.2 移动端与性能敏感平台优化
风格化云朵本身已经很轻量,但在低端设备上仍需精打细算。
- 纹理压缩与尺寸:将噪声纹理和渐变纹理的Max Size设置为512甚至256,压缩格式使用ASTC或ETC2(移动端)。对于渐变纹理,1x128或1x64的分辨率通常就足够了。
- 简化Shader:
- 减少纹理采样:尝试只用一层高质量的噪声纹理,通过不同的UV缩放和数学运算(如
Fract,Sin)衍生出细节。 - 降低计算精度:在Shader Graph的节点属性中,可以将某些计算节点的
Precision从Float改为Half,这在移动GPU上可能更快。 - 移除不必要的效果:如果深度交融(Depth Fade)不是必须的,可以移除
Scene Depth节点的采样,这是一个相对耗时的操作。
- 减少纹理采样:尝试只用一层高质量的噪声纹理,通过不同的UV缩放和数学运算(如
- 使用LOD(细节层次):为云朵材质创建两个变体,一个高质量(High Quality)用于PC/主机,一个低质量(Low Quality)用于移动端。可以通过脚本来根据平台或质量设置动态切换材质。
void AdjustCloudQuality() { Material targetMat; #if UNITY_IOS || UNITY_ANDROID targetMat = mobileCloudMaterial; // 预分配好的低配材质 #else targetMat = standardCloudMaterial; #endif GetComponent<MeshRenderer>().material = targetMat; } - 批处理与合批:如果你的场景中有多个独立的云朵面片(Billboard),确保它们使用相同的材质,并且材质上没有开启GPU Instancing禁止的属性(如不同的纹理)。这样可以促进动态批处理或GPU Instancing,大幅降低Draw Call。
5.3 与Unity渲染管线(URP/HDRP)的集成
- URP(通用渲染管线):上文创建的Unlit Shader Graph就是为URP设计的,兼容性最好。确保你的项目使用的是URP,并且在Graphics Settings中指定了正确的URP Asset。URP的后期处理(如Bloom, Color Grading)可以很好地增强云朵的视觉效果。
- HDRP(高清渲染管线):HDRP对Shader有更高要求。你需要创建HDRP Unlit Shader Graph。HDRP的优势在于可以更轻松地与体积雾、物理天空系统结合,实现更统一的视觉体验。但HDRP本身的性能开销更大,通常不用于移动端或风格化项目。
- 内置渲染管线(Built-in):虽然也可以用,但Shader Graph的支持不如URP/HDRP原生。你可能需要手动编写Surface Shader或Unlit Shader。对于风格化云朵,内置管线的简单Unlit Shader加上纹理动画,也是一个稳定高效的选择。
6. 常见问题排查与调试心得
在实际操作中,你肯定会遇到各种“坑”。这里记录了一些典型问题和我自己的解决思路。
6.1 云朵完全不显示或显示为纯色
- 检查渲染队列(Render Queue)和混合模式:这是最常见的原因。确保材质的渲染队列是
Transparent(3000),并且混合模式(Blend Mode)是Alpha Blend(SrcAlpha, OneMinusSrcAlpha)。如果设置为Opaque,透明度通道会被忽略。 - 检查Alpha输出:在Shader Graph中,确保最终连接到
Master Stack的Alpha端口的值不是常数0或1。用一个Split节点检查你的cloudMask值是否在合理范围(0到1之间)。可以临时将Color端口也连接到cloudMask(灰度显示),看看云朵形状是否正常,先排除颜色映射的问题。 - 检查模型和法线:确认你使用的模型(如球体)足够大,并且摄像机在模型内部(如果只渲染背面)。检查模型的Mesh Renderer组件是否被禁用,或材质球是否被错误覆盖。
6.2 云朵边缘闪烁或锯齿严重
- 深度冲突(Z-fighting):如前所述,调整物体的位置或使用
Depth Offset。在Shader Graph的Master Stack->Depth->Offset中,尝试将Factor和Unit设置为很小的正值(如0.01和0)。 - 纹理过滤与Mipmap:确保噪声纹理的导入设置中,
Generate Mip Maps是勾选的,并且Filter Mode不是Point(无过滤)。Bilinear或Trilinear可以提供更平滑的采样结果,减少移动时的锯齿。 - 抗锯齿(Anti-aliasing):在项目质量设置或URP Asset中开启MSAA或FXAA等后处理抗锯齿,可以平滑云朵边缘。
6.3 性能开销过大
- 使用Frame Debugger或Profiler定位:打开Window -> Analysis -> Profiler,查看GPU耗时。找到渲染你云朵的Pass,看其耗时是否异常。Frame Debugger可以查看具体的Draw Call和渲染状态。
- 检查纹理采样次数:在Shader Graph中,每一个
Sample Texture 2D节点都是一个纹理采样指令。尽量减少采样次数,考虑复用采样结果。 - 检查复杂的数学运算:
Pow、Sin、Noise节点相对昂贵。避免在片元着色器中无节制地使用。如果某些计算在顶点间变化不大,可以尝试移到顶点着色器阶段(在Shader Graph中,使用Custom Function节点并指定Stage为Vertex)。
6.4 动态效果不自然
- UV动画生硬:简单的UV偏移(
UV + Time * Speed)是线性的,看起来机械。可以尝试:- 使用两个不同速度的层,叠加在一起。
- 使用
Time的正弦或余弦值来调制速度,制造“阵风”效果:Speed * (1.0 + 0.3 * sin(Time * 0.5))。 - 使用一张流动纹理(Flow Texture)来扭曲主噪声的UV,模拟更复杂的风场。
- 颜色变化突兀:直接切换
ColorGradient纹理会显得很跳。可以通过脚本,使用两个材质属性(如_ColorGradientA和_ColorGradientB)以及一个混合系数_LerpFactor,在Shader中进行插值,实现平滑的昼夜或天气色调过渡。
最后,风格化渲染没有绝对的对错,只有是否适合你的项目。UnlitClouds项目提供了一个强大而灵活的起点,但最美的云朵永远来自于你对参数的反复调试和艺术感觉的注入。多参考优秀的游戏和艺术作品,思考它们的云朵在形状、颜色、动态上有什么特点,然后大胆地去尝试用Shader Graph复现。这个过程本身,就是技术美术成长中最有乐趣的部分。
