Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

边缘检测

在屏幕后处理的Shader中 需要设置深度测试、剔除、深度写入 - ZTest Always 打开深度测试 - Cull Off 关闭剔除 - ZWrite Off 关闭深度写入 主要是避免它"挡住"后面的渲染物体

1.边缘检测是什么?


边缘检测效果,
是一种用于突出图像中的边缘,使物体的轮廓更加明显的图像处理技术
边缘检测的主要目的是找到图像中亮度变化显著的区域,这些区域通常对应于物体的边界
边缘检测相当于利用 Shader 代码自动给屏幕图像进行描边处理 2024-11-14 161329.png ## 2.基本原理


计算每个像素的灰度值,用灰度值结合卷积核进行卷积运算,得到该像素的梯度值
梯度值越大越靠近边界,越趋近于描边颜色
梯度值越小表明不是边界位置,越趋近于原始颜色

所以基本步骤如下:

  1. 得到当前像素以及其上下左右、左上左下、右上右下共9个像素的灰度值
  2. 用这9个灰度值和Sobel算子进行卷积计算得到梯度值G = abs(Gx) + abs(Gy)
  3. 最终颜色= lerp(原始颜色,描边颜色,梯度值)

3.一些关键的知识点

灰度值、卷积、卷积核、梯度值

1.)灰度值


由于人眼对不同颜色的敏感度不同,所以在计算平均值时不会直接使用算数平均(R+G+B)/3
在图形学中我们一般使用加权平均法来计算灰度值


例如:
下面是基于Rec. 709标准计算的灰度值(高清电视和许多数字图像格式中常用的标准)
灰度值L = 0.2126R + 0.7152G + 0.0722*B

2.)卷积


卷积是一种数学计算方式
我们首先通过一个比喻来理解卷积在边缘检测中的作用
它就像是要用一个放大镜(卷积核)在图片上移动,放大镜(卷积核)的作用是帮助我们看到图
片上的细微变化。当我们用这个放大镜(卷积核)扫描整张图片时,它能帮助我们发现图片上哪
些地方颜色变化突然,这些突然变化的地方往往就是物体的边缘了

2024-11-14 161652.png

3.)卷积核

从卷积的计算方式我们可以得知,其中卷积核(也被称为边缘检测因子) 是非常重要的一个元素,在图形学中,有三种常用的卷积核(边缘检测因子) 他们分别是: - Roberts 算子:由拉里·罗伯茨(Larry Roberts)于1965年提出 - Prewitt 算子:由约翰·普雷维特(John Prewitt)于1970年提出 - Sobel 算子:由欧文·索贝尔(Irwin Sobel)于1968年提出 他们各有千秋,但是在图形学中最常用的还是Sobel算子 因为它更适合高精度的边缘检测 2024-11-14 161813.png

4.)梯度值


我们可以看到三种算子都包含了两个方向的卷积核
他们分别用来检测水平和竖直方向上的边缘信息
在边缘检测的卷积计算时,只需要对每个像素进行两次卷积计算即可
这样就可以得到两个方向的梯度值Gx 和Gy
而该像素的整体梯度值G = abs(Gx) + abs(Gy)

4.如何得到当前像素周围8个像素位置


Unity 提供给我们用于访问纹理对应的每个纹素(像素)的大小的变量
float4 纹理名_TexelSize
是一种类似于 纹理名_ST 的一种变量

其中xyzw的含义:(假设纹理宽高为1024 * 768) - x : 1 / 纹理宽度 = 1/1024 - y : 1 / 纹理高度 = 1/768 - y : 纹理宽度 = 1024 - y : 纹理高度 = 768


基于这个变量,我们可以进行uv坐标的偏移计算
在顶点着色器函数或者片元着色器函数中计算都行
但是建议在顶点着色器函数中计算,可以节约计算量
片元着色器中直接使用插值的结果也不会影响纹理坐标的计算结果

演示图像 通过纹素计算对应的像素uv坐标

5.具体实现

1.)主要步骤

