Unity Shader噪声图应用:5步实现物体溶解特效
1. 项目概述为什么噪声图是Shader特效的“万金油”在Unity里做特效尤其是那种需要动态、随机、不规则变化的效果很多开发者第一时间会想到去写复杂的算法或者依赖粒子系统堆叠。但如果你手头有一张噪声图Noise Texture事情往往会变得简单得多。这次要聊的“物体溶解消失”效果就是一个绝佳的例子。它听起来很酷炫——物体从某个点开始像被火焰烧蚀或者从边缘开始像素化消散最终完全消失。这种效果在角色死亡、场景切换、道具拾取等游戏场景中应用极广。而实现它的核心就是一张看似杂乱无章的灰度噪声图。你可以把它理解为一幅记录了“随机值”的地图图上每个像素的亮度从黑到白代表了一个0到1之间的值。在Shader中我们采样这张图用它的值来决定模型表面每个像素是“保留”还是“溶解”。通过一个随时间变化的阈值来控制低于阈值的像素被“剪裁”掉高于的则保留。随着阈值从0到1变化物体就会呈现出一种随机、颗粒感的溶解过程。这比单纯从边缘线性溶解要自然和有趣得多。这个项目适合所有想踏入Unity Shader编程大门的开发者无论你是刚学会写个Unlit Shader的新手还是想丰富自己特效工具箱的进阶者。它不涉及过于复杂的数学和光照模型核心逻辑清晰是理解片元着色器Fragment Shader中“裁剪”clip或“透明度测试”概念的完美入门实战。5分钟是个夸张的说法但跟着步骤走半小时内从零到一实现并调出满意的效果是完全可行的。2. 核心原理与噪声图深度解析2.1 溶解效果的数学本质阈值比较溶解效果在Shader层面的实现本质上是一个二值化判断过程。我们为模型表面的每个像素更准确地说是每个片元计算一个“溶解值”。这个值可以来自纹理采样也可以来自某种算法。然后我们设定一个全局的“溶解阈值”通常是一个0到1之间、随时间变化的浮点数。核心判断逻辑只有一行代码if (dissolveValue _Threshold) discard;或者使用clip(dissolveValue - _Threshold);。clip函数是HLSL/GLSL中的内置函数当传入的参数小于0时直接丢弃当前片元不进行后续的光照和输出计算。这就实现了像素的“消失”。所以整个效果的动态性就来自于两点一是每个像素独特的dissolveValue二是那个不断变化的全局_Threshold。_Threshold从0逐渐增加到1的过程就是物体从完全显示到完全消失的过程。2.2 噪声图随机性的源泉与选择为什么用噪声图而不是自己用sin、cos函数生成随机数因为噪声图提供了高质量、可控的随机性并且计算开销极低一次纹理采样。我们自己写的伪随机函数可能在片元着色器中带来巨大的性能开销。常用的噪声图有几种Perlin噪声/Value噪声产生连续、自然的云雾状纹理。它的灰度变化平滑适合做平滑的溶解边缘。Simplex噪声Perlin噪声的优化版计算更高效效果类似。白噪声完全随机每个像素值独立。它能产生非常尖锐、颗粒感强的溶解边缘。柏林噪声Perlin Noise的变体如分形噪声通过叠加不同频率的Perlin噪声可以产生更复杂、层次更丰富的纹理适合需要更多细节的溶解效果。在Unity中我们通常直接使用一张导入的2D纹理作为噪声图。为了获得无缝平铺的效果避免溶解边缘出现明显的接缝需要确保使用的噪声图本身是Tileable可平铺的。很多噪声生成工具如Photoshop的滤镜、专门的噪声生成软件都可以生成无缝噪声图。注意在Unity中导入噪声图时务必在导入设置Import Settings中将“Wrap Mode”设置为“Repeat”这样才能让纹理在UV坐标超出[0,1]范围时正确平铺这对于溶解大物体或让溶解效果动起来时至关重要。2.3 从UV到世界空间溶解的“起点”控制一个基础的溶解效果是直接使用模型自身的UV坐标去采样噪声图。但这有一个问题当阈值变化时溶解是均匀地发生在物体表面的没有一个明确的“起始点”。为了让溶解效果更有方向性、更有叙事感例如从被击中的点开始溶解或者从脚底向上溶解我们需要将噪声采样从简单的UV空间转换到更可控的空间比如模型局部空间Object Space或世界空间World Space。世界空间采样的优势稳定性无论物体如何旋转、移动溶解的“模式”在世界中是固定的。溶解看起来是从世界中的某个位置开始蔓延而不是粘在物体模型上。控制性我们可以很容易地定义一个世界空间坐标如角色脚底的位置、子弹击中的点作为溶解的中心然后计算片元世界坐标到该中心的距离混合进噪声值中从而实现从某点向周围扩散的效果。实现思路是在顶点着色器中将顶点从模型空间变换到世界空间并将世界坐标传递给片元着色器。在片元着色器中使用世界坐标的XZ分量如果我们希望从Y轴方向开始或者经过缩放后的坐标作为采样噪声图的UV。这样溶解的图案就“贴”在了世界平面上物体移动穿过这个图案时就会产生相应的溶解。3. 完整ShaderLab代码实现与逐行解析下面是一个基于Unity内置渲染管线Built-in RP的Surface Shader实现。它包含了基础溶解、边缘光效和世界空间溶解起点控制。我们将使用clip函数实现裁剪。Shader Custom/DissolveEffect { Properties { _MainTex (Albedo (RGB), 2D) white {} // 物体主纹理 _Color (Color, Color) (1,1,1,1) // 物体颜色 _NoiseTex (Noise Texture, 2D) white {} // 噪声纹理 _Threshold (Dissolve Threshold, Range(0, 1)) 0.0 // 溶解阈值 _EdgeWidth (Edge Width, Range(0, 0.5)) 0.1 // 溶解边缘宽度 _EdgeColor (Edge Color, Color) (1,0.5,0,1) // 溶解边缘颜色 _EdgeIntensity (Edge Intensity, Range(0, 10)) 3.0 // 边缘颜色强度 _DissolveStartPoint (Dissolve Start Point (World XZ), Vector) (0,0,0,0) // 世界空间溶解起点 _DissolveRadius (Dissolve Radius, Float) 5.0 // 溶解影响半径 } SubShader { Tags { RenderTypeOpaque QueueGeometry} // 注意Queue是Geometry确保在透明物体之前渲染被溶解的部分 LOD 200 CGPROGRAM // 使用Surface Shader简化了光照模型交互。这里用了简单的Lambert光照。 #pragma surface surf Lambert addshadow // addshadow 是关键它告诉Unity为这个Shader生成阴影投射Pass否则溶解物体无法正确投射阴影。 #pragma target 3.0 sampler2D _MainTex; sampler2D _NoiseTex; fixed4 _Color; float _Threshold; float _EdgeWidth; fixed4 _EdgeColor; float _EdgeIntensity; float3 _DissolveStartPoint; float _DissolveRadius; struct Input { float2 uv_MainTex; // 主纹理UV float2 uv_NoiseTex; // 噪声纹理UV可独立控制平铺 float3 worldPos; // 自动由Surface Shader填充的世界空间位置 }; void surf (Input IN, inout SurfaceOutput o) { // 1. 基础噪声采样基于UV fixed4 noiseDataUV tex2D(_NoiseTex, IN.uv_NoiseTex); float noiseValueUV noiseDataUV.r; // 通常取R通道因为噪声图是灰度的 // 2. 世界空间溶解控制 // 计算当前片元到溶解起点的水平距离忽略Y轴 float2 delta IN.worldPos.xz - _DissolveStartPoint.xz; float distanceFromStart length(delta); // 将距离映射到一个渐变因子0到1_DissolveRadius控制影响范围 float worldDissolveFactor saturate(distanceFromStart / _DissolveRadius); // 将世界空间因子叠加到噪声值上。这里使用加法世界因子越大噪声值被“抬高”越容易被溶解。 float finalNoiseValue noiseValueUV worldDissolveFactor; // 3. 核心溶解判断 // 计算当前片元与阈值的差值 float dissolveDelta finalNoiseValue - _Threshold; // 使用clip进行裁剪。如果差值小于0片元被丢弃。 clip(dissolveDelta); // 4. 边缘光效计算 // 如果差值小于边缘宽度说明该片元处于溶解边缘 if (dissolveDelta _EdgeWidth) { // 计算一个边缘强度因子差值越小越靠近消失边界强度越强 float edgeStrength 1 - (dissolveDelta / _EdgeWidth); // 将边缘颜色混合到最终输出。这里简单地将边缘颜色乘以强度加到漫反射颜色上。 // 更高级的做法可以单独输出自发光Emission。 fixed4 edgeGlow _EdgeColor * _EdgeIntensity * edgeStrength; o.Albedo tex2D(_MainTex, IN.uv_MainTex).rgb * _Color.rgb edgeGlow.rgb; o.Emission edgeGlow.rgb * 0.5; // 也可以给一点自发光让边缘更亮 } else { // 非边缘区域正常采样纹理 o.Albedo tex2D(_MainTex, IN.uv_MainTex).rgb * _Color.rgb; o.Emission fixed3(0,0,0); } o.Alpha 1.0; // 因为使用了clip我们当作不透明物体处理 } ENDCG } FallBack Diffuse // 失败时回退到漫反射Shader }关键代码解析与实操心得#pragma surface surf Lambert addshadowaddshadow指令至关重要。没有它你的溶解物体将无法产生阴影或者阴影不会随着物体的溶解而“溶解”这会严重破坏视觉效果的真实感。这是新手极易忽略的一点。clip(dissolveDelta)这是溶解效果的灵魂。所有魔法都发生在这里。clip的执行效率很高但要注意被clip掉的像素不会写入深度缓冲区Z-Buffer这可能会影响后续物体的深度测试。对于大多数情况这没问题。边缘光计算if (dissolveDelta _EdgeWidth)这个判断是在片元着色器中进行的。_EdgeWidth控制边缘的视觉宽度。计算edgeStrength时我们使用了1 - (dissolveDelta / _EdgeWidth)这确保在溶解边界处dissolveDelta接近0强度为1在边缘内侧dissolveDelta接近_EdgeWidth强度为0形成一个平滑过渡。世界空间因子混合finalNoiseValue noiseValueUV worldDissolveFactor;这里采用加法是一种简单有效的混合方式。它意味着离起点越远worldDissolveFactor越大最大为1finalNoiseValue被提升得越高就越容易超过阈值_Threshold而被保留反之则被溶解。这就实现了从起点向外扩散的溶解效果。如果你想实现“从外向内”溶解可以尝试finalNoiseValue noiseValueUV - worldDissolveFactor;。性能考量在片元着色器中进行if判断和额外的纹理采样噪声图会增加开销。对于移动平台要密切关注性能。如果物体像素很多可以考虑简化边缘光效或者使用step函数代替if。但总的来说这个Shader在PC和主流移动设备上压力不大。4. 在Unity编辑器中的完整配置与调试流程有了Shader代码我们需要在Unity中将其用起来。这个过程不仅仅是创建一个材质球那么简单合理的配置和调试技巧能让你事半功倍。4.1 创建材质与基础配置导入噪声图准备一张无缝的灰度噪声图比如Perlin噪声。将其导入Unity项目。在Inspector面板中关键设置如下Texture Type保持为Default即可。Wrap Mode必须设置为Repeat。这是实现平铺、让溶解效果无限延续的关键。Filter Mode设置为Bilinear或Trilinear可以获得更平滑的溶解边缘。如果你想要像素感强烈的效果可以选Point。其他设置如sRGB (Color Texture)通常需要取消勾选。因为噪声图是数据图不是颜色图关闭sRGB转换能保证我们采样到的是原始的线性数据。创建材质在项目窗口中右键 - Create - Material命名为Mat_Dissolve。将其Shader选择为我们刚创建的Custom/DissolveEffect。关联纹理将你的物体主纹理拖到_MainTex将导入的噪声图拖到_NoiseTex。初始测试先将_Threshold设为0.5_EdgeWidth设为0.1_EdgeColor调成一个亮色如橙色。将材质赋给一个简单的Cube或Sphere。你应该能看到物体表面出现了随机分布的溶解孔洞边缘带有颜色。4.2 动态控制溶解过程脚本编写溶解效果需要动起来我们需要一个C#脚本来控制_Threshold属性随时间或根据游戏逻辑变化。using UnityEngine; public class DissolveController : MonoBehaviour { public Material dissolveMaterial; // 需要控制的材质实例 public float dissolveDuration 3.0f; // 完全溶解所需时间 public bool startDissolveOnStart false; // 是否开始时自动溶解 public Vector3 worldStartPoint Vector3.zero; // 世界空间溶解起点 private float _currentThreshold 0f; private bool _isDissolving false; private Renderer _renderer; void Start() { _renderer GetComponentRenderer(); if (_renderer null) { Debug.LogError(DissolveController needs a Renderer component!); return; } // 重要获取材质实例而不是共享材质避免影响其他使用同一材质的物体。 if (dissolveMaterial null) { dissolveMaterial _renderer.material; } else { _renderer.material dissolveMaterial; } // 设置初始起点 dissolveMaterial.SetVector(_DissolveStartPoint, new Vector4(worldStartPoint.x, worldStartPoint.y, worldStartPoint.z, 0)); if (startDissolveOnStart) { StartDissolve(); } } void Update() { if (_isDissolving) { _currentThreshold Time.deltaTime / dissolveDuration; _currentThreshold Mathf.Clamp01(_currentThreshold); dissolveMaterial.SetFloat(_Threshold, _currentThreshold); // 溶解完成后可以销毁物体或触发事件 if (_currentThreshold 1.0f) { _isDissolving false; OnDissolveComplete(); } } } // 外部调用来开始溶解 public void StartDissolve(Vector3? startPoint null) { if (startPoint.HasValue) { worldStartPoint startPoint.Value; dissolveMaterial.SetVector(_DissolveStartPoint, new Vector4(worldStartPoint.x, worldStartPoint.y, worldStartPoint.z, 0)); } _currentThreshold 0f; _isDissolving true; } private void OnDissolveComplete() { Debug.Log(gameObject.name dissolve complete.); // 这里可以触发销毁、播放音效、生成粒子等逻辑 // gameObject.SetActive(false); // Destroy(gameObject, 0.5f); // 延迟销毁确保效果播放完 } }脚本使用要点材质实例化在Start方法中我们通过_renderer.material获取或设置材质。这个属性会创建该材质的一个实例这样修改属性就不会影响到其他使用同一材质球的物体。这是制作动态特效的通用最佳实践。属性设置使用SetFloat和SetVector来设置Shader中的属性。注意_DissolveStartPoint在Shader中是float3但SetVector需要传入Vector4我们只需填充xyzw可以设为0。性能在Update中每帧修改材质属性是常见的做法对于少量物体没问题。但如果场景中有成百上千个物体同时溶解应考虑使用材质属性块MaterialPropertyBlock来优化避免因修改材质属性而产生大量内存中的材质实例。4.3 高级调试与视觉优化技巧噪声图平铺Tiling控制在材质属性中你可以调整_NoiseTex的Tiling值。增大Tiling如设为5,5会让噪声图案更密集溶解的颗粒更小、更细碎减小Tiling则会让图案更稀疏产生大块的溶解区域。这能快速改变溶解的视觉风格。边缘光效的优化上述Shader的边缘光只是简单加在漫反射颜色上。一个更真实、更独立的方法是使用自发光Emission通道。将边缘光的颜色和强度直接输出到o.Emission并确保在渲染设置中启用了HDR和Bloom后处理这样边缘就能产生真实的辉光Glow效果在暗场景中尤其出色。使用屏幕空间深度图实现“烧焦痕迹”一个更高级的技巧是在溶解边缘处不仅发光还能模拟出物体被烧穿后背后场景的扭曲或颜色变化。这需要采样摄像机的深度法线纹理CameraDepthNormalsTexture或抓屏纹理GrabPass实现起来更复杂但效果提升显著。与粒子系统结合纯粹的Shader溶解有时显得单调。可以在物体开始溶解时在溶解起点_DissolveStartPoint生成一个粒子系统模拟飞溅的火花或灰烬。粒子系统的发射速率可以与_Threshold的变化率挂钩在溶解剧烈时发射更多粒子。5. 常见问题、性能考量与进阶方向5.1 常见问题排查速查表问题现象可能原因解决方案物体完全不溶解1._Threshold值始终为0。2. 噪声图采样错误UV不对或纹理未赋值。3. Shader编译错误clip未生效。1. 检查脚本是否成功设置_Threshold并在Inspector中确认数值变化。2. 在Shader中临时将o.Albedo float3(noiseValue,0,0);查看物体是否显示红色噪声图。检查噪声图导入设置Wrap Mode, sRGB。3. 查看Console窗口是否有Shader编译错误。溶解边缘锯齿严重1. 噪声图分辨率太低。2. 使用了Point滤波模式。3._EdgeWidth太小。1. 使用更高分辨率的噪声图如512x512或1024x1024。2. 将噪声图的Filter Mode改为Bilinear。3. 适当增加_EdgeWidth或在边缘计算中使用smoothstep代替线性插值实现抗锯齿。阴影不随物体溶解Shader中没有包含阴影投射Pass或addshadow指令缺失。确保Surface Shader声明中包含了addshadow。对于非Surface Shader需要手动编写ShadowCasterPass并在其中使用相同的clip逻辑。溶解效果在物体移动时“抖动”或“闪烁”1. 噪声图Wrap Mode不是Repeat导致UV超出[0,1]后采样错误。2. 世界空间计算时精度问题导致采样值跳变。1. 确认噪声图导入设置中Wrap Mode为Repeat。2. 在世界坐标参与计算前可以尝试乘以一个缩放系数如0.1让UV变化更平缓。检查_DissolveRadius是否过小。多个物体使用同一材质溶解效果同步脚本直接修改了材质的共享属性。务必使用renderer.material来获取材质实例而不是renderer.sharedMaterial。确保脚本中是对实例进行操作。5.2 性能优化要点纹理采样次数本Shader采样了两次纹理_MainTex和_NoiseTex。在移动端纹理采样是主要开销之一。如果性能吃紧可以考虑纹理合图将噪声图的R通道打包到主纹理的Alpha通道或其他空闲通道中减少一次采样。使用程序化噪声对于风格化或要求不高的场景可以用一个非常简单的伪随机函数如基于屏幕坐标的frac(sin(dot(...)) * ...)替代纹理采样但这可能牺牲视觉质量并增加ALU计算。分支判断片元着色器中的if语句在某些GPU架构上可能导致性能下降。我们可以尝试用step或smoothstep函数来重写边缘光计算消除分支。例如float isEdge step(dissolveDelta, _EdgeWidth); // dissolveDelta _EdgeWidth 则为1否则为0 float edgeStrength isEdge * (1 - (dissolveDelta / _EdgeWidth));但现代GPU对动态分支的优化已经很好在非极端性能敏感场景下可读性更好的if语句是可以接受的。Draw Call每个使用此Shader的材质如果属性不同如不同的_ThresholdUnity可能会进行动态合批Dynamic Batching失败导致Draw Call增加。对于大量需要独立溶解的小物体考虑使用GPU Instancing并将_Threshold等属性通过材质属性块MaterialPropertyBlock传递可以大幅降低Draw Call。5.3 效果进阶与扩展思路掌握了基础溶解后你可以尝试以下方向让效果更具个性多噪声图混合采样两张或多张不同频率、不同类型的噪声图如一张大尺度Perlin噪声决定溶解大区域一张小尺度Value噪声决定细节将它们混合起来可以创造出层次更丰富、更自然的溶解图案。溶解方向扭曲不仅仅是简单的阈值比较。你可以让溶解边缘根据一张法线图Normal Map或世界坐标的梯度产生扭曲模拟出“融化”或“腐蚀”的不规则边缘而不是硬生生的裁剪。顶点动画结合在溶解的同时让即将消失的顶点沿着法线方向膨胀、收缩或随机抖动可以模拟物体内部能量不稳定、即将爆裂的效果。这需要在顶点着色器中操作。后处理整合将溶解阈值写入自定义的渲染纹理Render Texture或利用Stencil Buffer标记正在溶解的像素。然后在后处理Post-processing阶段对整个屏幕中这些被标记的像素施加特效如色相偏移、模糊、扭曲等实现更震撼的全屏视觉反馈。URP/HDRP适配上述代码基于内置管线。在URPUniversal Render Pipeline中你需要编写一个Unlit或Lit Shader Graph或者手写HLSL代码核心逻辑采样噪声、clip、边缘计算是相通的但需要遵循URP的着色器库和光照函数。在HDRP中则可以获得更高质量的边缘光照和体积光交互。从一张简单的噪声图出发通过clip函数这个核心武器我们搭建起了一个动态、可调控的物体溶解系统。这个项目就像一把钥匙打开了Shader特效世界的一扇门。它教会你的不仅仅是实现一个效果更是一种“用数据噪声驱动视觉变化”的思维方式。在实际项目中大胆地调整参数、混合不同的噪声、结合动画和粒子你会发现它能演变出无数种可能——冰雪消融、沙化崩塌、数据化传输、幽灵显形……Shader的乐趣就在于用代码和数学去创造那些天马行空的视觉奇迹。