卷积描边算法

前段时间美术想要一些角色描边的效果,一开始做了外描边,但是美术想要身体内也有描边效果

类似这种描边,如果用外描边是没办法实现的。

这时候就需要能描述内描边的算法。

从实现来看,包括内描边的算法可以做的方式包括:

1.身体分开描边:

也就是衣服一次外描边,身体一次外描边,头发一次外描边。。。。

好处:容易操作,而且效果可以控制。

坏处:drawcall会多很多,相当于每个配件都多一个dc,相当于多了一倍的dc。如果角色多,那就很恐怖了。。。

2.算法描边:

(1)卷积描边:

这种算法是用了边缘检测的方式来实现的,边缘检测的目的是识别数字图像中亮度变化明显的点。

一般的卷积有Sobel, Laplacian, Canny filter等。

Sobel Kernel通常采用两个3x3的矩阵来表示。由于Sobel滤波是检测一个方向上(一阶)的色彩或者亮度的梯度变化,所以需要在水平和垂直方向进行两次卷积。其两个卷积核(Sobel算子 - Sobel Operator)分别如下:

我们分别获得水平和垂直的结果后将其相加

,这样通过判断G是否大于一个阈值,我们就能判断这个像素是否属于边缘。

另一方面,Laplacian算子是一个二阶的微分算子,所以只需一个卷积核就能够检测到边缘,其缺点是对于噪声(Noise)比较敏感,如果图像中有比较多的噪声可能处理出来的结果会有很多噪点。

传统的实现方式就不说了,网上有很多相关的算法。

因为他们都是3x3的矩阵,所以我们运算时需要9次采样。如果我们运行在手机上会法线卡顿明显(gpu运算量加大)

那么如果我们想要一个折中的算法,而且又能体现出内描边的效果,我就放弃了3x3的矩阵,用2x2的矩阵来实现。

卷积计算这里有个特点:9个数值相加后为0,这样才能保证我们的权重总值时1。

那么基于这个特点,我们用2x2的矩阵时就可以参考这个特点了。

算法原理是:

float2x2 sobel= float2x2(
                1, 1,
                1, 1
                );

这里申请了2x2的矩阵,全是1,这个矩阵主要用于采样邻近的四个斜对角的点的像素。然后我们需要再采样一个正常的当前像素乘(-4)的值来平衡整个权重

int forLen = 2;
			for (int m = 0; m < forLen; m++)
				for (int n = 0; n < forLen; n++)
				{
					sobelH += tex2D(_MainTex, i.uv + offsets[m * forLen + n] * adjacentPixel) *laplacianOperator[m][n];
				}

			sobelH += tex2D(_MainTex, i.uv) * -1 * forLen * forLen;

这就是整个shader中最耗时的地方了做了五次采样。

其他都是正常的算法了。

另外想说的是,sobel算子他需要算开根号

我们其实并不需要,只需要算平方后我们把值整体缩小就好了。(或者输入时让美术输入更大的值起步)

整个算法如下:


struct v2f_outline
{
			float2 uv: TEXCOORD0;
			float4 pos : SV_POSITION;
			float3 normal : TEXCOORD1;
};


v2f_outline vert_sobal(appdata v)
		{
			v2f_outline o;
			o.pos = UnityObjectToClipPos(v.vertex);
			o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
			return o;
		}
float4 fragColor_sobel(v2f_outline i) : COLOR
		{
			half offset = 1;

			float2 offsets[4] = {
				float2(-offset, offset),
				float2(offset, offset),
				float2(-offset, -offset),
				float2(offset, -offset)
			};

			float2x2 sobel = float2x2(
				1, 1,
				1, 1
				);
			float4 sobelH = float4(0, 0, 0, 0);
			float2 adjacentPixel = _MainTex_TexelSize.xy * _AdjacentPixel;
			int forLen = 2;
			for (int m = 0; m < forLen; m++)
				for (int n = 0; n < forLen; n++)
				{
					sobelH += tex2D(_MainTex, i.uv + offsets[m * forLen + n] * adjacentPixel) *sobel[m][n];
				}

			sobelH += tex2D(_MainTex, i.uv) * -1 * forLen * forLen;

			float sobel = sobelH * sobelH;

			float4 sceneColor = tex2D(_MainTex, i.uv);

			float edgeMask = saturate(sobel);
			float3 EdgeMaskColor = float3(edgeMask, edgeMask, edgeMask);

			float3 finalColor = saturate((EdgeMaskColor * _OutlineColor.rgb) + (sceneColor.rgb - EdgeMaskColor));
			return float4(_OutlineColor.rgb, sceneColor.a - 1 + EdgeMaskColor.r);

		}

Pass{
			Name "SIMPLE_TRANSPARENT"
			Tags{"LightMode" = "LightweightForward"}
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

			ENDCG
		}

最后看看效果

 

另外,关于锯齿可以通过msaa或做模糊来处理

 

如果想要描边选择区域可以控制,可以用_CameraDepthTexture获取深度信息,然后根据不同的深度显示不同的数据

 

 

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值