Unity Shader 使用 Noise 图 制作Shader 溶解效果

从原理到实践,完整剖析如何在 Unity URP 中

通过 Noise 纹理驱动逐像素溶解,并添加边缘发光特效。

什么是溶解效果?

溶解(Dissolve)是游戏中极为常见的视觉效果:角色死亡时身体"烧碎"消失、传送门开启时材质"腐蚀"消融、 技能施放时魔法光芒从内向外"蔓延"。这些效果看起来复杂,本质上只是一个优雅的 Shader 技巧------ 用一张 Noise 纹理控制像素的剔除阈值。

本文将从核心原理出发,逐步构建一个完整的溶解 Shader,最终实现可实时调节进度、带边缘火焰发光的溶解效果。

认识 Noise 图

Noise 图(噪声纹理)本质上是一张灰度图,每个像素值在 0 到 1 之间随机分布,但具有空间连续性------ 相邻像素的值变化是平滑渐进的,而非完全随机跳变。这种特性使得溶解效果显得自然、有机。

常用 Noise 类型对比

💡

在 Unity 中,你可以使用 Shader Graph 的 Noise 节点直接生成 Noise,也可以预烘焙一张 Noise 贴图放入材质球。 预烘焙贴图方案性能更好,适合移动端;Shader Graph 方案更灵活,适合动态变化效果。

// 原理示意:Noise 图灰度值 → 溶解阈值比较

核心原理:clip() 函数

溶解效果的核心只有一行代码。在 Fragment Shader 中,我们采样 Noise 图得到一个灰度值, 将其与一个从外部传入的 _Threshold(进度参数,0~1)比较:

clip( noiseValue - _Threshold )// noiseValue < _Threshold → 该像素被丢弃(透明) // noiseValue ≥ _Threshold → 该像素正常渲染

clip(x) 是 HLSL 的内置函数: 当传入值 小于 0 时,该像素被完全丢弃,不参与后续渲染(相当于透明)。

Shader 代码实现

1基础溶解 Shader(Built-in 管线)

先从最简版本开始。创建一个新的 Shader,命名为 Dissolve.shader:

