今天我们分享一个Unity中非常常见的特效: 溶解.
下面先来看看效果.
大家可能和我一样, 在不了解实现方式的时候, 会觉得这个效果很神奇, 实现起来十分复杂.
但是一旦你了解之后, 会觉得…更神奇, 简单到神奇.
溶解特效的实现原理非常非常简单, 一句话概况就是: 给定一个变化量, 在值变化时, 抛弃对象的一部分像素不进行渲染.
所谓给定一个变化量, 我们可以将其抽象为一个进度条Slider, 代表溶解程度, 从0到1, 0代表一点都不溶解, 1代表全部溶解.
也就是说, Slider从0到1, 溶解效果从无到有.
那么怎么做到抛弃对象一部分像素不进行渲染呢.
依然很简单, 在片段着色器中使用clip
函数, 抛弃掉不满足要求的片段.
clip(value)
函数的效果是: 当传入的value小于0时, 则抛弃本片段, 内部的实现类似:
void clip(int value)
{
if (value < 0)
discard;
}
那么我们唯一的问题就是这个value该怎么传了.
我们通过Slider取值, 作为一个阈值(threshold value), 所有小于该阈值的片段颜色(任选颜色向量x, y, z, w的一个值作为比较)丢弃.
fixed4 color = tex2D(_MainTex, data.uv);
clip(color.r - threshold);
这样, 随着阈值的增大, 对象身上的颜色值大的片段会慢慢被丢弃, 在视觉上就形成了溶解效果. 代码如下:
Shader "Dissolve_Origin"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
// 通过Slider取阈值
_DissolveThreshold("DissolveThreshold", Range(0, 1)) = 0
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _DissolveThreshold;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
fixed4 dissolveCol = col;
// 从噪声纹理采样颜色, 如果该值[小于阈值]则丢弃本片段
// 比如阈值为0.1, 则在噪声纹理上采样的所有像素r值小于0.1的片段都会被丢弃
// 即噪声纹理上偏黑的颜色(->0)首先开始溶解, 偏白的颜色(->1)最后溶解
clip(dissolveCol.r - _DissolveThreshold);
return col;
}
ENDCG
}
}
}
大家可以看到, 只是在最基本的着色器代码上加了区区4行代码就达到的一个酷炫的效果, 甚至真正起效的只有一行代码.
这个效果和第一张图有所差别, 是因为仅仅使用了对象自身的纹理信息, 效果比较受限.
业界常用的实现是还需要添加另一张噪声纹理, 用来指定溶解的区域和程度, 我们下面就开始介绍.
首先我们将纹理直接替换为另一张纹理, Shader不做任何变化, 可以看到下面的效果:
可以看到溶解是从黑色(0, 0, 0)的部分开始的, 渐渐溶解到白色的部分.
如果将对象的纹理信息和溶解信息分开, 也就是说使用另一张"溶解纹理(实际上被称为噪声纹理)", 我们就可以操作溶解的区域和速度.
噪声纹理的用途可谓是十分广, 很多特效都是使用噪声纹理, 比如水的流动, 旗帜的飘动, 裂缝等, 后面有机会我们会一一看到.
这里简单理解噪声纹理的意义就是: 将原来对象的纹理进行"扰动", 这些扰动信息使用单独的一张纹理来表示.
那么这是怎么实现呢?
其实也很简单, 在片段着色器中对对象本身的纹理正常采样, 然后使用同一个uv坐标在噪声纹理上采样, 使用该值来判断是否丢弃:
fixed4 frag (v2f i) : SV_Target
{
fixed4 dissolveCol = tex2D(_DissolveTex, i.uv);
// 从噪声纹理采样颜色, 如果该值[小于阈值]则丢弃本片段
// 比如阈值为0.1, 则在噪声纹理上采样的所有像素r值小于0.1的片段都会被丢弃
// 即噪声纹理上偏黑的颜色(->0)首先开始溶解, 偏白的颜色(->1)最后溶解
clip(dissolveCol.r - _DissolveThreshold);
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
相应的Shader属性面板如下:
这样就可以获得和第一张图一样的效果了, 从指定的位置开始溶解.
细心的同学可能看到, 最终效果会从怪物的嘴巴部分开始溶解, 但是噪声纹理最黑的部分明明是左下角:
这是因为本文使用的怪物纹理是基于PBR制作的, 和普通的纹理布局不太一样, 大家在实验的时候, 找一个正常的纹理就行了.
或者制作噪声纹理时也使用PBR的方式制作也可以达到相同的目的, 这里我是为了说明这种情况, 故意如此.
下面是完整的代码:
Shader "Dissolve"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_DissolveTex("DissolveTex", 2D) = "white" {}
_DissolveThreshold("DissolveThreshold", Range(0, 1)) = 0
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _DissolveTex;
fixed _DissolveThreshold;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 dissolveCol = tex2D(_DissolveTex, i.uv);
// 从噪声纹理采样颜色, 如果该值[小于阈值]则丢弃本片段
// 比如阈值为0.1, 则在噪声纹理上采样的所有像素r值小于0.1的片段都会被丢弃
// 即噪声纹理上偏黑的颜色(->0)首先开始溶解, 偏白的颜色(->1)最后溶解
clip(dissolveCol.r - _DissolveThreshold);
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
今天我们介绍了游戏中比较常见的溶解特效, 比如怪物在被击杀后, 可以溶解消失, 或者角色穿过传送门, 溶解消失等.
对于初次接触的同学来说, 溶解特效比较酷炫, 但是实现却超级简单, 不需要复杂的数学知识和图形学知识, 我认为这是一个非常好的学习Shader示例, 所以在此特意共享给大家.
后续我可能也会继续给大家分享一些简单却有意思的Shader, 让大家领略Shader的魅力, 坚定学习下去的信心.
我认为图形学的基础固然重要, 但是一上来就各种矩阵, 各种管线, 容易让人产生劝退效果, 所以我更喜欢从一些简单的特效来学习, 在学习和实现了效果之后, 再去探究其背后的原理, 这样比较容易产生成果, 也更能加深理解.
我觉得对于前端的同学, 做出各种绚丽的特效也是大家最初想做游戏的目的之一 , 不然做服务器不是更舒服? 虽然做游戏之后才知道, 这部分是美术的活儿, 哈哈.
好了, 今天就是这些啦, 希望对大家有所帮助.