1. 原理
在【Unity Shader入门精要 第12章】屏幕后处理效果(二)中曾经介绍过一种通过Sobel算子对屏幕图像进行卷积处理,从而达到边缘检测效果的方法。这种方法是完全基于渲染纹理中的像素来判断哪里是边缘哪里不是边缘,其优点是简单易实现,但缺点也很明显,因为该方法中完全不关心纹理像素所反映的真实世界信息,因此很容易出现错误的描边,比如阴影的边缘也会被描边。
下面的例子介绍另外一种边缘检测的方法,该方法通过深度+法线纹理对像素两侧做深度和法线的差异化检查:
- 当法线差异化达到一定程度时,认为像素两侧的面出现了朝向上的大幅度变化,此时判定该像素位于边缘上
- 当深度差异化达到一定程度时,认为像素两侧的面出现断层,如水平桌面上放了一个立方体的盒子,水平桌面和盒子的上表面法线一致,但深度差异明显,此时也判定该像素位于边缘上
2. 关于 DepthNormalsTexture 的设置
由于要获取深度+法线纹理,首先需要设置摄像机的深度纹理模式:
private void OnEnable()
{
Camera.depthTextureMode |= DepthTextureMode.DepthNormals;
}
并在Shader中设置变量接收数据:
sampler2D _CameraDepthNormalsTexture;
同时,要生成深度和法线纹理,需要在物体自身渲染所用的 SubShader 中设置标签 “RenderType” = “Opaque”,这里需要特别注意的是,虽然然在 SubShader 和 Pass 中都可以设置 “RenderType” 标签,但在生成 DepthNormalsTexture 时会用到着色器替换技术,而着色器替换技术筛选的是 SubShader 的标签,因此 “RenderType” = “Opaque” 必须要设置在 SubShader 中才能正常生效。
上图是在SubShader中设置 “RenderType” = “Opaque” 时,Frame Debug 显示的生成 DepthNormalsTexture 的过程,可见我们用于测试的Cube信息被正常计算到纹理中。如果将 “RenderType” = “Opaque” 设置在Pass中,生成 DepthNormalsTexture 时就不会包含测试Cube的信息,如下图:
另外,此前我们说过,只有 Pass 包含 “LightMode” = "ShadowCaster " 的物体才会参与到深度纹理的计算中,但对于 DepthNormalsTexture 来讲,即使物体身上没有 “LightMode” = "ShadowCaster " 的Pass,也可以被计算到 DepthNormalsTexture 中,只不过此时纹理中无法获取正确的深度,但法线部分是可以正常使用的。
3. 示例
本次使用的是Roberts算子,其结构如下:
其实正负顺序也无所谓,只是为了比较对角线方向上的差异。
另外,在正常默认情况下,OnRenderImage 方法是在透明物体和不透明物体都渲染完之后才回调执行,但是在本例中,我们希望描边效果只对不透明物体生效,因此需要为 OnRenderImage 增加 [ImageEffectOpaque] 属性。
测试脚本:
using UnityEngine;
public class PostEffect_EdgeDetection_DepthTex : PostEffectBase
{
public Shader EdgeDetectionDepthShader;
public Material EdgeDetectionDepthMaterial;
[Range(1, 8)]
public float SampleStep = 1;
public Color EdgeColor = Color.black;
public Color BackgroundColor = Color.white;
[Range(0, 1)]
public float OnlyEdge = 0;
public Vector4 Sensitivity = new Vector4(0.1f, 0.1f, 0, 0);
private void OnEnable()
{
Camera.depthTextureMode |= DepthTextureMode.DepthNormals;
}
[ImageEffectOpaque]
private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
Material _mat = CheckShaderAndMaterial(EdgeDetectionDepthShader, EdgeDetectionDepthMaterial);
if (null == _mat) Graphics.Blit(src, dest);
else
{
_mat.SetFloat("_SampleStep", SampleStep);
_mat.SetColor("_EdgeColor", EdgeColor);
_mat.SetColor("_BackGroundColor", BackgroundColor);
_mat.SetFloat("_OnlyEdge", OnlyEdge);
_mat.SetVector("_Sensitivity", Sensitivity);
Graphics.Blit(src, dest, _mat);
}
}
}
测试Shader:
Shader "MyShader/Chapter_13/Chapter_13_EdgeDetection_DepthTex_Shader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
ZTest Always ZWrite Off Cull Off
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float4 pos : SV_POSITION;
float2 uv[5] : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_TexelSize;
sampler2D _CameraDepthNormalsTexture;
half _SampleStep;
fixed4 _EdgeColor;
fixed4 _BackGroundColor;
fixed _OnlyEdge;
float4 _Sensitivity;
v2f vert(appdata_img v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv[0] = v.texcoord;
o.uv[1] = v.texcoord + _MainTex_TexelSize * float2(1, 1) * _SampleStep; //右上
o.uv[2] = v.texcoord + _MainTex_TexelSize * float2(-1, -1) * _SampleStep; //左下
o.uv[3] = v.texcoord + _MainTex_TexelSize * float2(-1, 1) * _SampleStep; //左上
o.uv[4] = v.texcoord + _MainTex_TexelSize * float2(1, -1) * _SampleStep; //右下
return o;
}
//计算某一方向上的差异化
//根据差异化结果返回在这一方向上判定是否为边缘
half GetEdgeRatio(float2 _uv_1, float2 _uv_2)
{
float4 _samplerDN_1 = tex2D(_CameraDepthNormalsTexture, _uv_1);
float4 _samplerDN_2 = tex2D(_CameraDepthNormalsTexture, _uv_2);
//比较法线的差异
//原理上虽然是通过法线的差异来反映朝向的差异
//但实际并不需要真的通过点乘之类的方法计算朝向差异
//直接比较采样的法线信息的差异即可,甚至都不需要将采样信息解码成真实法线
float2 _normal_1 = _samplerDN_1.xy;
float2 _normal_2 = _samplerDN_2.xy;
float2 _normal_diff = abs(_normal_1 - _normal_2);
int _sameNormal = (_normal_diff.x + _normal_diff.y) < saturate(_Sensitivity.x);
//比较深度的差异
float _depth_1 = DecodeFloatRG(_samplerDN_1.zw);
float _depth_2 = DecodeFloatRG(_samplerDN_2.zw);
float _depth_diff = abs(_depth_1 - _depth_2);
//这里 * _depth_1 主要是为了根据透视对阈值进行一个缩放
int _sameDepth = _depth_diff < saturate(_Sensitivity.y * _depth_1);
return _sameNormal * _sameDepth ? 0 : 1;
}
fixed4 frag(v2f i) : SV_Target
{
fixed4 _samplerColor = tex2D(_MainTex, i.uv[0]);
//收集roberts算子两个方向上是否为边缘
//只要任意方向是边缘即判定为边缘
float _edge = 0;
_edge += GetEdgeRatio(i.uv[1], i.uv[2]);
_edge += GetEdgeRatio(i.uv[3], i.uv[4]);
fixed4 _withEdgeColor = lerp(_samplerColor, _EdgeColor, _edge);
fixed4 _onlyEdgeColor = lerp(_BackGroundColor, _EdgeColor, _edge);
return lerp(_withEdgeColor, _onlyEdgeColor, _OnlyEdge);
}
ENDCG
}
}
}
测试效果: