邊緣檢測
邊緣檢測有兩種方式:
色差檢測:以像素中心周圍的像素顏色為根據判斷中心像素點是不是在邊緣線上。
深度法線檢測:檢測像素點所對應的視角空間中的深度和法線,以此做判斷當前點是否在邊緣上。
色差檢測
我們可以首先回想一下邊到底是如何形成的。如果相鄰像素之間存在差別明顯的顏色、亮度、紋理等屬性,我們就會認為它們之間應該有一條邊界。這種相鄰像素之間的差值可以用梯度(gradient)來表示,可以想象得到,邊緣處的梯度絕對值會比較大。基於這樣的理解,有幾種不同的邊緣檢測算子被先后提出來,分別是Robert算子、Prewitt算子、Sobel算子。
Robert算子:
\(\begin{bmatrix} -1 & 0 \\ 0 & -1 \end{bmatrix}\)
Prewitt算子:
\(\begin{bmatrix} -1 & -1 & -1 \\ 0 & 0 & 0 \\ 1 & 1 & 1 \end{bmatrix}\)
Sobel算子:
\(\begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \end{bmatrix}\)
色差檢測邊緣的原理:用一個卷積核做卷積運算,這個卷積核一般采用Sobel算子,先把卷積核置於像素中心做卷積運算,再翻轉卷積核做一次卷積運算(其實就相當於先求出水平X方向梯度差Gx,再求出垂直y方向上梯度差Gy),再根據這兩個梯度值得到整體梯度值G。
\(G = \sqrt{Gx^2 + Gy^2}\)
但是在Shader中,因為開根號的開銷比較大所以,可以用加法運算代替。
\(G = \|Gx\| + \|Gy\|\)
根據這個梯度值G可以檢測哪些像素點是邊緣。
下面以Sobel算子為例寫一個Shader:
//C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class WSPostEffect : MonoBehaviour
{
public Material material = null;
public bool IsEdgeOnly;
public Color edgeColor;
public Color backgroundColor;
public float edgeFactor;
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (material == null)
Graphics.Blit(source, destination);
else
{
material.SetFloat("_EdgeOnly", IsEdgeOnly ? 1.0f : 0.0f);
material.SetColor("_EdgeColor", edgeColor);
material.SetColor("_BackgroundColor", backgroundColor);
material.SetFloat("_EdgeFactor", edgeFactor);
Graphics.Blit(source, destination, material, -1);
}
}
}
//Shader
Shader "WS/EdgeDetect"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Transparent" }
LOD 100
Pass
{
ZTest Always
ZWrite Off
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
//像素點及其周圍的9個uv坐標
float2 uv[9] : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
half4 _MainTex_TexelSize;
//是否只顯示邊緣線條
fixed _EdgeOnly;
//邊緣線顏色
fixed4 _EdgeColor;
//背景色
fixed4 _BackgroundColor;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
half2 uv = v.uv;
//計算限速點及其周圍的9個uv坐標,放在頂點着色器計算能減少
o.uv[0] = uv + _MainTex_TexelSize.xy * (-1, -1);
o.uv[1] = uv + _MainTex_TexelSize.xy * (0, -1);
o.uv[2] = uv + _MainTex_TexelSize.xy * (1, -1);
o.uv[3] = uv + _MainTex_TexelSize.xy * (-1, 0);
o.uv[4] = uv + _MainTex_TexelSize.xy * (0, 0);
o.uv[5] = uv + _MainTex_TexelSize.xy * (1, 0);
o.uv[6] = uv + _MainTex_TexelSize.xy * (-1, 1);
o.uv[7] = uv + _MainTex_TexelSize.xy * (0, 1);
o.uv[8] = uv + _MainTex_TexelSize.xy * (1, 1);
return o;
}
//計算像素的明度值
fixed luminance(fixed4 color)
{
return 0.299 * color.r + 0.587 * color.g + 0.114 * color.b;
}
half Sobel(v2f v)
{
//Sobel算子及其翻轉后的算子
const half Gx[9] = {-1, -2, -1,
0, 0, 0,
1, 2, 1};
const half Gy[9] = {-1, 0, 1,
-2, 0, 2,
-1, 0, 1};
half texColor;
//水平梯度值
half edgeX = 0;
//垂直梯度值
half edgeY = 0;
for (int i = 0; i < 9; ++i)
{
texColor = luminance(tex2D(_MainTex, v.uv[i]));
edgeX += texColor * Gx[i];
edgeY += texColor * Gy[i];
}
//越小越是邊緣
half edge = 1 - sqrt(edgeX * edgeX + edgeY * edgeY);
//關注性能時可改為
//half edge = 1 - abs(edgeX) - abs(edgeY);
return edge;
}
fixed4 frag (v2f i) : SV_Target
{
half edge = Sobel(i);
fixed4 edgeOnlyColor = lerp(_EdgeColor, _BackgroundColor, edge);
fixed4 edgeWithColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), step(0.3, edge));
return lerp(edgeWithColor, edgeOnlyColor, _EdgeOnly);
}
ENDCG
}
}
}
效果:
使用前:
使用后:
深度法線檢測
原理:分別比較像素兩邊對角線的深度和法線差異,判斷深度或者法線差異是否達到一個閾值,打到閾值就判斷為這個點在邊緣上,只要其中一邊的對角線上檢測出邊緣就代表這個像素點在邊緣上。其實可以看出來這里用的是Robert算子。
簡易實現:
//C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class WSPostEffect : MonoBehaviour
{
private Camera cam;
public Material material = null;
public bool IsEdgeOnly;
public Color edgeColor;
private void Awake()
{
cam = this.gameObject.GetComponent<Camera>();
cam.depthTextureMode |= DepthTextureMode.DepthNormals;
}
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (material == null)
Graphics.Blit(source, destination);
else
{
material.SetFloat("_EdgeOnly", IsEdgeOnly ? 1.0f : 0.0f);
material.SetColor("_EdgeColor", edgeColor);
Graphics.Blit(source, destination, material, -1);
}
}
}
//Shader
Shader "WS/WS_EdgeDetectDepthNormal"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Transparent" }
Pass
{
ZTest Always
ZWrite Off
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv[5] : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
half4 _MainTex_TexelSize;
fixed4 _EdgeOnly;
fixed4 _EdgeColor;
fixed4 _BackgroundColor;
sampler2D _CameraDepthNormalsTexture;
v2f vert (appdata v)
{
v2f o;
half2 uv = v.uv;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv[0] = uv;
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
uv.y = 1 - uv.y;
#endif
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(1, 1);
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(-1, 1);
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(1, -1);
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(-1, -1);
return o;
}
float CheckSame(half4 sample1, half4 sample2)
{
//這個並不是真正的法線,只是說可以直接拿這個xy去作比較得到兩個法線的差異性
half2 sample1_normal = sample1.xy;
float sample1_depth = DecodeFloatRG(sample1.zw);
half2 sample2_normal = sample2.xy;
float sample2_depth = DecodeFloatRG(sample2.zw);
//法線差異
half2 diff_normal = abs(sample1_normal - sample2_normal);
int is_same_normal = (diff_normal.x, diff_normal.y) < 0.1;
//深度差異
float diff_depth = abs(sample1_depth - sample2_depth);
//這里這么做是想做相對比較,如果sample1的深度值很大的話,那么小的深度差異可以忽略,大的深度差才算。如果sample1的深度值比較小的話,那么在小的深度差異之下想要檢測出邊緣出來就要用較小的深度差異做判斷。
//這時候就可以以sample1為基准來判斷他們的深度差是否足夠被判斷成是邊緣,而這個因子是0.1
int is_same_depth = diff_depth < 0.1 * sample1_depth;
return is_same_normal * is_same_depth ? 1.0 : 0.0;
}
fixed4 frag (v2f i) : SV_Target
{
half4 sample1 = tex2D(_CameraDepthNormalsTexture, i.uv[1]);
half4 sample2 = tex2D(_CameraDepthNormalsTexture, i.uv[2]);
half4 sample3 = tex2D(_CameraDepthNormalsTexture, i.uv[3]);
half4 sample4 = tex2D(_CameraDepthNormalsTexture, i.uv[4]);
half edge = 1.0;
edge *= CheckSame(sample1, sample4);
edge *= CheckSame(sample2, sample3);
fixed4 edgeWithColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[0]), edge);
fixed4 edgeOnlyColor = lerp(_EdgeColor, _BackgroundColor, edge);
return lerp(edgeWithColor, edgeOnlyColor, _EdgeOnly);
}
ENDCG
}
}
}
效果:
使用前:
使用后:
只顯示邊緣線: