一、效果总览
溶解效果(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);
当 _Threshold 从 0 向 1 增大时, 越来越多的像素满足 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 逻辑,否则溶解物体仍然投射完整阴影。记得为
ShadowCasterPass 添加相同的噪声采样和clip。 - 噪声纹理压缩:使用 R8 或 BC4 压缩格式,节省显存带宽。灰度噪声不需要 RGB 通道。
- 动态批处理 :使用
MaterialPropertyBlock可保持动态批处理,使用.material实例化则会打破批次。
十、小结
溶解效果的实现思路极为简洁,但细节决定品质:

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