从原理到实践,完整剖析如何在 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 与顶点动画结合制作更复杂的形变溶解。