之前玩Tencent的仙剑4手游时,杀死boss会看到boss有“消融”的效果,就是身体上有多个洞洞然后往四周扩散直至尸体完全消失,但效果是没有关闭背面剔除的“穿帮”效果,可能也是考虑性能因素。 emmmm,那下面开始详细讲解消融等等噪声相关应用的实现~
· 消融效果
噪音,就是“沙沙渣渣渣”的杂乱无章的声音,在图形学中使用噪声是为了把一些随机变量来引入到程序中,如火焰、地形、云朵的模拟等等都要使用随机变量。有人可能会问,直接使用random这种函数不就好了吗?为什么要引入这么多算法生成的特定噪声呢?原因在于生成的随机值太“随机”了,在图形学中称这种噪声为“白噪声”(功率谱密度在整个频域内均匀分布的噪声),通俗理解就是类似小时候电视机上面的雪花噪声,
(二维的白噪声纹理)
噪声的基础来自于随机数,若屏幕上的每个像素点给一个0~1之间的随机数来表示象素点的亮度,就能得到上面的白噪声纹理,很像老式电视机故障时的黑白雪花。这种随机图像并没有太大的作用,因为每个点的随机数都是离散的,相互完全没有关系,能看到这幅图像中的世界是完全没有逻辑,没有能量的。 既然基本随机噪音因离散性而没意义,那就人为让噪音连续起来,这种平滑的连续化处理就是插值,在离散数据中间用函数插值的方法把空隙填满空间就自然连续了。
简单的线性插值得到的棱角分明的结构,指数/三角函数等插值组合能形成各种插值算法,从而对应多种不同噪声。如Perlin噪声被大量用于云朵、火焰和地形等自然环境的模拟;Simplex噪声在其基础上进行改进,提高效率和效果;而Worley噪声被提出用于模拟一些多孔结构,例如纸张、木纹等。有关噪声算法的详解,可参考 【图形学】谈谈噪声 。
不管怎样,对简单随机噪音插值后都能得到一种像霉菌表面的图像,本节的噪声纹理如下图:
开始编码,表现效果就是从不同的区域开始,并向随机方向扩张,最后整个物体都将消失不见。
原理:使用噪声纹理进行取样,将取样的结果和某个控制消融程度的阈值比较,若小于阈值就用clip函数将对应像素裁剪掉(透明度测试处理),不裁剪掉的像素就和另外两个颜色来进行混合插值处理从而达到烧焦的效果,并且将光照阴影和衰减也按照同样的处理,避免错误的投射阴影。
完整代码如下:
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld' // Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)' Shader "Shaders/Dissolve" { Properties { _BurnAmount ("Burn Amount",Range(0.0, 1.0)) = 0.0 //消融程度,0为正常显示,1为物体会完全消融 _LineWidth("Burn Line Width", Range(0.0, 0.2)) = 0.1 //模拟烧焦效果的线宽,值越大表示火焰边缘蔓延范围越广 _MainTex ("Base (RGB)", 2D) = "white" {} //物体本身的漫反射纹理 _BumpMap ("Normal Map", 2D) = "bump" {} //法线纹理 _BurnMap("Burn Map", 2D) = "white"{} //噪声纹理 //火焰边缘的两种颜色 _BurnFirstColor(, , , ) _BurnSecondColor(, , , ) } SubShader { Tags { "RenderType"="Opaque" "Queue"="Geometry"} Pass { Tags { "LightMode"="ForwardBase" } //关闭面片剔除, 则模型正面/背面都被渲染,因为消融会裸露模型内部的构造 Cull Off CGPROGRAM #include "Lighting.cginc" #include "AutoLight.cginc" #pragma multi_compile_fwdbase #pragma vertex vert #pragma fragment frag fixed _BurnAmount; fixed _LineWidth; sampler2D _MainTex; sampler2D _BumpMap; sampler2D _BurnMap; fixed4 _BurnFirstColor; fixed4 _BurnSecondColor; float4 _MainTex_ST; float4 _BumpMap_ST; float4 _BurnMap_ST; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; float4 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uvMainTex : TEXCOORD0; float2 uvBumpMap : TEXCOORD1; float2 uvBurnMap : TEXCOORD2; float3 lightDir : TEXCOORD3; float3 worldPos : TEXCOORD4; SHADOW_COORDS() }; v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); //使用 TRANSFORM_TEX内置宏计算三张纹理的纹理坐标 o.uvMainTex = TRANSFORM_TEX(v.texcoord, _MainTex); o.uvBumpMap = TRANSFORM_TEX(v.texcoord, _BumpMap); o.uvBurnMap = TRANSFORM_TEX(v.texcoord, _BurnMap); //将光源从模型空间变换到切线空间 TANGENT_SPACE_ROTATION; o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz; //阴影 o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; TRANSFER_SHADOW(o); return o; } fixed4 frag(v2f i) : SV_Target { //对噪声纹理采样 fixed3 burn = tex2D(_BurnMap, i.uvBurnMap).rgb; //返回值范围是0~1,小于消融阈值的像素被剔除 clip(burn.r - _BurnAmount); float3 tangentLightDir = normalize(i.lightDir); fixed3 tangentNormal = UnpackNormal(tex2D(_BumpMap, i.uvBumpMap)); fixed3 albedo = tex2D(_MainTex, i.uvMainTex).rgb; fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; fixed3 diffuse = _LightColor0.rgb * albedo * max(, dot(tangentNormal, tangentLightDir)); /* smoothstep( float _Min, float _Max, float _X ) 实现平滑过渡,让烧焦区域颜色混合渐变 若_X 比 _Min,小于返回 0; 若_X 比 _Max 大则返回 1; 在范围 [_Min, _Max]内返回介于 0 和 1 之间的值 smoothstep 函数用于在一段时间范围内逐渐但非线性地增加属性,如“不透明度”(Opacity)从 0 增加到 1。*/ //混合系数t,值为0不需要混合,像素为正常模型颜色;值为1表示像素位于消融边界处 - smoothstep(0.0, _LineWidth, burn.r - _BurnAmount); /*插值函数lerp实现颜色渐变过渡 lerp(a, b, w):a与b为floatX或fixedX等同种类型,返回值是对应同种类型 当w为0时返回a,为1时返回b,0~1之间,以比重w将a b进行线性插值计算。*/ fixed3 burnColor = lerp(_BurnFirstColor, _BurnSecondColor, t); burnColor = pow(burnColor, ); UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); //Mark! step(a, x):Returns (x >= a) ? 1 : 0 fixed3 finalColor = lerp(ambient + diffuse * atten, burnColor, t * step(0.0001, _BurnAmount)); ); } ENDCG } //透明度处理的物体的阴影做同样处理,以免被剔除的区域仍会向其他物体投射阴影从而“穿帮” Pass { Tags { "LightMode" = "ShadowCaster" } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_shadowcaster #include "UnityCG.cginc" fixed _BurnAmount; sampler2D _BurnMap; float4 _BurnMap_ST; struct v2f { V2F_SHADOW_CASTER; float2 uvBurnMap : TEXCOORD1; }; v2f vert(appdata_base v) { v2f o; TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) o.uvBurnMap = TRANSFORM_TEX(v.texcoord, _BurnMap); return o; } fixed4 frag(v2f i) : SV_Target { fixed3 burn = tex2D(_BurnMap, i.uvBurnMap).rgb; clip(burn.r - _BurnAmount); SHADOW_CASTER_FRAGMENT(i) } ENDCG } } FallBack "Diffuse" }
注意,最后通过step判断函数再对混合系数t做条件处理,因一开始_BurnAmount为0时,t可能为1或不为0,lerp函数运算导致显示了_BurnColor,实际上刚开始_BurnAmount为0时不显示任何消融效果。
运行游戏,显然有了!
(_BurnAmount = 0.3)
(_BurnAmount = 0.5)
明显看到关闭了剔除之后,背后的消融也不会穿帮。如果把剔除打开,个人觉得效果差不多且减小性能消耗,
(_BurnAmount = 0.5)
资料链接:
Unity Shader-死亡溶解效果 [UnityShader]溶解与重现效果(差不多)
一种基于边缘Bloom的溶解shader的实现 (效果挺酷)