关于屏幕后效果的控制类详细见之前写的另一篇博客:
https://cloud.tencent.com/developer/article/1601301
这篇主要是基于之前的控制类,实现另一种常见的屏幕后效果——边缘检测。
概念和原理部分:
首先,我们需要知道在图形学中经常处理像素的一种操作——卷积。
卷积操作的实质在于,对于图像中的每个像素与其周围的像素进行的重新融合计算行为,以得到不同的像素处理效果,例如锐化图像,模糊图像,检测边缘等。
卷积操作通过不同的像素融合算法能得到各不相同的效果,这主要依赖于卷积核。
可以把卷积核看作是一个n行n列方阵,原始像素则位于方阵的中心。
边缘检测的卷积核也叫边缘检测算子,以Sobel算子为例,形如:
需要特别注意的是,这里的Sobel算子是基于坐标轴以屏幕左上为原点,右下分别为 x, y方向的,而不是类似于uv坐标轴的以屏幕左下为原点,右上分别为 x, y方向的。这一点需要特别注意,不然后面的程序很容易写错。
其中Gx和Gy分别是纵向和横向两个方向的边缘线检测,你可以通过去掉矩阵中的零元素来想象,因为零元素不会对像素产生任何影响。也就是说,Gx是为了计算横向的梯度值,Gy为了计算纵向的梯度值。
横向的梯度值检测出来的是纵向的边缘线,纵向的梯度值检测出来的是横向的边缘线。这一点非常容易混淆,需要特别注意。
利用边缘检测算子除了融合像素外,主要是为了计算出像素的梯度值。
一个像素和周围的像素之间梯度值很高,意味着它与周围的像素差异很大,我们可以想象这个像素和周围的像素格格不入,存在一个无法逾越的阶梯;那么就可以这么认为,这个像素可以作为一条边界中的值。
对图像中的每个像素都如此处理,最终就能得到图像的边缘。这也就是边缘检测的实质内容。
计算方法:
1.得到每个像素周围的8个像素的坐标位置以便与Sobel算子进行计算,类似于:(排列方式应该与Sobel算子的坐标轴保持一致)
uv0 | uv1 | uv2 |
---|---|---|
uv3 | uv4(原始像素点) | uv5 |
uv6 | uv7 | uv8 |
但因为uv坐标的原点在左下角,因此在计算uv0-uv8时,若依据uv4为原始像素点,则它们的偏移可以表示为如下情况:
(-1,1)uv0 | (0,1)uv1 | (1,1)uv2 |
---|---|---|
(-1,0)uv3 | (0,0)uv4 | (1,0)uv5 |
(-1,-1)uv6 | (0,-1)uv7 | (1,-1)uv8 |
2.通过偏移值可以很快计算出目标像素的周围像素位置坐标信息,随后与Gx和Gy对应元素分别进行横向和纵向的梯度值计算,也就是分别进行纵向和横向的边缘检测:
具体计算方法为:先对卷积核进行180度翻转,得到新的矩阵,随后各项对应元素相乘并相加,注意,不要与矩阵的乘法计算混淆。
但因为Sobel算子是否执行翻转操作对计算结果没有任何影响,故对于Sobel算子来说,翻转操作可以省略。
Gx和Gy计算结束后再将它们开平方和;但往往为了简化GPU的计算量,可以直接取各自的绝对值再相加,得到最终的梯度值G。
3.计算出梯度值后对原始的采样结果进行关于G的插值操作以得到最终的图像。
程序实现:
首先是参数调控的脚本:
代码语言:javascript复制
代码语言:javascript复制 1 using UnityEngine;
2
3 public class EdgeDetectionCtrl : ScreenEffectBase
4 {
5 private const string _EdgeOnly = "_EdgeOnly";
6 private const string _EdgeColor = "_EdgeColor";
7 private const string _BackgroundColor = "_BackgroundColor";
8
9 [Range(0,1)]
10 public float edgeOnly = 0.0f;
11
12 public Color edgeColor = Color.black;
13
14 public Color backgroundColor = Color.white;
15
16 private void OnRenderImage(RenderTexture source, RenderTexture destination)
17 {
18 if (Material!=null)
19 {
20 Material.SetFloat(_EdgeOnly, edgeOnly);
21 Material.SetColor(_EdgeColor, edgeColor);
22 Material.SetColor(_BackgroundColor, backgroundColor);
23 Graphics.Blit(source, destination, Material);
24 }
25 else
26 Graphics.Blit(source, destination);
27 }
28 }
代码语言:javascript复制
同样是继承自ScreenEffectBase基类,三个参数的意义分别如下:
edgeOnly(shader中:_EdgeOnly):边缘线的叠加程度,0表示完全叠加,1表示只显示边缘线,不显示原图
edgeColor(shader中:_EdgeColor):边缘线的颜色
backgroundColor(shader中:_BackgroundColor):背景颜色,当只显示边缘线时,可以很清晰看出
基类脚本见:
https://cloud.tencent.com/developer/article/1601301
下面是Shader脚本:
代码语言:javascript复制 1 Shader "MyUnlit/EdgeDetection"
2 {
3 Properties
4 {
5 _MainTex ("Texture", 2D) = "white" {}
6 }
7 SubShader
8 {
9 Tags { "RenderType"="Opaque" }
10
11 Pass
12 {
13 ZTest always
14 Cull off
15 ZWrite off
16
17 CGPROGRAM
18 #pragma vertex vert
19 #pragma fragment frag
20
21 #pragma multi_compile_fog
22
23 #include "UnityCG.cginc"
24
25 struct appdata
26 {
27 float4 vertex : POSITION;
28 float2 uv : TEXCOORD0;
29 };
30
31 struct v2f
32 {
33 half2 uv[9] : TEXCOORD0;
34 UNITY_FOG_COORDS(1)
35 float4 pos : SV_POSITION;
36 };
37
38 sampler2D _MainTex;
39 //纹理映射到[0,1]之后的大小,用于计算相邻区域的纹理坐标
40 half4 _MainTex_TexelSize;
41 //定义控制脚本中对应的参数
42 fixed _EdgeOnly;
43 fixed4 _EdgeColor;
44 fixed4 _BackgroundColor;
45
46 v2f vert (appdata v)
47 {
48 v2f o;
49 o.pos = UnityObjectToClipPos(v.vertex);
50
51 half2 uv = v.uv;
52 half2 size = _MainTex_TexelSize;
53 //计算周围像素的纹理坐标位置,其中4为原始点,右侧乘积因子为偏移的像素单位,坐标轴为左下角原点,右上为 x, y方向,与uv的坐标轴匹配
54 o.uv[0] = uv size * half2(-1, 1);
55 o.uv[1] = uv size * half2(0, 1);
56 o.uv[2] = uv size * half2(1, 1);
57 o.uv[3] = uv size * half2(-1, 0);
58 o.uv[4] = uv size * half2(0, 0);
59 o.uv[5] = uv size * half2(1, 0);
60 o.uv[6] = uv size * half2(-1, -1);
61 o.uv[7] = uv size * half2(0, -1);
62 o.uv[8] = uv size * half2(1, -1);
63
64 UNITY_TRANSFER_FOG(o,o.pos);
65 return o;
66 }
67 //计算对应像素的最低灰度值并返回
68 fixed minGrayCompute(v2f i,int idx)
69 {
70 return Luminance(tex2D(_MainTex, i.uv[idx]));
71 }
72 //利用Sobel算子计算最终梯度值
73 half sobel(v2f i)
74 {
75 const half Gx[9] = {
76 - 1,0,1,
77 - 2,0,2,
78 - 1,0,1
79 };
80 const half Gy[9] = {
81 -1,-2,-1,
82 0, 0, 0,
83 1, 2, 1
84 };
85 //分别计算横向和纵向的梯度值,方法为各项对应元素相乘并相加
86 half graX = 0;
87 half graY = 0;
88
89 for (int it = 0; it < 9; it )
90 {
91 graX = Gx[it] * minGrayCompute(i, it);
92 graY = Gy[it] * minGrayCompute(i, it);
93 }
94 //绝对值相加近似模拟最终梯度值
95 return abs(graX) abs(graY);
96 }
97
98 fixed4 frag (v2f i) : SV_Target
99 {
100 half gra = sobel(i);
101 fixed4 col = tex2D(_MainTex, i.uv[4]);
102 //利用得到的梯度值进行插值操作,其中梯度值越大,越接近边缘的颜色
103 fixed4 withEdgeColor = lerp( col, _EdgeColor, gra);
104 fixed4 onlyEdgeColor = lerp( _BackgroundColor, _EdgeColor, gra);
105 fixed4 color = lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
106
107 UNITY_APPLY_FOG(i.fogCoord, color);
108 return color;
109 }
110 ENDCG
111 }
112 }
113 }
效果如下:
代码语言:javascript复制