Unity URP 溶解效果基于噪声纹理与 clip 函数实现物体渐隐渐显

一、效果总览

溶解效果(Dissolve Effect)是游戏开发中极为常见的视觉技巧------ 角色死亡时化为碎片消散、传送门边缘的灼烧侵蚀、魔法书扉页的火焰翻涌------ 它的核心原理只有三行字:对噪声纹理采样、与阈值比较、clip 掉不该显示的像素

本文从原理到实现,完整拆解 Unity URP(Universal Render Pipeline)下的溶解 Shader, 涵盖 HLSL 手写代码Shader Graph 节点 两种路线, 并给出边缘发光(Emissive Border)的扩展写法。

二、核心原理

2.1 噪声纹理的作用

溶解效果的本质是非均匀透明度遮罩 。 如果直接对 alpha 做线性插值,物体会均匀淡出,毫无质感。 噪声纹理(Noise Texture)为每个像素提供了一个 [0, 1] 范围内的随机灰度值, 使"哪些像素先消失"具有不规则的自然形态。

2.2 clip / discard 的工作机制

HLSL 的 clip(x) 函数在 x < 0 时丢弃当前片元(等价于 discard):

cs 复制代码
// 当参数 < 0 时,当前像素被完全丢弃(不写入深度/颜色缓冲)
void clip(float x);
// 溶解核心语句:noiseValue - threshold < 0 → 被溶解掉
clip(noiseValue - _Threshold);

_Threshold01 增大时, 越来越多的像素满足 noiseValue - _Threshold < 0,被丢弃,物体"溶解消失"; 反向减小则物体"凝聚出现"。

💡 渐隐 vs 渐显: 渐隐时 _Threshold 从 0 → 1; 渐显时从 1 → 0。两者复用同一 Shader,只需在 C# 中控制方向即可。

三、URP HLSL Shader 完整实现

3.1 项目准备

  • Unity 6 / 2022 LTS + URP 14+
  • 准备一张 灰度噪声纹理(推荐 Perlin / Simplex,分辨率 256×256 即可)
  • 新建 Dissolve.shader,置于 Assets/Shaders/

3.2 完整 Shader 代码

cs 复制代码
Shader "Custom/URP/Dissolve"
{
    Properties
    {
        _MainTex      ("Albedo",         2D)           = "white" {}
        _NoiseTex     ("Noise Texture", 2D)           = "white" {}
        _Threshold    ("Dissolve Threshold", Range(0,1)) = 0
        _EdgeWidth    ("Edge Width",   Range(0,0.2))= 0.05
        _EdgeColor    ("Edge Color",   Color)        = (1,0.48,0.1,1)
        _EdgeIntensity("Edge Intensity",Float)       = 3.0
    }
    SubShader
    {
        Tags { "RenderType"="TransparentCutout" "RenderPipeline"="UniversalPipeline"
               "Queue"="AlphaTest" }
        LOD 300
        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode" = "UniversalForward" }
            // 双面渲染,避免溶解边缘穿帮
            Cull Off
            HLSLPROGRAM
            #pragma vertex   DissolveVert
            #pragma fragment DissolveFrag
            #pragma multi_compile_fog
            #pragma multi_compile_instancing
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            // ── 属性声明 ────────────────────────────
            TEXTURE2D(_MainTex);  SAMPLER(sampler_MainTex);
            TEXTURE2D(_NoiseTex); SAMPLER(sampler_NoiseTex);
            CBUFFER_START(UnityPerMaterial)
                float4 _MainTex_ST;
                float4 _NoiseTex_ST;
                float  _Threshold;
                float  _EdgeWidth;
                float4 _EdgeColor;
                float  _EdgeIntensity;
            CBUFFER_END
            // ── 顶点输入 / 输出 ─────────────────────
            struct Attributes {
                float4 positionOS : POSITION;
                float3 normalOS   : NORMAL;
                float2 uv         : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };
            struct Varyings {
                float4 positionHCS : SV_POSITION;
                float2 uvMain      : TEXCOORD0;
                float2 uvNoise     : TEXCOORD1;
                float3 normalWS    : TEXCOORD2;
                UNITY_VERTEX_OUTPUT_STEREO
            };
            // ── 顶点着色器 ──────────────────────────
            Varyings DissolveVert(Attributes input)
            {
                Varyings output;
                UNITY_SETUP_INSTANCE_ID(input);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
                output.positionHCS = TransformObjectToHClip(input.positionOS.xyz);
                output.normalWS    = TransformObjectToWorldNormal(input.normalOS);
                output.uvMain  = TRANSFORM_TEX(input.uv, _MainTex);
                output.uvNoise = TRANSFORM_TEX(input.uv, _NoiseTex);
                return output;
            }
            // ── 片元着色器 ──────────────────────────
            half4 DissolveFrag(Varyings input) : SV_Target
            {
                // 1. 采样噪声纹理(只取 R 通道即可)
                half noiseVal = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, input.uvNoise).r;
                // 2. 核心溶解 clip
                clip(noiseVal - _Threshold);
                // 3. 采样主贴图
                half4 albedo = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uvMain);
                // 4. 计算边缘发光
                //    noiseVal 处于 [_Threshold, _Threshold + _EdgeWidth] 视为边缘
                half edge = saturate(1.0 - (noiseVal - _Threshold) / _EdgeWidth);
                half3 emissive = _EdgeColor.rgb * edge * _EdgeIntensity;
                // 5. 简易 Lambert 光照(可替换为 URP PBR)
                Light mainLight = GetMainLight();
                half  NdotL     = saturate(dot(normalize(input.normalWS), mainLight.direction));
                half3 lighting  = albedo.rgb * mainLight.color * (NdotL * 0.8 + 0.2);
                return half4(lighting + emissive, 1.0);
            }
            ENDHLSL
        }
    }
}

