前言
之前做3D描边的时候用的是法线挤出顶点位置,渲染两个pass的做法,没想过2D描边及发光的做法,这次正好遇到这样的需求,研究了一下,写篇心得分享给大家
内描边
内描边的做法其实总结就是找到sprite最边缘的像素然后填充成一个纯色,然后这个像素选区范围可以往内收缩,好的,那么开始吧,找了张sprite

先上代码
Pass
{
//计算描边做法,检测像素上下左右各一个像素的alpha为0即判断该像素处于边缘
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;
float2 up_uv : TEXCOORD1;
float2 down_uv : TEXCOORD2;
float2 left_uv : TEXCOORD3;
float2 right_uv : TEXCOORD4;
};
sampler2D _MainTex;
float4 _MainTex_TexelSize;
float _Width;
float4 _BloomColor;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.up_uv = o.uv + float2(0, 1)*_Width*_MainTex_TexelSize.xy;
o.down_uv = o.uv + float2(0, -1)*_Width*_MainTex_TexelSize.xy;
o.left_uv = o.uv + float2(-1,0)*_Width*_MainTex_TexelSize.xy;
o.right_uv = o.uv + float2(1, 0)*_Width*_MainTex_TexelSize.xy;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
float w = tex2D(_MainTex, i.up_uv).a * tex2D(_MainTex, i.down_uv).a *tex2D(_MainTex, i.left_uv).a *tex2D(_MainTex, i.right_uv).a;
col.rgb = lerp(_BloomColor.rgb, col.rgb, w);
return col;
}
ENDCG
}完成效果:

这个做法其实很简单,在frag阶段遍历每个图片像素,然后判断该像素的上下左右的像素alpha值是否是0,如果是0,我们就把该像素0涂成描边的颜色

_MainTex_TexelSize这个变量可以取到该纹理的纹理像素位置,指的就是一个像素占uv的比,比如512*512的图片,纹素就是(1/512,1/512)

这里我还加了一个变量_Width,这样可以控制扩大这个uv的选取范围,即更大的选区范围如果检测到0的alpha依旧把上图0号像素涂成边缘的颜色,这样可以通过控制_Width扩大内描边的大小
外发光(均值模糊做法)
其实简单的做法直接在pp里加一个Bloom的滤镜加上上面的内描边做法即可实现边缘发光的效果,但是介于项目性能需要,我们项目中不采用后期处理的做法,那么就要考虑直接在shader里实现bloom的做法
发光其实就是一个模糊的做法,在了解模糊做法之前得知道卷积的算子概念
卷积操作就是利用一个卷积核对图像进行一个遍历操作,卷积核通常是个正方形的网格结构(2X2,3X3,5X5)等。卷积核中每个元素都有自己的权重,比如常见的边缘检测算子sobel算子
GX
Gy
利用每个元素中的权重值乘以像素的亮度最后求和再利用公式求阈值得出边缘
我们这里利用均值模糊的卷积算子去做外发光,均值模糊的卷积核每个元素权重都一样,所以其实可以不用计算
Pass
{
//计算均值模糊的做法,3X3的像素卷积内透明像素越多,则(0,0)像素alpha值越低
//采用了均值权重3x3的卷积算法 实际纹理采样为3*3*图像高度*图像长度
Blend One One
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv[9] : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_TexelSize;
float _Width;
float4 _BloomColor;
float _Strength;
half CalculateAlphaSumAround(v2f i)
{
half alpha;
half aSum = 0;
_Strength /= 50;
for (int it = 0; it < 9; it++)
{
alpha = tex2D(_MainTex, i.uv[it]).a;
aSum += alpha * _Strength;
}
aSum = min(1, aSum);
return aSum;
}
v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv[0] = v.uv + _MainTex_TexelSize.xy * half2(-1,-1)*_Width;
o.uv[1] = v.uv + _MainTex_TexelSize.xy * half2(-1, 0)*_Width;
o.uv[2] = v.uv + _MainTex_TexelSize.xy * half2(-1, 1)*_Width;
o.uv[3] = v.uv + _MainTex_TexelSize.xy * half2(0, -1)*_Width;
o.uv[4] = v.uv + _MainTex_TexelSize.xy * half2(0, -1)*_Width;
o.uv[5] = v.uv + _MainTex_TexelSize.xy * half2(0, 0)*_Width;
o.uv[6] = v.uv + _MainTex_TexelSize.xy * half2(1, -1)*_Width;
o.uv[7] = v.uv + _MainTex_TexelSize.xy * half2(1, 0)*_Width;
o.uv[8] = v.uv + _MainTex_TexelSize.xy * half2(1, 1)*_Width;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
half a = CalculateAlphaSumAround(i);
return float4(_BloomColor.rgb*a,1);
}
ENDCG
}计算了3X3的一个卷积中的像素alpha值,最后把这9个像素的alpha值累加最后乘以bloomColor值,因为越靠近边缘它的alpha总值会越低,所以颜色会有渐变的过度,最后我们对这张sprite做downsample,因为有骨骼动画的需要,项目中采用downsampleRT去替代这张图

