这个屏幕特效我挺喜欢的,可以用来描边,也可以用来提取边缘,获得很多很棒的效果。
什么样的点可能是边缘部分所在的点呢,如果该点附近的法线值或者是深度值相差的很多,那么这个点就可以被认为是一条边,具体的值可以用参数来控制。法线以及深度的差值我们通过卷积来获取,使用2x2的矩阵(-1,0,0,1)检测x方向以及2x2矩阵(0,-1,1,0)检测y方向,这个卷积核叫做Roberts算子。
这个屏幕特效的脚本部分没什么好说的,就是直接给shader的每一个变量进行赋值,我们直接从shader开始讨论起,先贴一个完整代码:
Shader "Unity Shaders Book/Chapter 13/Edge Detection Normals And Depth" {
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)
}
SubShader {
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
fixed _EdgeOnly;
fixed4 _EdgeColor;
fixed4 _BackgroundColor;
float _SampleDistance;
half4 _Sensitivity;
sampler2D _CameraDepthNormalsTexture;
struct v2f {
float4 pos : SV_POSITION;
half2 uv[5]: TEXCOORD0;
};
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
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;
}
half CheckSame(half4 center, half4 sample) {
half2 centerNormal = center.xy;
float centerDepth = DecodeFloatRG(center.zw);
half2 sampleNormal = sample.xy;
float sampleDepth = DecodeFloatRG(sample.zw);
// difference in normals
// do not bother decoding normals - there's no need here
half2 diffNormal = abs(centerNormal - sampleNormal) * _Sensitivity.x;
int isSameNormal = (diffNormal.x + diffNormal.y) < 0.1;
// difference in depth
float diffDepth = abs(centerDepth - sampleDepth) * _Sensitivity.y;
// scale the required threshold by the distance
int isSameDepth = diffDepth < 0.1 * centerDepth;
// return:
// 1 - if normals and depth are similar enough
// 0 - otherwise
return isSameNormal * isSameDepth ? 1.0 : 0.0;
}
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;
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
Pass {
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment fragRobertsCrossDepthAndNormal
ENDCG
}
}
FallBack Off
}
首先我们讨论一下着色器的变量部分,
_Maintex没什么好说的,就是摄像机传来的渲染纹理。
_EdgeOnly用来控制背景和非边缘区域图像混合的比例
_EdgeColor表示边的颜色
_BackgroundColor表示背景颜色
_SampleDistance用来控制进行卷积的采样距离
_Sensitivity的第一项用来控制法线纹理的敏感性,第二项用来控制深度纹理的敏感性,后面两个项没有实际作用
然后就是顶点着色器部分,这个部分的作用就是计算采样坐标,也就是给uv赋值,根据我们的Roberts算子,采样坐标分别为右上左下以及左上右下,下面的代码的作用就是赋值好采样坐标,采样坐标的具体值还会根据_SampleDistance进行偏移。
然后就是具体的判断是不是边缘的函数了,这个函数也是简单粗暴,要么是边,要么不是边缘,也就是只返回0或者1,具体的判断方式为把边缘的像素点和中心的像素点进行对比,只要两者的深度差值或者法线差值有一个不满足条件,那么它就是边缘,具体的判定方法下面代码我感觉也算是经验所得,但是很有效。
写完了判定函数之后,后面就是片元着色器了,往往这个着色器在shader里面是最重要的。Roberts在x或者y方向只要有一个判定它是边缘,那么它就是边缘,最后3行的差值其实有点乱,乍一看不好理解,要耐心的去分析一下。首先我们要了解到一个重点,edge的值要么是0,要么是1,所以withEdgeColor的值要么是边,要么是原来的图像的值,也就是说withEdgeColor是原图像的描边效果,onlyEdgeColor的值要么是边,要么是背景,最后在对这两个值之间进行差值,换句话说,在描好边的情况之下,对原图和背景之间进行差值,就像我一开始放的那两张图一样,分别是原图,描边的背景图,描边图。