a.Shader部分

  1. 声明属性,进行属性映射
    • 主纹理 _MainTex
    • 描边用的颜色 _EgdeColor
    • 注意:映射时使用内置的纹素变量
  2. 屏幕后处理标配
    • ZTest Always
    • Cull off
    • ZWrite off
  3. 结构体相关
    • 顶点
    • uv数组 用于存储9个像素点的坐标
  4. 顶点着色器
    1. 顶点坐标转化
    2. 用uv坐标数组装载9个像素uv的坐标
  5. 片元着色器
    1. 利用卷积获取梯度值(可以声明一个Sobel算子计算函数,和一个灰度值计算函数)
    2. 利用梯度值在源颜色和边缘颜色之间进行插值得到最终的颜色
  6. FallBack off
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
Shader "Unlit/EdgeDetection"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_EdgeColor ("Edge Color", Color) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }

Pass
{
//后处理标配
ZTest Always
Cull Off
ZWrite Off

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

struct v2f
{
//用于存储9个像素uv坐标的变量
half2 uv[9] : TEXCOORD0;

float4 vertex : SV_POSITION;
};

sampler2D _MainTex;
//Unity内置纹素变量
float4 _MainTex_TexelSize;
fixed4 _EdgeColor;

v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
//当前的顶点纹理坐标
half2 uv = v.texcoord;
//对9个像素的uv坐标进行计算
o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1,-1);
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(0,-1);
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(1,-1);
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1,0);
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0,0);
o.uv[5] = uv + _MainTex_TexelSize.xy * half2(1,0);
o.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1,1);
o.uv[7] = uv + _MainTex_TexelSize.xy * half2(0,1);
o.uv[8] = uv + _MainTex_TexelSize.xy * half2(1,1);
return o;
}
//计算颜色的灰度值
fixed calcLuminance(fixed4 color)
{
return dot(color, half3(0.2126, 0.7152, 0.0722));
}
//Soble算子相关的卷积计算
half Sobel(v2f i)
{
//soble算子的两个卷积核
half Gx[9] = { -1 , -2, -1,
0 , 0, 0 ,
1 , 2, 1 };
half Gy[9] = { -1, 0, 1,
-2, 0, 2,
-1, 0, 1 };
half L;//灰度值
half edgeX = 0;//水平方向梯度值
half edgeY = 0;//竖直方法梯度值
for (int j = 0; j < 9; j++)
{
//采样颜色后计算灰度值 并记录下来
L = calcLuminance(tex2D(_MainTex, i.uv[j]));
//计算对应方向的梯度值
edgeX += Gx[j] * L;
edgeY += Gy[j] * L;
}
//返回并计算该像素最终的梯度值
//half G = abs(edgeX) + abs(edgeY);
return abs(edgeX) + abs(edgeY);
}


fixed4 frag (v2f i) : SV_Target
{
//利用Sobel算子计算梯度值
half edge = Sobel(i);
//根据梯度值 在源颜色和边缘颜色进行插值
fixed4 Color = lerp(tex2D(_MainTex, i.uv[4]),_EdgeColor,edge);
return Color;
}
ENDCG
}
}
Fallback off
}

b.C# 部分

  1. 继承屏幕后处理基类
  2. 声明边缘颜色变量,用于控制效果变化
  3. 重写UpdataProperty方法,用于设置材质球的颜色
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class EdgeDetection : PostEffectBase
    {
    public Color edgeColor = Color.white;

    protected override void UpadateProerty()
    {
    if (material)
    {
    material.SetColor("_EdgeColor", edgeColor);
    }
    }
    }

6.纯色背景功能

1.)什么是纯色背景


在边缘描边时,有时只想保留描边的边缘线
不想要显示原图的背景色
比如:把整个背景变成白色、黑色、等等自定义颜色
而抛弃原本图片的颜色信息
效果就像是一张描边的图片

2.)具体实现

  1. 在描边的基础上进行修改
  2. 属性声明与映射
    1. 添加背景颜色程度的变量 _BackgroudExtent (0表示保留原始颜色,1表示纯色)
    2. 添加自定义背景颜色 _BackgroundColor (定义用于替换原始颜色的颜色)
  3. 修改片源着色器
    1. 利用插值运算,在 原始颜色 和 纯色背景颜色描边 之间通过背景颜色程度的变量 进行插值
  4. 在C#代码中
    1. 添加背景颜色程度的变量
    2. 添加背景颜色

评论