因为这只兔子周围的alpha为0的像素也会遍历,增加_Width值可以使这个偏移的遍历范围更广,这样原本alpha为0的像素需要遍历的时候也会涂上颜色,就实现了外描边,Width值越大外描边宽度越大

最后让这张RT先渲染即可
完成效果:

外描边(高斯模糊)
这里我们使用一个5X5的标准方差为1的高斯卷积核

我们如果用上面的做法去做这个,那么我们需要对纹理采样5*5*图片宽度*图片高度这么多次,这里使用把这一个二维的卷积核拆成两个一维的卷积核,用两个pass分别计算,第一个pass只计算水平方向的像素,第二个计算垂直方向的,这样加起来的纹理采样是(5+5)*图片宽度*图片高度次,效率提高不少,因为这个一维的卷积核有些权重值是重复的,所以只需要记录三个值(0.0545,0.2442,0.4026)
Pass
{
//采用5X5卷积算子,分两个pass,将二维卷积核转化为两个一维卷积核计算
//采用了卷积权重计算,先计算水平的pass
Blend One One
NAME "BLOOM_VERTICAL"
CGPROGRAM
#pragma vertex vertBloomVertical
#pragma fragment fragBloom
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv[5] : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_TexelSize;
float _Width;
float4 _BloomColor;
float _Strength;
float _Power;
half CalculateAlphaSumAround(v2f i)
{
float weight[3] = { 0.4026,0.2442,0.0545 };
half aSum = tex2D(_MainTex,i.uv[0]).a * weight[0] * _Strength;
aSum += tex2D(_MainTex, i.uv[1]).a * weight[1] * _Strength;
aSum += tex2D(_MainTex, i.uv[2]).a * weight[1] * _Strength;
aSum += tex2D(_MainTex, i.uv[3]).a * weight[2] * _Strength;
aSum += tex2D(_MainTex, i.uv[4]).a * weight[2] * _Strength;
aSum = min(1, aSum);
return aSum;
}
v2f vertBloomVertical(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv[0] = v.uv;
o.uv[1] = v.uv + float2(_MainTex_TexelSize.x *1,0)*_Width;
o.uv[2] = v.uv - float2( _MainTex_TexelSize.x *1,0)*_Width;
o.uv[3] = v.uv + float2(_MainTex_TexelSize.x *2,0)*_Width;
o.uv[4] = v.uv - float2(_MainTex_TexelSize.x *2,0)*_Width;
return o;
}
fixed4 fragBloom(v2f i) : SV_Target
{
half a = CalculateAlphaSumAround(i);
return float4(pow(_BloomColor.rgb*a, _Power),1);
}
ENDCG
}
Pass
{
//采用5X5卷积算子,分两个pass,将二维卷积核转化为两个一维卷积核计算
//采用了卷积权重计算,后计算垂直的pass
Blend One One
NAME "BLOOM_HORIZONTAL"
CGPROGRAM
#pragma vertex vertBloomHorizontal
#pragma fragment fragBloom
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv[5] : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_TexelSize;
float _Width;
float4 _BloomColor;
float _Strength;
float _Power;
half CalculateAlphaSumAround(v2f i)
{
float weight[3] = { 0.4026,0.2442,0.0545 };
half aSum = tex2D(_MainTex,i.uv[0]).a * weight[0] * _Strength;
aSum += tex2D(_MainTex, i.uv[1]).a * weight[1] * _Strength;
aSum += tex2D(_MainTex, i.uv[2]).a * weight[1] * _Strength;
aSum += tex2D(_MainTex, i.uv[3]).a * weight[2] * _Strength;
aSum += tex2D(_MainTex, i.uv[4]).a * weight[2] * _Strength;
aSum = min(1, aSum);
return aSum;
}
v2f vertBloomHorizontal(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv[0] = v.uv;
o.uv[1] = v.uv + float2(0,_MainTex_TexelSize.y *1)*_Width;
o.uv[2] = v.uv - float2(0, _MainTex_TexelSize.y * 1)*_Width;
o.uv[3] = v.uv + float2(0, _MainTex_TexelSize.y * 2)*_Width;
o.uv[4] = v.uv - float2(0, _MainTex_TexelSize.y * 2)*_Width;
return o;
}
fixed4 fragBloom(v2f i) : SV_Target
{
half a = CalculateAlphaSumAround(i);
return float4(pow(_BloomColor.rgb*a, _Power),1);
}
ENDCG
}完成效果:

其实downsampleSprite对做模糊有好处,使图片更加模糊了,当然不能过于模糊,不然像素一块一块的了
总结
关于边缘发光,描边检测的做法应该还有蛮多,第一次写知乎,大家多多交流哈,之后有机会还会继续写的
原文地址:https://zhuanlan.zhihu.com/p/266242695 |