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

深度纹理实现边缘检测

一、为什么要实现深度纹理实现边缘检测


之前的边缘检测原理是利用Socle算子基于像素灰度值检测计算而来
这种计算方式依赖于像素的颜色(灰度值)变化来识别边缘,会受到物体纹理和阴影颜色等因素的影响
这种方式可能不能反应出物体的真实轮廓


而基于深度+法线纹理来实现屏幕边缘检测屏幕后期处理效果
不受纹理和光照的影响,只会根据渲染物体的模型信息(深度、法线)进行判断

注意: - 对于2D图片,基于灰度值的边缘检测更加合适(因为2D图片中的深度和法线信息往往是一致的,不存在差异) - 对于3D场景,基于深度纹理+法线纹理的边缘检测更加合适

二、基本原理

一句话:基于Robert交叉算子,通过比较对角线上的像素的深度和法线值,判断是否在边缘上

1.关键点

  1. 如何获取对角线上的像素
  2. 如何进行深度和法线值的比较
  3. 如何决定是否在边缘上
  4. 注意点:不会进行卷积计算

2. 解决关键点的具体方法

1.如何得到对角线上的像素

顶点着色器中,利用纹素进行uv坐标的偏移计算 并且我们可以添加一个可控的 采样偏移距离变量 _SampleDitance 它可以用来决定描边的粗细,值越大描边越粗

原理是: 采样离中心像素越近,检测的变化更细微,深度和法线变化值越小,边缘会较细 采样离中心像素越远,检测范围较大,深度和法线值变化大,边缘会较粗

2.如何进行深度和法线纹理的比较

片元着色器中,利用顶点着色器中得到的uv坐标,在深度+法线纹理中进行采样,得到深度和法线值 在求出对角线上的两个像素的 深度值差和法线值差 如果其中一个值大于了自定义的阈值 那么我们认为该像素点在物体的边缘上

3.如何决定是否在边缘上

自定义计算规则

三、实现示例步骤

1.Shader部分

  1. 声明属性,属性映射
    • 主纹理 _MainTex
    • 边缘检测强度 _EdgeOnly(0-显示场景,1-显示边缘 用于控制自定义背景的颜色)
    • 描边颜色 _EdgeColor
    • 背景颜色 _BackgroudColor
    • 采样偏移距离 _SampleDistance
    • 深度敏感度 _SensitivityDepyh
    • 法线敏感度 _SensitiviyNormal
    • 属性映射时,加入纹素(_MainTex_TexelSize)和深度法线纹理映射(_CameraDephtNormalsTexture)
  2. 屏幕后处理标配
    • ZTest Always
    • ZWrite Off
    • Cull Off
  3. 结构体
    • 顶点坐标
    • uv数组,5个空间(储存中心点、对角线4个点)
  4. 顶点着色器
    • 顶点坐标转化
    • 5个uv坐标赋值 注意顺序
      中心点、左上、右下、右上、左下
  5. 片元着色器
    • 直接采样四个对角线四个点的深度法线信息
    • 实现一个用于比较两点深度、法线信息的函数,返回0或者1,方便进行插值计算
    • 声明一个插值变量,0代表边缘色、1代表原颜色
    • 考虑背景色插值
  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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
