這個屏幕特效我挺喜歡的,可以用來描邊,也可以用來提取邊緣,獲得很多很棒的效果。
什麼樣的點可能是邊緣部分所在的點呢,如果該點附近的法線值或者是深度值相差的很多,那麼這個點就可以被認爲是一條邊,具體的值可以用參數來控制。法線以及深度的差值我們通過卷積來獲取,使用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的值要麼是邊,要麼是背景,最後在對這兩個值之間進行差值,換句話說,在描好邊的情況之下,對原圖和背景之間進行差值,就像我一開始放的那兩張圖一樣,分別是原圖,描邊的背景圖,描邊圖。