溶解效果在游戏中是很常见的,比如在一些神话或者魔法世界中,一些NPC角色在剧情需要时候会身体会渐渐的消失掉.甚至有一些更炫的,比如用火焰喷射器把目标燃尽。这些都可以用到溶解效果。这篇文章主要是讲解一下比较基础的溶解效果如何实现,实现的方法并不唯一,本篇只是其中一种思路。

原理

既然想让角色的身体一块块渐渐消失,不妨就让角色身体上相应的部位不进行渲染(或者改成透明,我们这里选择前者)。那根据什么来判断身体的哪一部分需要被溶解呢,这时候就需要一张额外的贴图或者利用角色纹理贴图的Alpha通道(本篇选择前者)。这个贴图和纹理贴图一样,对应着玩家身体的每一个位置,这样我们就可以根据贴图上某个指定通道的颜色值来控制角色各个身体部位是否溶解了。

另外我们还会涉及到的一个命令叫discard,是由CG提供的,若出现在fragmentShader中表示立即放弃当前处理的片元。也就是说当我们判定当前片远需要溶解的时候我们就使用discard命令。

泰课在线

上面这张图就是用来控制溶解程度的纹理,我们这里比较简单,只使用了R通道,如果你想做的很复杂也可以利用上其它的通道。在本篇中我们将根据时间的推进不断溶解掉贴图上R通道颜色值较小的区域。美术可以利用这张贴图控制角色的任意溶解顺序和效果。由于我的素材都是随便找的,自己又不会画画,所以这个图和角色纹理可能并不搭配,请不要在意。

再看下面这个图,我先来提前说一下本篇Shader在Properties中提供的各个可调节的参数

  1. Base(RGB)是角色纹理贴图,不用解释了.
  2. NoiseTex(R)是很重重要的,我们用来控制角色溶解的样式贴图,我们只利用了R通道。
  3. DissolveSpeed(Second),整个溶解过程需要的时间,单位是秒
  4. EdgeWidth,这个就是额外加的一个边缘效果,比如你观察纸在化作灰烬时他的周围会先变成黑褐色。我们这里的EdgeWidth就是定义这个周围区域的大小,注意这个width并不是指的长度,而是定义一个透明度的间隔区间,也就是与基准值相差多少可以算作边缘处理。
  5. EdgeColor,和4一样,边缘效果的颜色。
实现
 1 Shader "Esfog/Dissolve" 
 2 {
 3     Properties 
 4     {
 5         _MainTex ("Base (RGB)", 2D) = "white" {}
 6         _NoiseTex ("NoiseTex (R)",2D) = "white"{}
 7         _DissolveSpeed ("DissolveSpeed (Second)",Float) = 1
 8         _EdgeWidth("EdgeWidth",Range(0,0.5)) = 0.1
 9         _EdgeColor("EdgeColor",Color) =  (1,1,1,1)
10     }
11     SubShader 
12     {
13         Tags { "RenderType"="Opaque" }
14         
15         Pass
16         {
17             CGPROGRAM
18             #pragma vertex vert_img
19             #pragma fragment frag
20             #include "UnityCG.cginc"
21              
22             uniform sampler2D _MainTex;
23             uniform sampler2D _NoiseTex;
24             uniform float _DissolveSpeed;
25             uniform float _EdgeWidth;
26             uniform float4 _EdgeColor;
27             
28             float4 frag(v2f_img i):COLOR
29             {
30                 float DissolveFactor = saturate(_Time.y / _DissolveSpeed);
31                 float noiseValue = tex2D(_NoiseTex,i.uv).r;            
32                 if(noiseValue <= DissolveFactor)
33                 {
34                     discard;
35                 }
36                 
37                 float4 texColor = tex2D(_MainTex,i.uv);
38                 float EdgeFactor = saturate((noiseValue - DissolveFactor)/(_EdgeWidth*DissolveFactor));
39                 float4 BlendColor = texColor * _EdgeColor;
40                                 
41                 return lerp(texColor,BlendColor,1 - EdgeFactor);
42             }
43             
44             ENDCG
45         }
46     } 
47     
48     FallBack Off
49 }

5~9行,前面解释过了,这里就不再说明了。

18行,这里我使用了unity提供的vert_img顶点着色器,因为我们的需求很简单,只需要进行顶点坐标变换以及把uv传给后面就行了,既然有现成的就不自己写了,它会自动把结果返回到一个v2f_img结构体中.

30行,这里的_Time.y就是从物体一开始渲染到现在所过去的时间unity给我们提供了几个不同的时间参数都保存在_Time中,其中_Time.y是标准的时间,单位(s)。用它去除以_DissolveSpeed.就能得到当前时间已经占用了多少总溶解时间.由于我们要保证比值范围限定在(0,1)的范围内,所以最后调用一个saturate()函数,这个函数式CG提供的就是来完成这个目的的。

31行,从溶解贴图里面取出r通道的值.

32~35行,我们利用刚才算出比值来对溶解贴图里面的R通道颜色值进行判断,如果R通道颜色值小于等于这个比值那么我们就通过discard抛弃当前片远,当前片远的处理会立刻结束。

38行,又是计算一个比值,分子是noiseValue - DissolveFactor,这个值表示溶解贴图上的R通道值和目前的溶解基准值相差多少,而(_EdgeWidth*DissolveFactor)可以这样理解,_EdgeWidth表示可以多大的差值可以算作是边缘,而乘以一个DissolveFactor,就表示边缘最大宽度会随着时间变化时间越长,宽度越宽。最后这两个值相除就代表当前片元的边缘程度,值越大表示离他被溶解掉的时间越长,反之表示他很快就要被溶解掉了.

39行,我们计算一下当前纹理颜色与边缘颜色相乘的值,后面需要用。

41行,这里我们用lerp函数来利用上一步计算出来的(1-EdgeFactor)对原始纹理颜色和上一步中的混合颜色进行一个插值,在解释第38行时候我们已经说了,EdgeFactor越小表示离溶解时间越近,也就是说它边缘化的颜色成分越重,那么由于lerp(x,y,a) = x*(1-a) + y*a;为了让边缘化的颜色重一些我们就使用1-EdgeFactor作为因子了,反之亦然。

好了看一下效果,使用不同的溶解贴图会产生完全不同的效果,下面展示一下我从网上随便找到的两个贴图,由于和原模型纹理并不是一套的,所以看上去会有点怪,不要在意。
使用这个贴图1:
效果如下:
使用贴图2:
效果如下:

好了基本效果就是如此,总体来说这个Shader的效果看起来好与不好,主要还是取决于美术提供的溶解贴图是否合适,本节素材并不是一套的,看起来会有点怪。

另外一点就是大家在学习Shader的过程中你总会发现别人打源代码中会出现很多pow,lerp,或者对各个参数的加减乘除,希望大家不要太纠结这些地方的意义,有时候确实是有一定的数学原理,但很多时候都是开发者自己写的一个经验公式,为了调效果而写的。随着你的经验慢慢增多,渐渐的你也会开始使用一些经验公式。效果对了就好,只要不影响性能。毕竟给玩家看的又不是源代码。