关于屏幕后效果的控制类详细见之前写的另一篇博客:
https://www.cnblogs.com/koshio0219/p/11131619.html
这篇主要是基于之前的控制类, 实现另一种常见的屏幕后效果 -- 边缘检测.
概念和原理部分:
首先, 我们需要知道在图形学中经常处理像素的一种操作 -- 卷积.
卷积操作的实质在于, 对于图像中的每个像素与其周围的像素进行的重新融合计算行为, 以得到不同的像素处理效果, 例如锐化图像, 模糊图像, 检测边缘等.
卷积操作通过不同的像素融合算法能得到各不相同的效果, 这主要依赖于卷积核.
可以把卷积核看作是一个 n 行 n 列方阵, 原始像素则位于方阵的中心.
边缘检测的卷积核也叫边缘检测算子, 以 Sobel 算子为例, 形如:
需要特别注意的是, 这里的 Sobel 算子是基于坐标轴以屏幕左上为原点, 右下分别为 + x,+y 方向的, 而不是类似于 uv 坐标轴的以屏幕左下为原点, 右上分别为 + x,+y 方向的. 这一点需要特别注意, 不然后面的程序很容易写错.
其中 Gx 和 Gy 分别是纵向和横向两个方向的边缘线检测, 你可以通过去掉矩阵中的零元素来想象, 因为零元素不会对像素产生任何影响. 也就是说, Gx 是为了计算横向的梯度值, Gy 为了计算纵向的梯度值.
横向的梯度值检测出来的是纵向的边缘线, 纵向的梯度值检测出来的是横向的边缘线. 这一点非常容易混淆, 需要特别注意.
利用边缘检测算子除了融合像素外, 主要是为了计算出像素的梯度值.
一个像素和周围的像素之前梯度值很高, 意味着它与周围的像素差异很大, 我们可以想象这个像素和周围的像素格格不入, 存在一个无法逾越的阶梯; 那么就可以这么认为, 这个像素可以作为一条边界中的值.
对图像中的每个像素都如此处理, 最终就能得到图像的边缘. 这也就是边缘检测的实质内容.
计算方法:
1. 得到每个像素周围的 8 个像素的坐标位置以便与 Sobel 算子进行计算, 类似于:(排列方式应该与 Sobel 算子的坐标轴保持一致)
uv[0] | uv[1] | uv[2] |
uv[3] | uv[4](原始像素点) | uv[5] |
uv[6] | uv[7] | uv[8] |
但因为 uv 坐标的原点在左下角, 因此在计算 uv[0]-uv[8]时, 若依据 uv[4]为原始像素点, 则它们的偏移可以表示为如下情况:
(-1,1)uv[0] | (0,1)uv[1] | (1,1)uv[2] |
(-1,0)uv[3] | (0,0)uv[4] | (1,0)uv[5] |
(-1,-1)uv[6] | (0,-1)uv[7] | (1,-1)uv[8] |
2. 通过偏移值可以很快计算出目标像素的周围像素位置坐标信息, 随后与 Gx 和 Gy 对应元素分别进行横向和纵向的梯度值计算, 也就是分别进行纵向和横向的边缘检测:
具体计算方法为各项对应元素相乘并相加, 注意, 不要与矩阵的相乘计算混淆.
Gx 和 Gy 计算结束后再将它们开平方和; 但往往为了简化 GPU 的计算量, 可以直接取各自的绝对值再相加, 得到最终的梯度值 G.
3. 计算出梯度值后对原始的采样结果进行关于 G 的插值操作以得到最终的图像.
程序实现:
首先是参数调控的脚本:
- using UnityEngine;
- public class EdgeDetectionCtrl : ScreenEffectBase
- {
- private const string _EdgeOnly = "_EdgeOnly";
- private const string _EdgeColor = "_EdgeColor";
- private const string _BackgroundColor = "_BackgroundColor";
- [Range(0,1)]
- public float edgeOnly = 0.0f;
- public Color edgeColor = Color.black;
- public Color backgroundColor = Color.white;
- private void OnRenderImage(RenderTexture source, RenderTexture destination)
- {
- if (Material!=null)
- {
- Material.SetFloat(_EdgeOnly, edgeOnly);
- Material.SetColor(_EdgeColor, edgeColor);
- Material.SetColor(_BackgroundColor, backgroundColor);
- Graphics.Blit(source, destination, Material);
- }
- else
- Graphics.Blit(source, destination);
- }
- }
同样是继承自 ScreenEffectBase 基类, 三个参数的意义分别如下:
edgeOnly(shader 中:_EdgeOnly): 边缘线的叠加程度, 0 表示完全叠加, 1 表示只显示边缘线, 不显示原图
edgeColor(_EdgeColor): 边缘线的颜色
backgroundColor(_BackgroundColor): 背景颜色, 当只显示边缘线时, 可以很清晰看出
下面是 Shader 脚本:
- Shader "MyUnlit/EdgeDetection"
- {
- Properties
- {
- _MainTex ("Texture", 2D) = "white" {}
- }
- SubShader
- {
- Tags { "RenderType"="Opaque" }
- Pass
- {
- ZTest always
- Cull off
- ZWrite off
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- // make fog work
- #pragma multi_compile_fog
- #include "UnityCG.cginc"
- struct appdata
- {
- float4 vertex : POSITION;
- float2 uv : TEXCOORD0;
- };
- struct v2f
- {
- half2 uv[9] : TEXCOORD0;
- UNITY_FOG_COORDS(1)
- float4 pos : SV_POSITION;
- };
- sampler2D _MainTex;
- // 纹理映射到 [0,1] 之后的大小, 用于计算相邻区域的纹理坐标
- half4 _MainTex_TexelSize;
- // 定义控制脚本中对应的参数
- fixed _EdgeOnly;
- fixed4 _EdgeColor;
- fixed4 _BackgroundColor;
- v2f vert (appdata v)
- {
- v2f o;
- o.pos = UnityObjectToClipPos(v.vertex);
- half2 uv = v.uv;
- half2 size = _MainTex_TexelSize;
- // 计算周围像素的纹理坐标位置, 其中 4 为原始点, 右侧乘积因子为偏移的像素单位, 坐标轴为左下角原点, 右上为 + x,+y 方向, 与 uv 的坐标轴匹配
- o.uv[0] = uv + size * half2(-1, 1);
- o.uv[1] = uv + size * half2(0, 1);
- o.uv[2] = uv + size * half2(1, 1);
- o.uv[3] = uv + size * half2(-1, 0);
- o.uv[4] = uv + size * half2(0, 0);
- o.uv[5] = uv + size * half2(1, 0);
- o.uv[6] = uv + size * half2(-1, -1);
- o.uv[7] = uv + size * half2(0, -1);
- o.uv[8] = uv + size * half2(1, -1);
- UNITY_TRANSFER_FOG(o,o.pos);
- return o;
- }
- // 计算对应像素的最低灰度值并返回
- fixed minGrayCompute(v2f i,int idx)
- {
- return Luminance(tex2D(_MainTex, i.uv[idx]));
- }
- // 利用 Sobel 算子计算最终梯度值
- half sobel(v2f i)
- {
- const half Gx[9] = {
- - 1,0,1,
- - 2,0,2,
- - 1,0,1
- };
- const half Gy[9] = {
- -1,-2,-1,
- 0, 0, 0,
- 1, 2, 1
- };
- // 分别计算横向和纵向的梯度值, 方法为各项对应元素相乘并相加
- half graX = 0;
- half graY = 0;
- for (int it = 0; it < 9; it++)
- {
- graX += Gx[it] * minGrayCompute(i, it);
- graY += Gy[it] * minGrayCompute(i, it);
- }
- // 绝对值相加近似模拟最终梯度值
- return abs(graX) + abs(graY);
- }
- fixed4 frag (v2f i) : SV_Target
- {
- half gra = sobel(i);
- fixed4 col = tex2D(_MainTex, i.uv[4]);
- // 利用得到的梯度值进行插值操作, 其中梯度值越大, 越接近边缘的颜色
- fixed4 withEdgeColor = lerp( col, _EdgeColor, gra);
- fixed4 onlyEdgeColor = lerp( _BackgroundColor, _EdgeColor, gra);
- fixed4 color = lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
- UNITY_APPLY_FOG(i.fogCoord, color);
- return color;
- }
- ENDCG
- }
- }
- }
效果如下:
来源: https://www.cnblogs.com/koshio0219/p/11137155.html