cs 复制代码
Shader "Custom/Dissolve"
{
    Properties
    {
        _MainTex   ("主纹理", 2D)     = "white" {}
        _NoiseTex  ("Noise 纹理", 2D)  = "white" {}
        _Threshold ("溶解进度", Range(0,1)) = 0.0
    }

    SubShader
    {
        Tags { "RenderType"="TransparentCutout"
               "Queue"="AlphaTest" }
        Cull Off   // 双面渲染,防止穿帮

        Pass
        {
            CGPROGRAM
            #pragma vertex   vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            sampler2D _MainTex;
            sampler2D _NoiseTex;
            float     _Threshold;

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv     : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv  : TEXCOORD0;
            };

            v2f vert(appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv  = v.uv;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                // 1. 采样 Noise 图(只需 R 通道)
                float noise = tex2D(_NoiseTex, i.uv).r;

                // 2. 核心:当 noise < threshold 时丢弃该像素
                clip(noise - _Threshold);

                // 3. 正常采样主纹理输出
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}

注意 RenderType 设为 TransparentCutout、 Queue 设为 AlphaTest, 这确保使用了 clip() 的物体能正确写入深度缓冲,避免半透明排序问题。

2添加边缘发光效果

基础溶解太平淡了。现实中的溶解往往伴随边缘的灼烧、发光。 实现思路:检测 "刚好在溶解边界附近" 的像素,给它们叠加一个高亮颜色。

cpp 复制代码
Properties
{
    _MainTex    ("主纹理",     2D)             = "white" {}
    _NoiseTex   ("Noise 纹理",  2D)             = "white" {}
    _Threshold  ("溶解进度",   Range(0,1))       = 0.0
    _EdgeWidth  ("边缘宽度",   Range(0,0.2))     = 0.05
    _EdgeColor  ("边缘颜色",   Color)           = (1, 0.4, 0.1, 1)
    _EdgeGlow   ("发光强度",   Range(0,8))       = 3.0
}

// ── Fragment Shader ──────────────────────────
fixed4 frag(v2f i) : SV_Target
{
    float noise = tex2D(_NoiseTex, i.uv).r;

    // 基础 clip:噪声值低于阈值的像素被剔除
    clip(noise - _Threshold);

    // 计算当前像素距离溶解边界的"距离"
    // 当 noise 处于 [_Threshold, _Threshold+_EdgeWidth] 区间时为边缘
    float edgeFactor = saturate((noise - _Threshold) / _EdgeWidth);

    // 采样主纹理
    fixed4 mainCol = tex2D(_MainTex, i.uv);

    // 边缘颜色:edgeFactor=0 时完全是边缘色,=1 时完全是主纹理色
    // pow(1-edgeFactor, 2) 使边缘更锐利
    float  edgeMask = pow(1.0 - edgeFactor, 2.0);
    fixed4 glowCol  = _EdgeColor * _EdgeGlow * edgeMask;

    // 叠加:基础颜色 + 边缘发光
    fixed4 finalCol = mainCol + glowCol;
    return finalCol;
}

3边缘宽度与颜色渐变原理

URP 版本适配

如果你使用的是 Unity 的 Universal Render Pipeline(URP) , 需要将 Shader 改写为 HLSLPROGRAM 块并引用 URP 的核心库:

cpp 复制代码
Shader "Custom/URP_Dissolve"
{
    Properties { /* 同上 */ }

    SubShader
    {
        Tags
        {
            "RenderType"      = "TransparentCutout"
            "RenderPipeline"  = "UniversalPipeline"  // ← URP 必须
            "Queue"           = "AlphaTest"
        }

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode" = "UniversalForward" }

            HLSLPROGRAM
            #pragma vertex   vert
            #pragma fragment frag

            // 引用 URP 核心库(替代 UnityCG.cginc)
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            TEXTURE2D(_MainTex);   SAMPLER(sampler_MainTex);
            TEXTURE2D(_NoiseTex);  SAMPLER(sampler_NoiseTex);

            CBUFFER_START(UnityPerMaterial)
                float4  _MainTex_ST;
                float   _Threshold;
                float   _EdgeWidth;
                float4  _EdgeColor;
                float   _EdgeGlow;
            CBUFFER_END

            struct Attributes
            {
                float4 positionOS : POSITION;
                float2 uv         : TEXCOORD0;
            };

            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
                float2 uv          : TEXCOORD0;
            };

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                OUT.uv = TRANSFORM_TEX(IN.uv, _MainTex);
                return OUT;
            }

            half4 frag(Varyings IN) : SV_Target
            {
                float noise = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, IN.uv).r;
                clip(noise - _Threshold);

                float edgeFactor = saturate((noise - _Threshold) / _EdgeWidth);
                half4 mainCol    = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, IN.uv);
                half4 glowCol    = _EdgeColor * _EdgeGlow * pow(1 - edgeFactor, 2);
                return mainCol + glowCol;
            }
            ENDHLSL
        }
    }
}

📌

URP 的核心变化:① 使用 HLSLPROGRAM / ENDHLSL ; ② 用 TEXTURE2D / SAMPLER / SAMPLE_TEXTURE2D 代替 tex2D ; ③ 所有材质参数放入 CBUFFER 以支持 SRP Batcher 合批优化。

C# 脚本控制动画

通过 C# 脚本在运行时动态更新 _Threshold,即可实现溶解动画:

cs 复制代码
using UnityEngine;
using System.Collections;

public class DissolveController : MonoBehaviour
{
    [Header("溶解设置")]
    public float  dissolveDuration = 2.0f;  // 溶解总时长(秒)
    public bool   autoDissolveOnStart = false;

    private Material    _mat;
    private static readonly int _ThresholdID
        = Shader.PropertyToID("_Threshold");  // 缓存 ID,避免字符串查找

    void Awake()
    {
        // GetComponent 获取 Renderer 并克隆材质(避免修改共享材质)
        var rend = GetComponent<Renderer>();
        _mat = rend.material;  // .material 自动克隆
    }

    void Start()
    {
        if (autoDissolveOnStart)
            StartCoroutine(DissolveRoutine(0f, 1f));
    }

    /// <summary>外部调用:开始溶解消失</summary>
    public void Dissolve() => StartCoroutine(DissolveRoutine(0f, 1f));

    /// <summary>外部调用:溶解恢复出现</summary>
    public void Appear()   => StartCoroutine(DissolveRoutine(1f, 0f));

    private IEnumerator DissolveRoutine(float from, float to)
    {
        float elapsed = 0f;
        while (elapsed < dissolveDuration)
        {
            elapsed += Time.deltaTime;
            float t = Mathf.Clamp01(elapsed / dissolveDuration);

            // 用 SmoothStep 让动画有缓入缓出感
            float smooth = Mathf.SmoothStep(from, to, t);
            _mat.SetFloat(_ThresholdID, smooth);

            yield return null;  // 等待下一帧
        }
        _mat.SetFloat(_ThresholdID, to);

        // 完全溶解后可选择隐藏 GameObject
        if (to >= 1f) gameObject.SetActive(false);
    }
}

