US4L8-3——消融效果
US4L8-3——消融效果
消融效果
消融效果是模拟物体逐渐从屏幕上消失或溶解的过程,它通常利用噪声纹理实现,使物体按照某种规则逐渐透明或完全不可见。
这种效果常用于:
- 角色死亡、传送场景
- 魔法消失,比如燃烧、消失等
消融效果基本原理
一句话总结消融效果基本原理:通过对比噪声纹理值与消融进度参数,剔除低于阈值的像素,同时在边缘添加渐变颜色实现动态溶解效果。
关键点:
-
如何剔除像素
我们在片元着色器中对噪声纹理进行采样,由于噪声纹理是灰度图,只需取出其中的RGB中的任意一通道的颜色来使用。
再自定义一个用于控制消融进度的参数(0~1),最后利用该片元的 噪声纹理值 减去 进度参数
若小于 0 则不渲染该片元,通过控制进度参数,便可以控制消融程度了
Unity Shader中 提供了一个内置函数
clip(x)
,它的作用就是在片元着色器中调用时来丢弃片元的,
传入的值x小于0,则会丢弃当前片元,被丢弃的片元不会被进一步处理也就不会被渲染了相关内容可见:US3S3L4——透明度测试
1
2
3
4
5
6fixed4 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
来从渐变纹理采样
这样就能做到,片元越接近消融的边缘,颜色就越接近纹理右侧的颜色的效果
接着,我们在原本的颜色和渐变颜色之间进行
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 时)不会使用渐变颜色,
利用自定义参数来决定边缘范围,从而实现消融边缘渐变色 -
消融效果具体实现
使用如下的噪声图和渐变纹理:
-
新建 Shader
Dissolve
,复用切线空间下法线纹理 Shader代码详见:US3S2L6——切线空间下计算法线纹理贴图
-
添加属性
-
_Noise
噪声纹理 -
_Gradient
渐变纹理 -
_Dissolve
消融进度 -
_Range
边缘范围
添加属性映射
1
2
3
4
5
6
7
8
9
10
11
12
13Properties
{
_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 // 消融边界范围
} -
-
结构体 加入一个噪声纹理 UV
1
2
3
4
5
6
7
8
9
10struct 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; //相对于切线空间下的视角方向
}; -
顶点着色器
计算噪声纹理的偏移和缩放
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25v2f 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;
} -
片元着色器
-
剔除效果制作
从噪声纹理中采样,利用
clip
函数进行剔除 -
边缘渐变采样,范围控制,颜色插值
- 利用
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
32fixed4 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);
} -
-
阴影相关添加
- 加入
SHADOW_COORDS
、TRANSFER_SHADOW
、UNITY_LIGHT_ATTENUATION
- 加入
FallBack "Diffuse"
- 加入
-
阴影消融效果处理
目前投射的阴影是没有消融效果的:
因此还需要以下的步骤让影子也可以消融:
-
复用自定义投射阴影 Shader 相关代码
代码详见:US3S6L3——让物体投射阴影 的 让物体投射阴影的部分
-
加入噪声纹理和消融进度属性映射
-
结构体中加入 UV
1
2
3
4
5
6
7
8
9struct v2f
{
float2 uvNoise: TEXCOORD0; //噪声纹理的UV,用于之后计算偏移缩放
V2F_SHADOW_CASTER; // 顶点到片元着色器阴影投射结构体数据宏,定义了一些标准的成员变量,这些变量用于在阴影投射路径中传递顶点数据到片元着色器
};
sampler2D _Noise; // 噪声纹理
float4 _Noise_ST; // 噪声纹理的缩放和平移
fixed _Dissolve; // 消融进度 -
顶点着色器中计算 UV 缩放偏移
1
2
3
4
5
6
7v2f vert(appdata_base v)
{
v2f data;
data.uvNoise = v.texcoord.xy * _Noise_ST.xy + _Noise_ST.zw; // 计算噪声纹理的缩放平移
TRANSFER_SHADOW_CASTER_NORMALOFFSET(data); // 转移阴影投射器法线偏移宏,用于在顶点着色器中计算和传递阴影投射所需的变量
return data;
} -
片元着色器中剔除
1
2
3
4
5
6
7
8fixed4 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 | Shader "TeachShader/Dissolve" |
显示效果(消融进度0.5,消融边界范围0.1):
可见,控制消融进度即可让模型和阴影呈现消融效果,之后我们就可以通过 C# 脚本等方式来控制模型的动态消融