🔍 关键行逐行解析:

  • 第 83 行SAMPLE_TEXTURE2D 采样噪声纹理的 R 通道,得到 [0,1] 的标量。
  • 第 86 行clip(noiseVal - _Threshold) 是溶解效果的全部核心,一行代码。
  • 第 93 行 :将 [_Threshold, _Threshold+_EdgeWidth] 这段区间映射到 [1,0],计算边缘强度。
  • 第 94 行 :边缘强度乘以 _EdgeIntensity(>1),产生 HDR 溢出感,配合 Bloom 后处理效果更佳。

四、边缘发光原理图

五、C# 控制脚本

在 Inspector 拖动或通过脚本动态修改 _Threshold 即可驱动溶解动画:

cs 复制代码
using UnityEngine;
using System.Collections;
public class DissolveController : MonoBehaviour
{
    [SerializeField] private float  _duration  = 2f;   // 溶解总时长(秒)
    [SerializeField] private bool   _autoPlay  = false;
    private Material  _mat;
    private static readonly int ThresholdID = Shader.PropertyToID("_Threshold");
    void Awake()
    {
        // 获取材质副本,避免污染共享材质
        _mat = GetComponent<Renderer>().material;
    }
    void Start()
    {
        if (_autoPlay) StartCoroutine(DissolveOut());
    }
    /// <summary>渐隐:threshold 从 0 → 1</summary>
    public IEnumerator DissolveOut()
    {
        yield return AnimateThreshold(0f, 1f);
        gameObject.SetActive(false); // 完全溶解后隐藏
    }
    /// <summary>渐显:threshold 从 1 → 0</summary>
    public IEnumerator DissolveIn()
    {
        gameObject.SetActive(true);
        yield return AnimateThreshold(1f, 0f);
    }
    private IEnumerator AnimateThreshold(float from, float to)
    {
        float t = 0f;
        while (t < _duration)
        {
            t += Time.deltaTime;
            // smoothstep 缓出,视觉上更自然
            float progress = Mathf.SmoothStep(0f, 1f, t / _duration);
            _mat.SetFloat(ThresholdID, Mathf.Lerp(from, to, progress));
            yield return null;
        }
        _mat.SetFloat(ThresholdID, to);
    }
}

⚠️ 注意: 务必使用 renderer.material(而非 sharedMaterial) 获取独立副本,否则修改会影响场景中所有使用该材质的物体。 若需批量溶解同材质物体,应改用 MaterialPropertyBlock

六、Shader Graph 节点等效实现

如果你偏好可视化编辑,下图展示了与上述 HLSL 等价的 Shader Graph 节点连接方式:

🔗 Shader Graph 中没有内置 Clip 节点,需使用 Alpha Clip Threshold (在 Fragment 节点 Alpha 接口配合 Alpha Clip 开关), 或手动连接 Step + Multiply 节点模拟丢弃效果。 手写 HLSL 更直接灵活,推荐在复杂场景下使用。

七、材质参数速查

参数 类型 范围 说明
_MainTex Texture2D --- 物体主贴图(Albedo)
_NoiseTex Texture2D --- 灰度噪声纹理,驱动溶解形状
_Threshold Float [0, 1] 溶解阈值:0 = 完整,1 = 完全溶解
_EdgeWidth Float [0, 0.2] 边缘发光宽度(噪声空间,非世界空间)
_EdgeColor Color (HDR) --- 边缘发光颜色,建议 HDR 开启以配合 Bloom
_EdgeIntensity Float [1, 10] 边缘强度倍数,>1 产生 HDR 溢出,触发 Bloom

