US4L8-3——消融效果

消融效果

消融效果是模拟物体逐渐从屏幕上消失或溶解的过程,它通常利用噪声纹理实现,使物体按照某种规则逐渐透明或完全不可见。

这种效果常用于:

  1. 角色死亡、传送场景
  2. 魔法消失,比如燃烧、消失等

image

消融效果基本原理

一句话总结消融效果基本原理:通过对比噪声纹理值与消融进度参数,剔除低于阈值的像素,同时在边缘添加渐变颜色实现动态溶解效果。

关键点:

  • 如何剔除像素

    我们在片元着色器中对噪声纹理进行采样,由于噪声纹理是灰度图,只需取出其中的RGB中的任意一通道的颜色来使用。
    再自定义一个用于控制消融进度的参数(0~1),最后利用该片元的 噪声纹理值 减去 进度参数
    若小于 0 则不渲染该片元,通过控制进度参数,便可以控制消融程度了

    image

    Unity Shader中 提供了一个内置函数 clip(x)​,它的作用就是在片元着色器中调用时来丢弃片元的,
    传入的值x小于0,则会丢弃当前片元,被丢弃的片元不会被进一步处理也就不会被渲染了

    相关内容可见:US3S3L4——透明度测试

    1
    2
    3
    4
    5
    6
    fixed4 frag(v2f i) : SV_Target
    {
    // 噪声图上的颜色是线性随机的,我们通过一个消融阈值来控制,当颜色值(0~1)的值小于该阈值时,抛弃对应像素不渲染,使其出现局部镂空效果
    fixed3 noiseColor = tex2D(_Noise, i.uv2.xy).rgb;
    clip(noiseColor.r - _Dissolve);
    }
  • 如何处理边缘

    在处理边缘渐变颜色效果时,我们将使用 Unity 中内置的三个 Shader 函数:

    • smoothstep(a, b, x)

      a​ 起始值;b​ 结束值;x​ 输入值(用于在 a​ 和 b​ 之间平滑插值),
      x < a​ 时,返回 0​;当 x > b​ 时,返回 1​;a < x < b​ 时,返回 0~1​ 之间的值

    • lerp(a, b, t)

      a​ 起始值;b​ 结束值;t​ 插值因子,
      t = 0​ 时,返回 a​;当 t = 1​ 时,返回 b​;当 0 < t < 1​ 时;返回 a​ 和 b​ 之间的值

    • step(value, x)

      value​ 阈值,x​ 输入值;两值用于比较,x < value​,返回 0​;x >= value​,返回 1

    首先我们利用 smoothstep​ 函数决定边缘颜色,我们利用 噪声颜色值 减去 消融进度值 得到一个 剔除阈值Value
    然后自定义一个边缘范围值 _Range​,然后用 smoothstep​ 函数来得到一个值 t​,我们根据这个 t​ 来从渐变纹理采样
    这样就能做到,片元越接近消融的边缘,颜色就越接近纹理右侧的颜色的效果

    image

    接着,我们在原本的颜色和渐变颜色之间进行 lerp​ 插值,决定使用哪个颜色

    1
    2
    3
    4
    5
    // 边缘越趋近于不渲染的像素点,noiseColor.r - _Dissolve的值就越小,进而t也就越趋近于1
    fixed t = 1 - smoothstep(0, _Range, noiseColor.r - _Dissolve);
    fixed3 gradientColor = tex2D(_Gradient, fixed2(t, 0.5)).rgb; // t越趋近于1,颜色就越趋近于外火焰颜色(渐变纹理右侧颜色)
    // 越趋近于边缘的像素点,越应该趋近于使用渐变纹理中的颜色,而step(0.00001, _Dissolve)的目的是当_Dissolve为0时,始终不会使用渐变纹理的颜色
    fixed3 finalColor = lerp(color, gradientColor, t * step(0.00001, _Dissolve));

    利用 smoothstep​ 结合消融阈值来决定在渐变纹理中采用的渐变颜色,
    利用 lerp​ 来决定在原始颜色和边缘渐变颜色中使用哪个颜色,
    利用 step​ 来确保尚未消融时(即 _Dissolve​ 为 0 时)不会使用渐变颜色,
    利用自定义参数来决定边缘范围,从而实现消融边缘渐变色

消融效果具体实现

使用如下的噪声图和渐变纹理:

