0x00 需求
在UI上实现一种类似战争迷雾的效果。
0x01分析需求
战争迷雾是来自于RTS游戏,[此处可以插入链接]并经由MOBA游戏发扬广大的一种在地图上增加一种不透明的迷雾,造成信息的不对称从而增加游戏的趣味性。那么第一个想法是寻找战争迷雾插件,导入,使用之。
0x02实现方案对比
从实现原理上分类,战争迷雾有两种实现方法:
- 屏幕后处理,
- 遮罩擦除法
屏幕后处理中,最成熟的当属Fog Of War,在Unity Asset Store中搜索,第一个跳出的就是这个插件,作者:AsehesL,blog地址在此。
这个插件确实功能很丰富,也下载下来把玩了一番。
功能都可以满足,甚至还根据点亮范围实时生成了一个小地图。
太强了,然鹅我只需要一个UI上的迷雾,并不需要depth texture,计算视野,生成mesh,生成mask贴图等等功能。虽然也有2D模式,看了代码感觉还是有很多不必要的功能引入。
遮罩擦除法,这篇文章介绍了两种实现方法,对于2D游戏来说,可能确实是一种方案,对于我目前仅仅用在UI上的需求来说,还是觉得有些冗余。
本着节约性能偷懒的原则,对比了几个插件(实现方案)后,发现战争迷雾插件主要还是面向3D场景中的需求,对于UI上这种伪需求都有一种杀鹌鹑用牛刀之感。
0x03自己实现
先看效果:
首先考虑的就是使用屏幕后处理的做法来实现一个简易版本。做完demo后,移植到项目后,发现没效果。wtf!!!查了半天,发现URP管线已然不支持OnRenderImage接口了。需要实现自己的ScriptableRendererFeature类和ScriptableRenderPass类。由于还在爬SRP Batcher的坑,没有料到URP直接釜底抽薪,把OnRenderImage给抽了,是我大意了。
在实现自己的一套后处理机制时,跟虹老板讨论了一下方案,虹老板第二天灵机一动:为什么不用stencil test?我一想,也对。其实UI上的迷雾原理上与mask差不多。于是重新开始实现基于stencil test的demo。
实现原理只要明白stencil test的原理,自己实现并有什么难点。
简述一下步骤:主要是根据这个shader创建的两个材质,UIFog.mat用于迷雾,UIFogMask.mat用于遮罩(点亮范围)。
- 先用默认的image(UI-Default)画一个背景
- 使用自定义的shader,将需要标记的区域(透过迷雾的部分)设置stencil buffer。
Stencil Comparion 对应Stencil 中的 Comp(即stencil test中的比较运算)
Stencil ID 对应 Ref(即我们俗称的stencil buffer)
Stencil Operation 对应 Pass(即通过stencil test时执行的操作)
Stencil Comparion = 5(Greater)表示如果stencil buffer(1)大于当前的stencil buffer(UI上的默认值为0)时,执行Stencil Operation = 2(Replace),将Texture范围内的Stencil Buffer设置为1。
- 使用自定义的shader,判断渲染的区域的stencil buffer 是否等于0,等于0即通过stencil test.反之,如果区域内的stencil buffer不等于0就没有通过stencil test,默认情况下没有通过stencil test的区域将被放弃。
0x04源代码
shader代码直接拿了UI-Default.shader的代码,把_MainTex开放出来。
// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)
Shader "coffeecat/UIFog"
{
Properties
{
// [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
_MainTex ("Sprite Texture", 2D) = "white" {}
_Color ("Tint", Color) = (1,1,1,1)
_StencilComp ("Stencil Comparison", Float) = 8
_Stencil ("Stencil ID", Float) = 0
_StencilOp ("Stencil Operation", Float) = 0
_StencilWriteMask ("Stencil Write Mask", Float) = 255
_StencilReadMask ("Stencil Read Mask", Float) = 255
_ColorMask ("Color Mask", Float) = 15
[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
}
SubShader
{
Tags
{
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
"PreviewType"="Plane"
"CanUseSpriteAtlas"="True"
}
Stencil
{
Ref [_Stencil]
Comp [_StencilComp]
Pass [_StencilOp]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
}
Cull Off
Lighting Off
ZWrite Off
ZTest [unity_GUIZTestMode]
Blend SrcAlpha OneMinusSrcAlpha
ColorMask [_ColorMask]
Pass
{
Name "Default"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0
#include "UnityCG.cginc"
#include "UnityUI.cginc"
#pragma multi_compile_local _ UNITY_UI_CLIP_RECT
#pragma multi_compile_local _ UNITY_UI_ALPHACLIP
struct appdata_t
{
float4 vertex : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
float2 texcoord : TEXCOORD0;
float4 worldPosition : TEXCOORD1;
UNITY_VERTEX_OUTPUT_STEREO
};
sampler2D _MainTex;
fixed4 _Color;
fixed4 _TextureSampleAdd;
float4 _ClipRect;
float4 _MainTex_ST;
v2f vert(appdata_t v)
{
v2f OUT;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
OUT.worldPosition = v.vertex;
OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);
OUT.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
OUT.color = v.color * _Color;
return OUT;
}
fixed4 frag(v2f IN) : SV_Target
{
half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
#ifdef UNITY_UI_CLIP_RECT
color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
#endif
#ifdef UNITY_UI_ALPHACLIP
clip (color.a - 0.001);
#endif
return color;
}
ENDCG
}
}
}