八、进阶拓展思路

8.1 方向性溶解

将溶解从"随机"改为"有方向":用顶点的世界空间 Y 坐标与噪声混合, 可以实现"从脚底向上燃烧"的溶解效果。

cs 复制代码
// 将世界空间高度归一化后混合进噪声值
float heightFactor = (_WorldSpaceCameraPos.y - input.positionWS.y) * _HeightScale;
half  dissolveVal  = noiseVal + saturate(heightFactor);
clip(dissolveVal - _Threshold);

8.2 世界空间噪声(消除 UV 拉伸)

对于没有 UV 展开或 UV 拉伸严重的模型,用世界空间 XZ 坐标作为噪声采样 UV, 溶解形状在模型表面分布更均匀。

cs 复制代码
// 顶点着色器中传出世界坐标
output.positionWS = TransformObjectToWorld(input.positionOS.xyz);
// 片元着色器:用世界 XZ 作为噪声 UV
float2 worldUV   = input.positionWS.xz * _NoiseScale;
half   noiseVal  = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, worldUV).r;

8.3 配合 MaterialPropertyBlock 批量控制

场景中同一材质的多个物体需要不同溶解进度 (如死亡的每个敌人各自独立消散)时, 使用 MaterialPropertyBlock 避免实例化多份材质:

cs 复制代码
private MaterialPropertyBlock _mpb;
private Renderer _renderer;
void Awake()
{
    _renderer = GetComponent<Renderer>();
    _mpb = new MaterialPropertyBlock();
}
void SetThreshold(float t)
{
    _renderer.GetPropertyBlock(_mpb);
    _mpb.SetFloat(ThresholdID, t);
    _renderer.SetPropertyBlock(_mpb);  // 不产生新的 draw call 合批
}

九、性能注意事项

  • clip / discard 不等于免费:被 clip 的像素不写入颜色缓冲,但在 Tile-Based GPU(移动端)上仍会影响 Early-Z 优化,大量使用时需测量 overdraw。
  • 关闭阴影接收的 clip 同步 :ShadowCaster Pass 中也需要同样的 clip 逻辑,否则溶解物体仍然投射完整阴影。记得为 ShadowCaster Pass 添加相同的噪声采样和 clip
  • 噪声纹理压缩:使用 R8 或 BC4 压缩格式,节省显存带宽。灰度噪声不需要 RGB 通道。
  • 动态批处理 :使用 MaterialPropertyBlock 可保持动态批处理,使用 .material 实例化则会打破批次。

十、小结

溶解效果的实现思路极为简洁,但细节决定品质:

通过噪声纹理 提供天然的不规则性、 clip 函数 实现无中间灰度的硬截断、 边缘宽度 构造发光带、 C# 协程 驱动 _Threshold 随时间变化------ 四个模块各司其职,任何一个都可以单独替换和扩展, 比如把噪声换成 SDF 字体,就能得到文字侵蚀效果; 把方向性溶解与粒子系统结合,就是经典的死亡灰烬特效。

掌握这一套思路,你会发现 URP Shader 里的诸多"炫技"效果, 本质上都是噪声 × 阈值 × 片元丢弃的变体。

相关推荐
CheerWWW3 小时前
GameFramework——Download篇
笔记·学习·unity·c#
mxwin3 小时前
Unity URP 下的 Early-Z / Depth Prepass 解决复杂片元着色器造成的 Overdraw 问题
unity·游戏引擎·着色器
mxwin3 小时前
Unity Shader 顶点色:利用模型顶点颜色传递渲染数据
unity·游戏引擎·shader
星夜泊客5 小时前
Unity 排行榜 UI 优化:从全量生成到滚动复用
ui·unity·性能优化·游戏引擎
CDN3605 小时前
游戏盾导致 Unity/UE 引擎崩溃?内存占用、SO 库冲突深度排查
游戏·unity·游戏引擎
心前阳光6 小时前
Unity之Luban使用流程
unity·游戏引擎
mxwin6 小时前
Unity URP 下的 GPU Instancing减少 DrawCall 的关键技术
unity·游戏引擎·shader
小贺儿开发7 小时前
Unity3D LED点阵屏幕模拟
http·unity·浏览器·网络通信·led·互动·点阵屏
RReality8 小时前
【Unity Shader】 溶解效果实战教程
unity·游戏引擎