参考书籍:《Unity Shader 入门精要》
Unity Shader 学习笔记(20) 卷积、卷积核、边缘检测算子、边缘检测
使用深度和法线纹理实现,是不受图像纹理和光照影响的,仅保存了当前渲染物体的模型信息。这里使用Roberts算子实现,采样的像素法线变化大的(差值大于0.1)或深度变化大的作为边。
using UnityEngine;
///
/// 边缘检测(法线和深度纹理上进行,之前的是对颜色处理)
///
public class EdgeDetectNormalsAndDepth : PostEffectsBase
{
[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()
{
GetComponent().depthTextureMode |= DepthTextureMode.DepthNormals;
}
[ImageEffectOpaque] // 不透明物体渲染完后执行(不影响透明物体),因为默认是在不透明和透明物体都渲染完才调用的。
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (TargetMaterial != null)
{
... // 把上面变量都传入Shader
}
Graphics.Blit(src, dest, TargetMaterial);
}
}
Shdaer:
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) // x作为法线的灵敏度,y作为深度的灵敏度,zw没用
}
SubShader {
CGINCLUDE
...
sampler2D _CameraDepthNormalsTexture;
struct v2f {
float4 pos : SV_POSITION;
half2 uv[5]: TEXCOORD0; // 第一组存屏幕颜色采样纹理,剩下四个Roberts算子采样的纹理坐标
};
v2f vert(appdata_img v) {
v2f 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
// Roberts算子
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 fragRobertsCrossDepthAndNormal(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;
// 两个纹理差值 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);
}
// 采样判断两点间是否存在一条边,存在返回0
half CheckSame(half4 center, half4 sample) {
half2 centerNormal = center.xy;
float centerDepth = DecodeFloatRG(center.zw);
half2 sampleNormal = sample.xy;
float sampleDepth = DecodeFloatRG(sample.zw);
// 法线差值,差值大于0.1判断为变换明显,作为边。
half2 diffNormal = abs(centerNormal - sampleNormal) * _Sensitivity.x;
int isSameNormal = (diffNormal.x + diffNormal.y) < 0.1;
// 深度差值,差值大于0.1判断为变换明显,作为边。
float diffDepth = abs(centerDepth - sampleDepth) * _Sensitivity.y;
int isSameDepth = diffDepth < 0.1;
// 法线 或 深度 其中一个相差大(为0)就是边
return isSameNormal * isSameDepth ? 1.0 : 0.0;
}
ENDCG
Pass {
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment fragRobertsCrossDepthAndNormal
ENDCG
}
}
要单独对某个物体描边,可以使用Graphics.DrawMesh把目标物体再渲染一遍(即渲染了两层),在通过边缘检测算法把小于阈值的剔除掉即可。