Noise​​Gradient​​​

  1. 新建 Shader Dissolve​,复用切线空间下法线纹理 Shader

    代码详见:US3S2L6——切线空间下计算法线纹理贴图

  2. 添加属性

    • _Noise​ 噪声纹理
    • _Gradient​ 渐变纹理
    • _Dissolve​ 消融进度
    • _Range​ 边缘范围

    添加属性映射

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Properties
    {
    _MainColor("MainColor", Color) = (1, 1, 1, 1)
    _MainTex("MainTex", 2D) = ""{}
    _BumpMap("BumpMap", 2D) = ""{}
    _BumpScale("BumpScale", Range(0, 1)) = 1
    _SpecularColor("SpecularColor", Color) = (1, 1, 1, 1)
    _SpecularNum("SpecularNum", Range(0, 20)) = 18
    _Noise("Noise", 2D) = ""{} // 噪声纹理
    _Gradient("Gradient", 2D) = ""{} // 渐变纹理
    _Dissolve("Dissolve", Range(0, 1)) = 0 // 消融进度
    _EdgeRange("EdgeRange", Range(0, 1)) = 0 // 消融边界范围
    }
  3. 结构体 加入一个噪声纹理 UV

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct v2f
    {
    float4 pos: SV_POSITION;
    //float2 uvTex: TEXCOORD0;
    //float2 uvBump: TEXCOORD1; //可以使用两个float2来分别存储主要纹理的uv和法线纹理的uv
    float4 uv: TEXCOORD0; //可以使用一个float4来同时存储主要纹理的uv(xy存储)和法线纹理的uv(zw存储)
    float2 uvNoise: TEXCOORD1; //噪声纹理的UV,用于之后计算偏移缩放
    float3 lightDir: TEXCOORD2; //相对于切线空间下的光的方向
    float3 viewDir: TEXCOORD3; //相对于切线空间下的视角方向
    };
  4. 顶点着色器

    计算噪声纹理的偏移和缩放

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    v2f vert (appdata_full v)
    {
    v2f data;

    data.pos = UnityObjectToClipPos(v.vertex); //计算裁剪空间下顶点坐标
    // 分别计算主纹理和法线纹理和噪声纹理的缩放平移
    data.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
    data.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
    data.uvNoise = v.texcoord.xy * _Noise_ST.xy + _Noise_ST.zw;

    // 计算副切线
    float3 binormal = cross(normalize(v.tangent), normalize(v.normal)) * v.tangent.w;
    // 得到模型空间到切线空间的转换矩阵
    float3x3 rotation = float3x3(
    v.tangent.xyz, //切线
    binormal, //副切线
    v.normal //法线
    );

    data.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)); // 切线空间下的光的方向
    data.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)); // 切线空间下的视角方向
    data.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 世界空间下的顶点位置

    return data;
    }
  5. 片元着色器

    1. 剔除效果制作

      从噪声纹理中采样,利用 clip​ 函数进行剔除

    2. 边缘渐变采样,范围控制,颜色插值

      • 利用 smoothstep​ 函数计算出采样系数
      • 利用 lerp​ 函数决定是用哪个颜色
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    fixed4 frag (v2f i) : SV_Target
    {
    // 剔除 —— 消融
    fixed3 noiseColor = tex2D(_Noise, i.uvNoise.xy).rgb;
    // 用三目运算符来确保完全消融时不会出现出现残留
    clip(_Dissolve == 1 ? -1 : noiseColor.r - _Dissolve);

    float4 packedNormal = tex2D(_BumpMap, i.uv.zw); // 获取法线纹理的颜色数据
    float3 tangentNormal = UnpackNormal(packedNormal); // 将颜色数据逆运算并解压缩,得到切线空间下法线数据
    // 将法线数据的xy乘以凹凸系数,根据xy修正z,避免凹凸系数影响光照亮度
    tangentNormal.xy *= _BumpScale;
    tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));

    // 计算带纹理颜色的BlinnPhong光照计算,这里使用已经计算好的切线数据
    fixed3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _MainColor.rgb; // 反射率

    // 漫反射光照计算:这里需要使用已经计算完毕的切线数据和光照方向,注意!这里的 tangentNormal 无需归一化
    fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(tangentNormal, normalize(i.lightDir)));
    // 高光反射光照计算:这里需要使用已经计算完毕的切线数据和光照方向
    float3 halfA = normalize(normalize(i.viewDir) + normalize(i.lightDir)); // 半角向量
    fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(tangentNormal, halfA)), _SpecularNum);
    // 最终颜色计算
    fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;

    // 需要在模型原本的颜色 和 消融边缘的颜色之间 选择
    fixed value = 1 - smoothstep(0, _EdgeRange, noiseColor.r - _Dissolve);
    fixed3 gradientColor = tex2D(_Gradient, fixed2(value, 0.5)).rgb;
    // step(0.00001, _Dissolve)的目的是当_Dissolve为0时,始终不会使用渐变纹理的颜色
    fixed3 finalColor = lerp(color, gradientColor, value * step(0.00001, _Dissolve));

    return fixed4(finalColor.rgb, 1);
    }
  6. 阴影相关添加

    • 加入 SHADOW_COORDS​、TRANSFER_SHADOW​、UNITY_LIGHT_ATTENUATION
    • 加入 FallBack "Diffuse"
  7. 阴影消融效果处理

    目前投射的阴影是没有消融效果的:

    image

    因此还需要以下的步骤让影子也可以消融:

    1. 复用自定义投射阴影 Shader 相关代码

      代码详见:US3S6L3——让物体投射阴影 的 让物体投射阴影的部分

    2. 加入噪声纹理和消融进度属性映射

    3. 结构体中加入 UV

      1
      2
      3
      4
      5
      6
      7
      8
      9
      struct v2f
      {
      float2 uvNoise: TEXCOORD0; //噪声纹理的UV,用于之后计算偏移缩放
      V2F_SHADOW_CASTER; // 顶点到片元着色器阴影投射结构体数据宏,定义了一些标准的成员变量,这些变量用于在阴影投射路径中传递顶点数据到片元着色器
      };

      sampler2D _Noise; // 噪声纹理
      float4 _Noise_ST; // 噪声纹理的缩放和平移
      fixed _Dissolve; // 消融进度
    4. 顶点着色器中计算 UV 缩放偏移

      1
      2
      3
      4
      5
      6
      7
      v2f vert(appdata_base v)
      {
      v2f data;
      data.uvNoise = v.texcoord.xy * _Noise_ST.xy + _Noise_ST.zw; // 计算噪声纹理的缩放平移
      TRANSFER_SHADOW_CASTER_NORMALOFFSET(data); // 转移阴影投射器法线偏移宏,用于在顶点着色器中计算和传递阴影投射所需的变量
      return data;
      }
    5. 片元着色器中剔除

      1
      2
      3
      4
      5
      6
      7
      8
      fixed4 frag(v2f i) : SV_Target
      {
      // 剔除 —— 消融
      fixed3 noiseColor = tex2D(_Noise, i.uvNoise.xy).rgb;
      // 用三目运算符来确保完全消融时不会出现出现残留
      clip(_Dissolve == 1 ? -1 : noiseColor.r - _Dissolve);
      SHADOW_CASTER_FRAGMENT(i); //阴影投射片元宏,将深度值写入到阴影映射纹理中
      }

