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 与顶点动画结合制作更复杂的形变溶解。

相关推荐
mxwin3 小时前
Unity Shader 用 Ramp 贴图实现薄膜干涉效果
unity·游戏引擎·贴图·shader·uv
魔士于安4 小时前
Unity星球资源,八大星球,带fps显示
游戏·unity·游戏引擎·贴图·模型
张老师带你学5 小时前
unity资源,深空陨石,适合太空背景的游戏开发
游戏·unity·模型
鹿野素材屋7 小时前
Unity动画幅度太大怎么办
unity·游戏引擎
垂葛酒肝汤8 小时前
Unity Sprite Rect 越界问题笔记
笔记·unity·游戏引擎
平行云9 小时前
数字孪生信创云渲染系列(一):混合信创与全国产化架构
unity·ue5·3dsmax·webgl·gpu算力·实时云渲染·像素流送
废嘉在线抓狂.10 小时前
Unity拓展关于阵列物品生成以及物品替换
unity·游戏引擎
电子云与长程纠缠10 小时前
Godot学习01 - HelloWorld
学习·游戏引擎·godot
mxwin10 小时前
Unity Shader · UV 技术 用 UV 坐标打造水波涟漪效果
unity·游戏引擎·shader·uv