进阶变体与技巧

方向性溶解

不使用 Noise 图,而是用 顶点的世界坐标 Y 值 作为溶解参数,实现"从下往上"或"从上往下"的方向性溶解------ 常用于魔法传送、角色出场效果。

cpp 复制代码
// Vertex Shader 传递世界坐标 Y
Varyings vert(Attributes IN)
{
    Varyings OUT;
    float3 worldPos = TransformObjectToWorld(IN.positionOS.xyz);
    OUT.worldY = worldPos.y;  // 传入世界 Y
    OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
    return OUT;
}

// Fragment Shader 中用 worldY 代替 Noise
half4 frag(Varyings IN) : SV_Target
{
    // 将世界 Y 映射到 [0,1](需根据模型高度调整 _MinY/_MaxY)
    float normalizedY = saturate((IN.worldY - _MinY) / (_MaxY - _MinY));

    // 叠加 Noise 增加不规则感
    float noise    = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, IN.uv).r;
    float combined = normalizedY + noise * 0.3;  // Noise 起辅助扰动作用

    clip(combined - _Threshold);
    // ... 边缘光逻辑同前
}

多层 Noise 叠加(更丰富的边缘细节)

cs 复制代码
// 对同一张 Noise 图以不同频率采样 3 次,叠加 fBm
float2 uv0 = IN.uv;
float2 uv1 = IN.uv * 2.1 + float2(0.13, 0.27);  // 偏移避免重复感
float2 uv2 = IN.uv * 4.3 + float2(0.51, 0.89);

float n0 = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, uv0).r;
float n1 = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, uv1).r;
float n2 = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, uv2).r;

// 权重叠加:低频主导形态,高频增加细节
float noise = n0 * 0.6 + n1 * 0.3 + n2 * 0.1;

💡

在移动端,每次额外的纹理采样都有性能开销。对于移动端项目,建议将 fBm 预烘焙到一张贴图, 而不是在 Shader 运行时计算多次采样。

性能优化小结

在实际项目中需要注意以下几点:

使用 GPU Instancing 批量渲染 预烘焙 Noise 图避免运行时计算 移动端关闭 Bloom 替代边缘发光 合理压缩 Noise 贴图格式(ETC2/BC4) SRP Batcher 需要 CBUFFER 包装参数 LOD 远距离关闭边缘效果

小结

Unity Shader 溶解效果的核心思路极为简洁:Noise 图提供每像素的随机阈值,clip() 函数根据阈值剔除像素, edgeFactor 在溶解边界附近叠加发光颜色。 整个实现只需不到 20 行核心代码,却能产生极具视觉冲击力的效果。

掌握这个基础之后,你可以进一步扩展:结合粒子系统在溶解边缘发射火花,或者用 Shader Graph 的可视化节点 实现相同效果,或者将 Noise 与顶点动画结合制作更复杂的形变溶解。

相关推荐
淡海水2 小时前
16-02-C#常用数据结构源码-附录B-源码索引速查
unity·c#·游戏引擎·il2cpp
速易达网络4 小时前
无人机远程操控仿真
游戏引擎·cocos2d
郝学胜-神的一滴4 小时前
游戏引擎原理与实践 02:揭开3A游戏背后的技术面纱
开发语言·计算机网络·程序人生·游戏·游戏引擎
警醒与鞭策13 小时前
【无标题】
android·unity·性能优化·游戏引擎·perforce
zhchyun200814 小时前
【Unity UI 进阶】仿 Element UI 打造企业级 Unity UI 组件库(10)
ui·unity·游戏引擎
地狱为王19 小时前
Unity使用NVIDIA的Audio2Face驱动口型
unity·游戏引擎
ebiobiz21 小时前
STM32F4 Unity 单元测试与多次测试失败率统计详细步骤
stm32·unity·单元测试
一线灵1 天前
跨平台游戏引擎 Axmol-2.11.5 发布
游戏引擎·lua
淡海水1 天前
16-01-C#数据结构源码-附录A-术语表
unity·c#·游戏引擎·il2cpp
淡海水1 天前
16-03-C#数据结构源码-附录C-参考资源
unity·c#·游戏引擎·il2cpp