完整 Shader 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
Shader "TeachShader/Dissolve"
{
Properties
{
_MainColor("MainColor", Color) = (1, 1, 1, 1)
_MainTex("MainTex", 2D) = ""{}
_BumpMap("BumpMap", 2D) = ""{}
_BumpScale("BumpScale", Range(0, 1)) = 1
_SpecularColor("SpecularColor", Color) = (1, 1, 1, 1)
_SpecularNum("SpecularNum", Range(0, 20)) = 18
_Noise("Noise", 2D) = ""{} // 噪声纹理
_Gradient("Gradient", 2D) = ""{} // 渐变纹理
_Dissolve("Dissolve", Range(0, 1)) = 0 // 消融进度
_EdgeRange("EdgeRange", Range(0, 1)) = 0 // 消融边界范围
}
SubShader
{
// Bass Pass 基础渲染通道
Pass
{
Tags { "LightMode" = "ForwardBase" }

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase

#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"

struct v2f
{
float4 pos: SV_POSITION;
//float2 uvTex: TEXCOORD0;
//float2 uvBump: TEXCOORD1; //可以使用两个float2来分别存储主要纹理的uv和法线纹理的uv
float4 uv: TEXCOORD0; //可以使用一个float4来同时存储主要纹理的uv(xy存储)和法线纹理的uv(zw存储)
float2 uvNoise: TEXCOORD1; //噪声纹理的UV,用于之后计算偏移缩放
float3 lightDir: TEXCOORD2; //相对于切线空间下的光的方向
float3 viewDir: TEXCOORD3; //相对于切线空间下的视角方向
float3 worldPos: TEXCOORD4; //世界空间下的顶点坐标
SHADOW_COORDS(5) //阴影坐标宏
};

float4 _MainColor; // 漫反射颜色
sampler2D _MainTex; // 颜色纹理
float4 _MainTex_ST; // 颜色纹理的缩放和平移
sampler2D _BumpMap; // 法线纹理
float4 _BumpMap_ST; // 法线纹理的缩放和平移
float _BumpScale; // 凹凸程度
float4 _SpecularColor; // 高光颜色
fixed _SpecularNum; // 光泽度
sampler2D _Noise; // 噪声纹理
float4 _Noise_ST; // 噪声纹理的缩放和平移
sampler2D _Gradient; // 渐变纹理
float4 _Gradient_ST; // 渐变纹理的缩放和平移
fixed _Dissolve; // 消融进度
fixed _EdgeRange; // 消融边界范围

v2f vert (appdata_full v)
{
v2f data;

data.pos = UnityObjectToClipPos(v.vertex); //计算裁剪空间下顶点坐标
// 分别计算主纹理和法线纹理和噪声纹理的缩放平移
data.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
data.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
data.uvNoise = v.texcoord.xy * _Noise_ST.xy + _Noise_ST.zw;

// 计算副切线
float3 binormal = cross(normalize(v.tangent), normalize(v.normal)) * v.tangent.w;
// 得到模型空间到切线空间的转换矩阵
float3x3 rotation = float3x3(
v.tangent.xyz, //切线
binormal, //副切线
v.normal //法线
);

data.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)); // 切线空间下的光的方向
data.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)); // 切线空间下的视角方向
data.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 世界空间下的顶点位置
TRANSFER_SHADOW(data); // 阴影转换坐标相关宏

