一、边缘检测
没有什么新的东西,只需了解《UnityShader26:运动模糊》这一章里面的内容就好
1):Roberts算子:
本质就是计算左上角和右下角的差值,乘上右上角和左下角的插值,作为评估边缘的依据
2):边缘检测步骤:
了解 Roberts 算子的逻辑后,实现边沿检测就简单了:
- 拿到当前 uv 坐标相邻的4个采样点,分别作为 Roberts 的四个候选点
- 得到每个点的深度和法线信息
- 计算两个对角线上候选点的法线和深度差
- 判断这个差值是否超过设定的阈值,如果超过则判定当前像素为物体边缘,通过!
直接上代码:
- [ImageEffectOpaque]:具有此属性的图像效果会在不透明物体渲染过后、透明物体渲染前被呈现
using UnityEngine;
using System.Collections;
public class EdgeDetect: PostEffectsBase
{
public Shader shader;
private Material _material;
public Material material
{
get
{
_material = CheckShaderAndCreateMaterial(shader, _material);
return _material;
}
}
private Camera _myCamera;
public Camera myCamera
{
get
{
if (_myCamera == null)
_myCamera = GetComponent<Camera>();
return _myCamera;
}
}
//是否忽略其它片段仅显示描边,非描边片段将会和描边背景色做混合
[Range(0.0f, 1.0f)]
public float edgesOnly = 0.0f;
//描边颜色
public Color edgeColor = Color.black;
//描边背景色
public Color backgroundColor = Color.white;
//采样距离,会影响描边粗细
public float sampleDistance = 1.0f;
//当领域深度和法线相差多少时,会被当作是边界
public float sensitivityDepth = 1.0f;
public float sensitivityNormals = 1.0f;
void OnEnable()
{
//https://docs.unity3d.com/2021.1/Documentation/ScriptReference/DepthTextureMode.html
myCamera.depthTextureMode |= DepthTextureMode.DepthNormals;
}
//如果声明了ImageEffectOpaque,在不透明的物体渲染完成后就会立刻执行OnRenderImage
[ImageEffectOpaque]
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (material != null)
{
material.SetFloat("_EdgeOnly", edgesOnly);
material.SetColor("_EdgeColor", edgeColor);
material.SetColor("_BackgroundColor", backgroundColor);
material.SetFloat("_SampleDistance", sampleDistance);
material.SetVector("_Sensitivity", new Vector4(sensitivityNormals, sensitivityDepth, 0.0f, 0.0f));
Graphics.Blit(src, dest, material);
}
else
{
Graphics.Blit(src, dest);
}
}
}
Shader "Jaihk662/EdgeDetect"
{
Properties
{
_MainTex("Base (RGB)", 2D) = "white" {}
_EdgeOnly("Edge Only", Float) = 1.0
_EdgeColor("Edge Color", Color) = (0, 0, 0, 1)
_BackgroundColor("Background Color", Color) = (1, 1, 1, 1)
_SampleDistance("Sample Distance", Float) = 1.0
_Sensitivity("Sensitivity", Vector) = (1, 1, 1, 1)
}
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
fixed _EdgeOnly;
fixed4 _EdgeColor;
fixed4 _BackgroundColor;
float _SampleDistance;
half4 _Sensitivity;
//如果设置了 myCamera.depthTextureMode |= DepthTextureMode.DepthNormals,那么这里就可以通过 _CameraDepthNormalsTexture 拿到深度法线图
sampler2D _CameraDepthNormalsTexture;
half4 _MainTex_TexelSize;
struct vert2frag
{
float4 pos: SV_POSITION;
half2 uv[5]: TEXCOORD0;
};
vert2frag vert(appdata_img v)
{
vert2frag o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv;
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
uv.y = 1 - uv.y;
#endif
//Robert算子的四个采样点
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,当且仅当法线和深度值都相似才返回1,只要有一者差值超过了对应的阈值则返回0,通过边缘检测
half CheckSame(half4 center, half4 sample)
{
//DecodeFloatRG:解码颜色RG通道信息到Float
//因为只需要知道差异,所以这里没有对法线进行解码,直接取了xy值拿来比较
half2 centerNormal = center.xy;
float centerDepth = DecodeFloatRG(center.zw);
half2 sampleNormal = sample.xy;
float sampleDepth = DecodeFloatRG(sample.zw);
half2 diffNormal = abs(centerNormal - sampleNormal) * _Sensitivity.x;
int isSameNormal = (diffNormal.x + diffNormal.y) < 0.1;
float diffDepth = abs(centerDepth - sampleDepth) * _Sensitivity.y;
int isSameDepth = diffDepth < 0.1 * centerDepth;
return isSameNormal * isSameDepth ? 1.0 : 0.0;
}
fixed4 frag(vert2frag 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 = 0
edge *= CheckSame(sample1, sample2);
edge *= CheckSame(sample3, sample4);
fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[0]), edge);
fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);
return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
}
ENDCG
Subshader
{
ZTest Always Cull Off ZWrite Off
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
FallBack Off
}
这样实现的边缘检测依旧有三个缺陷
- 测只适用于 3D 图像,对于 2D 纹理的边缘检测可能需要采用其它算子
- 一些深度和法线变化很小的边很难被检测处理,例如桌面上的白纸
- 难以实现一些轮廓线的风格渲染
参考资料:
- 《UnityShader入门精要》