1. 从一张“灰度图”开始理解卡通云消散的核心做卡通渲染的天空盒云的效果绝对是点睛之笔。静态的云贴图虽然简单但总感觉少了点灵气。你看《原神》里那种云会随着太阳光的移动边缘慢慢变淡、消散整个天空一下子就活了起来。我最初也想实现这个效果翻遍了资料发现很多教程都是用噪声图叠加来做云的动态虽然也能动但总感觉那种“消散”的质感不够细腻更像是云的透明度在变化而不是形状在自然地生长或消融。后来我盯上了《原神》的云贴图资源像很多研究者一样把它的RGBA通道拆开来看。这一看就发现了门道。它的B通道存储的并不是颜色信息而是一张神奇的“灰度图”。这张图上的每一个像素的灰度值本质上代表了该点到云朵实体边界的“有向距离”。白色区域值接近1意味着你在云朵内部很深的地方黑色区域值接近0意味着你在云朵外部很远的地方而灰色地带就是云朵的边缘过渡区。这个技术就是SDFSigned Distance Field有向距离场。你可以把SDF想象成一个地形海拔图。白色是高山云内部黑色是深海云外部灰色就是海岸线。我们控制云消散其实就是控制“海平面”的升降。当“海平面”一个阈值上升时低海拔的灰色区域云的边缘就被“淹没”了看起来云就缩小了、消散了当“海平面”下降时更多的高海拔区域显露出来云就显得生长了。用SDF来控制云的形态变化最大的好处就是边界极其平滑无论你怎么放大云的边缘都不会出现锯齿而且消散的过程非常自然是一种从边缘向内部的、柔和的形状侵蚀这正是我们想要的那种卡通渲染的质感。所以实现卡通云消散效果第一步不是急着写Shader而是理解并准备好你的SDF图。如果你有现成的、带SDF通道的云贴图就像《原神》那种那恭喜你可以直接开工。如果没有你可能需要学习一下如何从一张普通的云遮罩图Alpha图去生成SDF这个过程通常会用到一个叫8SSEDT的算法。不过别担心我们今天主要聚焦在如何利用已有的SDF图在Unity Shader里玩出花样来。2. Shader实战让云朵“呼吸”起来理论懂了手就痒了。咱们直接进入Unity创建一个Unlit Shader我把它命名为“CartoonCloudDissolve”。核心思路很简单我们有一张RGBA贴图其中RGB通道可能是云的基础颜色和光照信息A通道或者我们指定的B通道就是SDF距离场。然后我们需要一个动态的“阈值”来控制消散。2.1 基础消散阈值与裁剪最基础的实现就是用一个_Cutoff参数来裁剪SDF。在片元着色器里我们会做这样的事// 从纹理中采样假设SDF信息存储在texColor.b通道B通道 half4 texColor tex2D(_MainTex, i.uv); half sdfValue texColor.b; // 提取SDF值 // 基础消散如果SDF值小于阈值则完全透明丢弃片元 if (sdfValue _Cutoff) { discard; // 或者 clip(sdfValue - _Cutoff); } // 否则输出云的颜色这里简单用纹理的r通道作为颜色 half3 cloudColor texColor.rrr; return half4(cloudColor, 1.0);通过动画或者脚本在0到1之间改变_Cutoff你就能看到云朵从完整到完全消失的过程。但是这样做的边缘非常生硬一刀切没有那种渐变的、半透明的消散感。这显然不是我们想要的。2.2 平滑边缘软粒子与渐变过渡卡通渲染虽然风格化但视觉舒适度同样重要。我们需要一个平滑的过渡区域。这里引入一个_Softness参数来控制过渡带的宽度。// 计算SDF值相对于阈值的相对位置 half sdfRelative (sdfValue - _Cutoff) / _Softness; // 将位置限制在0到1之间这个值就是透明度因子 half alphaFactor saturate(sdfRelative); // 最终输出颜色alphaFactor为1表示完全不透明为0表示完全透明 return half4(cloudColor, alphaFactor);当sdfValue刚好等于_Cutoff时alphaFactor为0完全透明。当sdfValue大于_Cutoff _Softness时alphaFactor为1完全不透明。在这之间的区域就会产生一个平滑的透明度渐变。调整_Softness你可以让云的边缘看起来是毛茸茸的还是很锐利的这给了你很大的艺术控制空间。2.3 注入灵魂与太阳光方向联动静态的消散已经不错了但如何模拟《原神》中那种“迎着阳光消散”的感觉呢关键在于让消散的阈值_Cutoff不再是全局统一的而是根据片元相对于太阳的方向发生变化。我们需要在Shader中获取太阳光的方向可以是一个世界空间向量_SunDir。然后在顶点着色器中将顶点的世界位置或模型空间法线传递到片元着色器。在片元着色器中我们计算该点指向太阳的方向并与一个预设的“消散方向”做点积。// 假设我们使用顶点的世界空间法线需要从模型空间变换 float3 worldNormal normalize(i.worldNormal); // 或者更简单点我们直接用从云片中心指向片元的向量对于面片云更直观 float3 worldPos i.worldPos; float3 centerToFragment normalize(worldPos - _CloudCenterWorldPos); // 计算与太阳方向的点积太阳方向需要是单位向量 half sunDot dot(centerToFragment, _SunDir.xyz); // 将点积范围[-1,1]重映射到[0,1]表示“朝向太阳的程度” half facingSun saturate(sunDot * 0.5 0.5); // 让阈值受到朝向影响越朝向太阳阈值越高消散得越多 half dynamicCutoff _Cutoff facingSun * _SunDissolveStrength; // 使用动态阈值进行之前的平滑消散计算 half sdfRelative (sdfValue - dynamicCutoff) / _Softness; half alphaFactor saturate(sdfRelative);这样云朵朝向太阳的那一侧dynamicCutoff会变大导致sdfValue更容易小于阈值从而透明度降低看起来就像是被阳光“吹散”了一样。背对太阳的一侧则保持相对完整。调整_SunDissolveStrength可以控制这个效果的强弱。我实测下来这个小小的改动让整个天空盒的动态表现提升了不止一个档次从“会动的云”变成了“有生命的云”。3. 性能优化让效果更“丝滑”效果出来了但作为一个有经验的开发者肯定会关心性能。尤其是在移动平台或者需要绘制大量云朵的场景下Shader的优化至关重要。SDF本身的计算已经很高效了只是一次纹理采样和简单算术但我们可以做得更好。3.1 纹理采样与Mipmap的权衡我们的云贴图含SDF通常不需要特别高的分辨率因为云本身的细节是柔和的。使用过大的纹理不仅占用内存采样也更慢。强烈建议开启纹理的Mipmap。对于SDF数据在缩小时进行线性过滤是安全的并且能有效减少远处云朵的锯齿感。在Shader中可以使用tex2Dlod或让Unity自动选择Mip层级。另外考虑将SDF图从RGBA贴图中分离出来单独存成一张单通道R通道的纹理。这样在采样时带宽占用更小。在Unity中单通道纹理可以设置为Alpha8格式压缩效率很高。3.2 剔除与几何复杂度我们用来显示云的模型通常只是一个简单的面片Quad或者经过简单形变的网格。确保面片始终朝向相机Billboard可以用最小的几何体覆盖最大的屏幕空间。同时要利用好相机的视锥体剔除Frustum Culling和遮挡剔除Occlusion Culling。对于远离相机或者被地形遮挡的云根本就不要提交渲染。对于多层云为了增加纵深感不要简单地复制几十个面片。可以采用单个面片但采样多次不同缩放和偏移的SDF纹理来模拟多层效果。也就是在一个片元着色器内进行多次比如3-4次纹理采样每次采样的UV加上不同的偏移和旋转然后将结果叠加混合。这比绘制多个Draw Call要高效得多。half4 sumColor 0; for (int layer 0; layer _LayerCount; layer) { float2 offsetUV i.uv _LayerOffset[layer].xy; float layerScale _LayerScale[layer]; offsetUV (offsetUV - 0.5) * layerScale 0.5; // 以UV中心缩放 half4 tex tex2D(_MainTex, offsetUV); half sdf tex.b; // ... 每层独立的消散计算 ... sumColor layerColor; } // 对叠加结果进行归一化或自定义混合 return sumColor;3.3 Shader计算精度优化在片元着色器中half类型中精度对于颜色和透明度计算通常就足够了比float高精度更快。尤其是在移动平台的GPU上这个优化能带来可观的性能提升。确保像sdfValue、_Cutoff、alphaFactor这类参数都使用half。将一些在逐顶点阶段可以计算的值比如世界空间方向向量的部分计算移到顶点着色器中。虽然会因插值损失一些精度但对于云这种柔和的对象视觉差异几乎不可见却能减轻片元着色器的负担。4. 艺术化调参塑造独特的云风格技术实现是骨架艺术调参才是血肉。我们的SDF Shader提供了几个关键的“旋钮”让你可以轻松调整出不同风格的云。基础形状与密度这主要由你的SDF贴图本身决定。一张SDF图就定义了一类云的“家族”。你可以准备多张不同风格的SDF图积云、卷云、层云在运行时混合或切换。消散边缘的质感_Softness参数是核心。值调小云的边缘清晰利落像剪纸或卡通画值调大边缘模糊柔和更像棉花糖或水彩晕染的效果。你可以尝试让_Softness本身也随着facingSun变化朝向太阳的边缘更柔和消散中背对太阳的边缘更硬朗。光照与颜色我们之前只用了简单的颜色。实际上可以结合太阳方向进行简单的卡通着色。例如根据sunDot将光照分为明暗两阶half lightStep step(0.2, sunDot); // 点积大于0.2算亮部否则暗部 half3 shadedColor lerp(_ShadowColor, _LitColor, lightStep); cloudColor * shadedColor;更进一步可以采样崩坏3/原神那种多通道贴图用R通道作为基础色G通道作为边缘光Rim Light强度结合视角方向做出发光边缘。这样在云消散变薄时边缘光效果可以增强模拟阳光透射的感觉。动态变化不要让_Cutoff只是单调地来回变化。可以结合时间使用正弦波、噪声函数来制造更自然、随机的生长消散循环。甚至可以用一张低分辨率的噪声图来扰动整个云层的消散阈值制造出云层中局部先消散的效果避免整体变化过于均匀和机械。我踩过的一个坑是初期把所有的参数变化都做得很规律导致云的运动看起来像机器在呼吸很不自然。后来引入了少量的随机噪声和时间倍频不同层的云运动速度不同整个动态效果立刻就生动起来了。记住“有序中的无序”是自然感的关键。5. 进阶思路从面片到体积感使用单个面片加SDF效率最高但缺点是缺乏体积感特别是当相机移动或从侧面看时。如果你对效果有更高要求可以考虑以下进阶方向。多层叠加与视差这是提升体积感性价比最高的方法。使用3-5个不同高度、不同缩放、不同运动速度的面片叠加同一张SDF图。在着色时根据片元所在的层对UV施加一个基于视角方向的微小偏移视差偏移虽然简单但能极大地增强云的立体感和深度。计算时可以用层的深度高度乘以视角向量的水平分量来偏移UV。Raymarching体积云这是终极方案完全实现真正的体积云。它不再依赖面片而是在Shader中通过光线步进Raymarching一个3D噪声场来渲染云。SDF在这里可以作为一个“密度分布”的指导场。你可以先用SDF定义出云的大致形状边界然后在边界内用3D噪声填充细节。这种方法计算量巨大通常用于PC或主机平台但效果也最震撼云会有真正的光影、散射和体积感。对于卡通渲染风格全物理的体积云可能过于“写实”。一个折中的思路是在SDF面片渲染的基础上增加一个基于视角的“边缘厚度”模拟。当视线与云表面法线接近垂直时看云侧面让云的透明度根据SDF值的变化更慢一些模拟一个简单的厚度。这只需要在片元着色器中增加一个基于视角因子的计算对性能影响微乎其微却能有效改善侧面观看的扁平感。实现这个效果已经足够让你的卡通天空盒脱颖而出。最重要的是理解SDF这个工具它给了你精确控制形状边界的能力。剩下的就是发挥你的创意结合光照、颜色和动态去创造属于你自己的那片会呼吸的卡通天空了。