先上一张效果图:
这篇文章我准备写两部分:
- 使用噪声图在unity shader里实现物体消融的效果
- 消融是随机的,但每次过程又是固定的,这一种“有序的无序”模式的理解,这一点是我更想说的,消融效果是帮助解释这个概念。
有规律的随机
玩游戏的时候,有时会有火焰特效,有次我盯住一个火焰看,发现火焰是循环播放的,比如下面这张图。类似火焰这种东西,是变化无常的,一般就是随机来做,比如用unity自带的粒子系统来实现,那么火焰的形态是完全随机无序的,是我们无法控制的。但又是怎么让规定的样式进行晃动呢?这里就有一个冲突,就是火焰的形态的无序随机的,否则就会不像;然后火焰这种无序又是按照一种既定的规律重复的进行。
简单说,随机是13、313、54、114、521、14、41、4521、421这样任意的,足够时间,都是随机的;而有规律的随机是13、313、54、114、521、14、41、4521、421这样随机了一轮后,下一轮跟这个是相同的,这样重复。可以理解为套了两层,第一层重复,是有规律的,第二层随机。
这就是我的困惑,怎么构造这种“有序的无序” ?
消融效果
先上代码,如果你不了解unity shader,也可以继续看看,我会把里面的逻辑用常人理解的方式理一遍。
Shader "Unlit/Dissolve"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_BurnMap ("Fire Map", 2D) = "white" {}
_BurnSpeed ("Burn Speed", float) = 1.0
_Specular ("Specular", range(0, 1)) = 0.5
_Gloss ("Gloss", range(8, 256)) = 20
//_BurnAmount ("BurnAmount", range(0,1)) = 0
_BurnFirstColor ("Burn First Color", Color) = (1,0,0,1)
_BurnSecondColor ("Burn Second Color", Color) = (0,0,0,1)
_BurnRange ("Burn Range", float) = 0.1
}
SubShader
{
Tags { "LightMode"="ForwardBase" }
Pass
{
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BurnMap;
fixed _BurnSpeed;
fixed _Gloss;
float _Specular;
fixed _BurnAmount;
fixed4 _BurnFirstColor;
fixed4 _BurnSecondColor;
float _BurnRange;
struct a2v{
float4 vertex : POSITION;
float4 normal : NORMAL;
float4 uv : TEXCOORD0;
};
struct v2f{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float4 worldPos : TEXCOORD1;
float3 worldNormal : TEXCOORD2;
};
v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.worldNormal = normalize(UnityObjectToWorldNormal(v.normal));
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//burn map
fixed3 burn = tex2D(_BurnMap, i.uv).rgb;
_BurnAmount += _Time.y * _BurnSpeed;
clip(burn.r - _BurnAmount);
// sample the texture
fixed3 albedo = tex2D(_MainTex, i.uv).rgb;
//diffuse
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 diffuse = _LightColor0.rgb * saturate(dot(lightDir, i.worldNormal)) * albedo;
//specular
fixed3 reflectDir = normalize(reflect(-lightDir, i.worldNormal));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 specular = _LightColor0.rgb * _Specular * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo;
fixed3 finalColor = specular + diffuse + ambient;
//在消融的边缘位置,添加红色和黑色,模拟烧焦的效果。当前正在烧的边缘就是那些r - _BurnAmount刚好为0的位置。
//float burnRate = 1 - saturate((burn.r - _BurnAmount) / _BurnRange);
float burnRate = 1 - smoothstep(0, _BurnRange, burn.r - _BurnAmount);
fixed3 burnColor = lerp(_BurnFirstColor, _BurnSecondColor, burnRate);
//burnColor = pow(burnColor, 5);
finalColor = lerp(finalColor, burnColor, burnRate);
return fixed4(finalColor, 1);
}
ENDCG
}
}
}
shader里有个函数clip可以丢弃像素,就是不渲染模型的这个点了。什么是物体的消融?不就是你本来看到的是物体A,然后变成了看到物体A后面的背景了。使用丢弃像素的操作,本来渲染物体A的,变成显示后面的东西了,消融效果就出来了。
消融还有个特性,就是它像水滴在纸上面那样逐渐扩撒开来,而不是无规律的随意消融。那么问题就转移为:按照一个逐渐扩散的模式丢弃模型的像素。
如果要像现实里一样,那么消融扩散的方向、大小、快慢都该是无规律任意的,这是这个问题里的无序。但是如果要用程序随机计算哪些地方消融,那么就是不断出现一个个小洞那样的效果,要模拟扩散消融,个人觉得那就太难了。而现在的做法就是使用纹理图。
比如我用相机给现实的火拍一张照,把这个作为模板。物体的表面有个uv坐标系,简单说就是3维物体表面跟2维的平面建立一个对应关系,那么物体表面任意一点,我都可以找到之前拍的火焰纹理图上一个对应点,然后根据这个点的颜色里的红色多少决定是否丢弃。
主要代码就是这一段,其他只是实现光照和边缘烧焦效果。
fixed3 burn = tex2D(_BurnMap, i.uv).rgb;
_BurnAmount += _Time.y * _BurnSpeed;
clip(burn.r - _BurnAmount);
_BurnMap
是火焰纹理图,tex2D(_BurnMap, i.uv)
这个函数就是根据点的uv左边取到对应纹理图上那一点的颜色信息,clip(burn.r - _BurnAmount);
如果这个一点的r(红色)通道值小于_BurnAmount
,就剔除这个像素。因为_BurnAmount += _Time.y * _BurnSpeed;``_BurnAmount
不断增大,那么就会有越来越多的像素被丢弃,最后整个物体都被丢弃,彻底消融。
因为纹理图本身就是扩散模式的,所以物体被剔除也是这个模式。
如果把物体表面的纹理也换成火焰纹理,就可以很明显的看到,从最红的位置开始消融,消融的位置跟纹理的颜色分布是完全一样的。
纹理的力量
回到上面
怎么构造这种“有序的无序” ?
使用纹理。纹理上面的内容本身是混乱无序的,但是纹理图已经是一张固定的图,我们参照纹理来决定哪里消融,那纹理图就是有规律的了,是有序的。
在unity里,shader里是使用纹理,那么在其他地方呢?更抽象的说,就是使用一个模板,把无序记录下来,然后按照这个已经记录下来的模板来操作,这样就可以实现“有序的无序”。
最后一个问题,这样做有什么好处?简便直接效率高。
资源
消融效果的材质包放在github这个库里,DissoveShader.unitypackage
就是。