Shader "Unlit/EdgeDetetionWithDepthAndNormalTexture"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
//边缘检测强度 用于控制自定义背景颜色的程度 0-显示原始颜色 1-显示自定义背景颜色
_EdgeOnly("EdgeOnly",Float) = 0
//描边颜色
_EdgeColor("EdgeColor",Color) = (0,0,0,1)
//背景颜色
_BackgroundColor("BackgroundColor",Color) = (1,1,1,1)
//采样偏移距离 用于控制描边的粗细 值越大 越粗
_SampleDistance("SampleDistance",Float) = 0.01
//深度敏感度
_SensitivityDepth("SensitivityDepth",Float) = 0.01
//法线敏感度
_SensitivityNormal("SensitivityNormal",Float) = 0.01
}
SubShader
{
//屏幕后厨标配
Zwrite Off
Cull Off
ZTest Always
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

struct v2f
{
//用于存储5个像素的uv坐标
half2 uv[5] : TEXCOORD0;
float4 vertex : SV_POSITION;
};

sampler2D _MainTex;
//纹素 用于进行uv坐标偏移 取得周围像素的uv坐标
half4 _MainTex_TexelSize;
//深度加法线纹理
sampler2D _CameraDepthTexture;
fixed _EdgeOnly;
fixed4 _EdgeColor;
fixed4 _BackgroundColor;
float _SampleDistance;
float _SensitivityDepth;
float _SensitivityNormal;

//用于比较两个点中深度和法线纹理中的信息 用来判断是否边缘
//返回值:
//1 - 法线和深度值基本相同
//0 - 代表不在一个平面上
half CheckSame(half4 depthNormal1, half4 depthNormal2)
{
//分别得到两个点信息的深度和法线
//第一个点
//得到深度值
float depth1 = DecodeFloatRG(depthNormal1.zw);
//得到法线的xy
float2 normal1 = depthNormal1.xy;
//第二个点
//得到深度值
float depth2 = DecodeFloatRG(depthNormal2.zw);
//得到法线的xy
float2 normal2 = depthNormal1.xy;

//法线的差异计算
//计算两条法线的xy的插值 并且乘以敏感度
float2 normalDiff = abs(normal1 - normal2) * _SensitivityNormal;
//判读两个法线是否在同一个平面上
int isSameNormal = (normalDiff.x + normalDiff.y) < 0.1 ? 1 : 0;

//深度的差异计算
float depthDiff = abs(depth1 - depth2) * _SensitivityDepth;
//判读两个深度是否在同一个平面上
int isSameDepth = depthDiff < 0.1 * depth1 ? 1 : 0;
//返回值:
//1 - 法线和深度值基本相同
//0 - 代表不在一个平面上
return isSameNormal * isSameDepth ? 1 : 0;
}
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);

half2 uv = v.texcoord;
//中心点
o.uv[0] = uv;
//左上
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(-1, 1) * _SampleDistance;
//右下
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1) * _SampleDistance;
//右上
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(1, 1) * _SampleDistance;
//左下
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(-1, -1) * _SampleDistance;
return o;
}

fixed4 frag (v2f i) : SV_Target
{
//获取四个点的深度和法线值
half4 TL = tex2D(_CameraDepthTexture, i.uv[1]);
half4 BR = tex2D(_CameraDepthTexture, i.uv[2]);
half4 TR = tex2D(_CameraDepthTexture, i.uv[3]);
half4 BL = tex2D(_CameraDepthTexture, i.uv[4]);

//根据深度+法线信息 判断是否时边缘
half edgeLerpValue = 1;
//得到判断结果 返回1 代表在同一个平面上 0 代表不在一个平面上
edgeLerpValue *= CheckSame(TL, BR);
edgeLerpValue *= CheckSame(TR, BL);

//通过插值进行颜色变换
fixed4 wihtEdgeColor = lerp(_EdgeColor,tex2D(_MainTex, i.uv[0]),edgeLerpValue);
fixed4 onlyEdgeColor = lerp(_EdgeColor,_BackgroundColor,edgeLerpValue);


return lerp(wihtEdgeColor,onlyEdgeColor,_EdgeOnly);
}
ENDCG
}
}
}

2.C#部分

  1. 继承PostEffectBase
  2. 声明可控变量
  3. Start中开启深度法线纹理
    注意:不要直接-、要 |= ,避免关闭深度法线纹理
  4. 重写UpdataProperty函数
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
public class EdgeDetetionWithDepthAndNormalTexture : PostEffectBase
{
[Range(0,1)]
public float edgeOnly = 0;

public Color edgeColor = Color.black;
public Color backgroundColor = Color.white;

public float sampleDistance = 1.0f;
public float sensitivityDepth = 1.0f;
public float sensitivityNormal = 1.0f;



void Start()
{
//避免关闭深度纹理 影响其他屏幕后处理效果 使用|=(或等)
Camera.main.depthTextureMode |= DepthTextureMode.DepthNormals;
}
protected override void UpadateProerty()
{
if (material != null)
{
material.SetFloat("_EdgeOnly", edgeOnly);
material.SetColor("_EdgeColor", edgeColor);
material.SetColor("_BackgroundColor", backgroundColor);
material.SetFloat("_SampleDistance", sampleDistance);
material.SetFloat("_SensitivityDepth", sensitivityDepth);
material.SetFloat("_SensitivityNormal", sensitivityNormal);
}
}
}

评论