return data;
}

fixed4 frag (v2f i) : SV_Target
{
// 剔除 —— 消融
fixed3 noiseColor = tex2D(_Noise, i.uvNoise.xy).rgb;
// 用三目运算符来确保完全消融时不会出现出现残留
clip(_Dissolve == 1 ? -1 : noiseColor.r - _Dissolve);

float4 packedNormal = tex2D(_BumpMap, i.uv.zw); // 获取法线纹理的颜色数据
float3 tangentNormal = UnpackNormal(packedNormal); // 将颜色数据逆运算并解压缩,得到切线空间下法线数据
// 将法线数据的xy乘以凹凸系数,根据xy修正z,避免凹凸系数影响光照亮度
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));

// 计算带纹理颜色的BlinnPhong光照计算,这里使用已经计算好的切线数据
fixed3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _MainColor.rgb; // 反射率

// 漫反射光照计算:这里需要使用已经计算完毕的切线数据和光照方向,注意!这里的 tangentNormal 无需归一化
fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(tangentNormal, normalize(i.lightDir)));
// 高光反射光照计算:这里需要使用已经计算完毕的切线数据和光照方向
float3 halfA = normalize(normalize(i.viewDir) + normalize(i.lightDir)); // 半角向量
fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(tangentNormal, halfA)), _SpecularNum);
// 光照衰减计算
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
// 最终颜色计算
fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor * atten + specularColor;

// 需要在模型原本的颜色 和 消融边缘的颜色之间 选择
fixed value = 1 - smoothstep(0, _EdgeRange, noiseColor.r - _Dissolve);
fixed3 gradientColor = tex2D(_Gradient, fixed2(value, 0.5)).rgb;
// step(0.00001, _Dissolve)的目的是当_Dissolve为0时,始终不会使用渐变纹理的颜色
fixed3 finalColor = lerp(color, gradientColor, value * step(0.00001, _Dissolve));

return fixed4(finalColor.rgb, 1);
}
ENDCG
}

// ShadowCaster Pass 投影阴影通道
Pass
{
Tags { "LightMode" = "ShadowCaster" }

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 告诉Unity编译器生成多个着色器变体,用于支持不同类型的阴影(SM,SSSM等等),确保着色器能够在所有可能的阴影投射模式下正确渲染
#pragma multi_compile_shadowcaster

#include "UnityCG.cginc"

struct v2f
{
float2 uvNoise: TEXCOORD0; //噪声纹理的UV,用于之后计算偏移缩放
V2F_SHADOW_CASTER; // 顶点到片元着色器阴影投射结构体数据宏,定义了一些标准的成员变量,这些变量用于在阴影投射路径中传递顶点数据到片元着色器
};

sampler2D _Noise; // 噪声纹理
float4 _Noise_ST; // 噪声纹理的缩放和平移
fixed _Dissolve; // 消融进度

v2f vert(appdata_base v)
{
v2f data;
data.uvNoise = v.texcoord.xy * _Noise_ST.xy + _Noise_ST.zw; // 计算噪声纹理的缩放平移
TRANSFER_SHADOW_CASTER_NORMALOFFSET(data); // 转移阴影投射器法线偏移宏,用于在顶点着色器中计算和传递阴影投射所需的变量
return data;
}

fixed4 frag(v2f i) : SV_Target
{
// 剔除 —— 消融
fixed3 noiseColor = tex2D(_Noise, i.uvNoise.xy).rgb;
// 用三目运算符来确保完全消融时不会出现出现残留
clip(_Dissolve == 1 ? -1 : noiseColor.r - _Dissolve);
SHADOW_CASTER_FRAGMENT(i); //阴影投射片元宏,将深度值写入到阴影映射纹理中
}
ENDCG
}

}

Fallback "Diffuse"
}

显示效果(消融进度0.5,消融边界范围0.1):

image

可见,控制消融进度即可让模型和阴影呈现消融效果,之后我们就可以通过 C# 脚本等方式来控制模型的动态消融