US3L11L7——利用深度法线纹理实现边缘检测效果
US3L11L7——利用深度法线纹理实现边缘检测效果
知识回顾 边缘检测屏幕后处理效果
边缘检测效果是什么
是一种用于突出图像中的边缘,使物体的轮廓更加明显的图像处理技术,利用 Shader 代码自动给屏幕图像进行描边处理
边缘检测效果的基本原理
- 得到 当前像素以及其上下左右、左上左下、右上右下共9个像素的灰度值
- 用这 9 个灰度值和 Sobel 算子进行卷积计算得到梯度值
G = abs(Gx) + abs(Gy)
-
最终颜色 = lerp(原始颜色,描边颜色,梯度值)
如何得到当前像素周围 8 个像素位置
利用
float4 纹理名_TexelSize
纹素信息得到当前像素周围8个像素位置
为什么要基于深度法线纹理实现边缘检测
我们之前利用 Sobel 算子基于像素灰度值计算出来的边缘检测效果,其实总体上来说不太理想。
因为这种计算方式依赖于像素的颜色(灰度值)变化来识别边缘,会受到物体纹理和阴影颜色等因素影响。
所以这种制作方式往往不能准确的反应出物体的真实轮廓!
因此我们才会来学习 基于深度+法线纹理来实现边缘检测屏幕后期处理效果,
通过这种方式实现的边缘检测,不会受纹理和光照的影响,
只会根据渲染物体的模型信息(深度、法线)来进行判断检测,这样检测出来的边缘更加的可靠!
不过要注意:
- 对于 2D 图片,基于灰度值的边缘检测更合适
- 对于 3D 场景,基于深度+法线纹理的边缘检测更合适
主要原因是 2D 图片中的深度和法线信息往往是一致的,不存在差异性
利用深度+法线纹理实现边缘检测的基本原理
一句话概括它的基本原理:基于Roberts(罗伯茨)交叉算子,通过比较对角线上的的像素的深度和法线值,判断是否在边缘上。 它的关键点是:
- 如何得到对角线上的像素
- 如何进行深度和法线值的比较
- 如何决定是否在边缘
注意:这里我们不会使用算子去进行卷积运算
针对每个关键点进行分析:
-
如何得到对角线上的像素
在顶点着色器中,利用纹素进行 UV 坐标偏移计算
并且我们可以添加一个可控的采样偏移距离变量_SampleDistance
,它可以用来决定描边的粗细,值越大描边越粗,它的原理是:- 采样离中心像素越近,检测的变化更细微,深度和法线值变化小,边缘会较细
- 采样离中心像素越远,检测的范围较大,深度和法线值变化大,边缘会较粗
1
2
3
4
5
6
7half2 uv = v.texcoord;
o.uv[0] = uv; // 当前中心点位置UV坐标
// 沿着对角线获取周围像素的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; -
如何进行深度和法线值的比较
在片元着色器中,利用顶点着色器中得到的UV坐标,
在深度+法线纹理中进行采样,得到深度值和法线值,再求出对角线上对角的两个像素的深度值差和法线值差。
如果其中一个的差值大于了自定义的阈值,那么我们认为该像素点在物体的边缘上,否则不是边缘 -
如何决定是否在边缘
根据刚才进行的深度和法线值的比较,只要其中之一(深度或者法线)满足了大于自定义阈值的条件,
那么该像素就在边缘上,就使用描边颜色进行颜色处理,关键计算规则如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18half CheckSame(half4 point1, half4 point2)
{
// 得到两个点的法线xy(不需要解码,在同一空间下,只使用xy计算就够用了)和深度值
half2 point1Normal = point1.xy;
float point1Depth = DecodeFloatRG(point1.zw);
half2 point2Normal = point2.xy;
float point2Depth = DecodeFloatRG(point2.zw);
// 法线差异计算,并判断是否在同一法线区间
half2 diffNormal = ads(point1Normal - point2Normal) * 自定义法线敏感度变量;
int isSameNormal = (diffNormal.x + diffNormal.y) < 0.1; // 0.1是一个常用的阈值
// 深度差异计算,并判断是否在同一深度区间
float diffDepth = abs(point1Depth - point2Depth) * 自定义深度敏感度变量;
int isSameDepth = diffDepth < 0.1 * point1Depth; // 0.1是一个常用的阈值
// 返回值:1 - 法线和深度基本相似、0 - 不相似,证明中心像素在物体边缘处
return isSameNormal * isSameDepth ? 1.0 : 0.0;
}
实现 利用深度+法线纹理实现边缘检测屏幕后期处理效果 对应 Shader
-
新建 Shader,取名
EdgeDetectionWithDepthNormalsTexture
,删除无用代码 -
声明属性,属性映射
- 主纹理
_MainTex
- 边缘检测强度
_EdgeOnly
,其中:0
显示场景、1
只显示边缘,用于控制自定义背景色程度 - 描边颜色:
_EdgeColor
- 背景颜色:
_BackgroundColor
- 采样偏移距离:
_SampleDistance
- 深度敏感度:
_SensitivityDepth
- 法线敏感度:
_SensitivityNormal
注意:映射属性时需要加入:
- 纹素:
_MainTex_TexelSize
- 深度+法线纹理:
_CameraDepthNormalsTexture
1
2
3
4
5
6
7
8
9
10Properties
{
_MainTex("Texture", 2D) = "white"{}
_EdgeOnly("EdgeOnly", Float) = 0 // 控制自定义背景颜色显示程度的属性
_EdgeColor("EdgeColor", Color) = (0,0,0,0) // 边缘的描边颜色
_BackgroundColor("BackgroundColor", Color) = (1,1,1,1) // 自定义背景颜色
_SampleDistance("SampleDistance", Float) = 1 // 采样偏移程度,用于控制描边的粗细,值越大越粗
_SansitivityDepth("SansitivityDepth", Float) = 1 // 深度变化敏感度
_SansitivityNormal("SansitivityNormal", Float) = 1 // 法线变化敏感度
}1
2
3
4
5
6
7
8
9sampler2D _MainTex;
half4 _MainTex_TexelSize; // 纹素,用于进行UV坐标偏移和取周围像素时使用
sampler2D _CameraDepthNormalsTexture; // 屏幕的深度+法线纹理
fixed _EdgeOnly; // 控制自定义背景颜色显示程度的属性
fixed4 _EdgeColor; // 边缘的描边颜色
fixed4 _BackgroundColor; // 自定义背景颜色
float _SampleDistance; // 采样偏移程度,用于控制描边的粗细,值越大越粗
float _SansitivityDepth; // 深度变化敏感度
float _SansitivityNormal; // 法线变化敏感度 - 主纹理
-
屏幕后处理标配设置
ZTest Always
、Cull Off
、ZWrite Off
1
2
3
4
5
6
7
8SubShader
{
ZTest Always
Cull Off
ZWrite Off
Pass {/*...*/}
} -
结构体
- 顶点坐标
- UV 数组,5个空间(存储中心点、对角线4个点)
1
2
3
4
5struct v2f
{
half2 uv[5] : TEXCOORD0; // 用于存储5个像素的UV坐标
float4 vertex : SV_POSITION;
}; -
顶点着色器
- 顶点坐标转换
- 5 个 UV 坐标赋值,按这样的顺序:中心点、左上角、右下角、右上角、左下角
1
2
3
4
5
6
7
8
9
10
11
12
13
14v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv; // 中心点
// 沿着对角线获取周围像素的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;
} -
片元着色器
- 直接采样得到对角线四个点的深度法线信息
- 实现一个用于比较两点深度、法线信息的函数,返回 0 或 1,方便进行插值计算
- 声明一个插值变量 0 - 代表使用边缘色,1 - 代表使用源颜色
- 考虑背景色差值
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// 用于比较两个点的深度和法线纹理中的采样得到的消息,用来判断是否是边缘
// 返回值:1 - 法线和深度基本相同,处于同一平面上,0 - 差异大,不在一个平面上
half CheckSame(half4 depthNormal1, half4 depthNormal2)
{
// 法线消息在这个场景下不需要解码就可以对比了
float depth1 = DecodeFloatRG(depthNormal1.zw);
float2 normal1 = depthNormal1.xy;
float depth2 = DecodeFloatRG(depthNormal2.zw);
float2 normal2 = depthNormal2.xy;
// 法线的差异计算,计算两点法线xy的差值,并乘以自定义的敏感度,如果差异不大,返回1,反之为0
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;
return isSameNormal * isSameDepth ? 1 : 0;
}
fixed4 frag (v2f i) : SV_Target
{
// 获取四个点的深度和法线消息
half4 topLeft = tex2D(_CameraDepthNormalsTexture, i.uv[1]);
half4 bottomRight = tex2D(_CameraDepthNormalsTexture, i.uv[2]);
half4 topRight = tex2D(_CameraDepthNormalsTexture, i.uv[3]);
half4 bottomLeft = tex2D(_CameraDepthNormalsTexture, i.uv[4]);
// 根据深度+法线消息,去判断是否是边缘
half edgeLerpValue = 1;
edgeLerpValue *= CheckSame(topLeft, bottomRight);
edgeLerpValue *= CheckSame(topRight, bottomLeft);
// 通过插值进行颜色变化
fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[0]), edgeLerpValue);
fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edgeLerpValue);
// 通过_EdgeOnly决定屏幕显示纯色还是原背景颜色
return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
} -
FallBack Off
1 | Shader "PostEffect/EdgeDetectionWithDepthNormalsTexture" |
实现 利用深度+法线纹理实现边缘检测屏幕后期处理效果 对应 C# 脚本
-
新建 C# 脚本,取名和 Shader 相同
-
继承
PostEffect
具体代码详见:US3S10L4——屏幕后处理基类
-
声明可控变量
-
Start
函数中 开启深度+法线纹理注意:不要直接
=
,要|=
,避免关闭深度纹理 -
重写
UpdateProperty
函数,在其中设置属性即可
1 | using UnityEngine; |
显示效果
可见,基于深度+法线纹理的边缘检测能够更好的根据物体的轮廓来描边