關於屏幕后效果的控制類詳細見之前寫的另一篇博客:
https://www.cnblogs.com/koshio0219/p/11131619.html
這篇主要是基於之前的控制類,實現另一種常見的屏幕后效果——邊緣檢測。
概念和原理部分:
首先,我們需要知道在圖形學中經常處理像素的一種操作——卷積。
卷積操作的實質在於,對於圖像中的每個像素與其周圍的像素進行的重新融合計算行為,以得到不同的像素處理效果,例如銳化圖像,模糊圖像,檢測邊緣等。
卷積操作通過不同的像素融合算法能得到各不相同的效果,這主要依賴於卷積核。
可以把卷積核看作是一個n行n列方陣,原始像素則位於方陣的中心。
邊緣檢測的卷積核也叫邊緣檢測算子,以Sobel算子為例,形如:
需要特別注意的是,這里的Sobel算子是基於坐標軸以屏幕左上為原點,右下分別為+x,+y方向的,而不是類似於uv坐標軸的以屏幕左下為原點,右上分別為+x,+y方向的。這一點需要特別注意,不然后面的程序很容易寫錯。
其中Gx和Gy分別是縱向和橫向兩個方向的邊緣線檢測,你可以通過去掉矩陣中的零元素來想象,因為零元素不會對像素產生任何影響。也就是說,Gx是為了計算橫向的梯度值,Gy為了計算縱向的梯度值。
橫向的梯度值檢測出來的是縱向的邊緣線,縱向的梯度值檢測出來的是橫向的邊緣線。這一點非常容易混淆,需要特別注意。
利用邊緣檢測算子除了融合像素外,主要是為了計算出像素的梯度值。
一個像素和周圍的像素之間梯度值很高,意味着它與周圍的像素差異很大,我們可以想象這個像素和周圍的像素格格不入,存在一個無法逾越的階梯;那么就可以這么認為,這個像素可以作為一條邊界中的值。
對圖像中的每個像素都如此處理,最終就能得到圖像的邊緣。這也就是邊緣檢測的實質內容。
計算方法:
1.得到每個像素周圍的8個像素的坐標位置以便與Sobel算子進行計算,類似於:(排列方式應該與Sobel算子的坐標軸保持一致)
uv[0] | uv[1] | uv[2] |
uv[3] | uv[4](原始像素點) | uv[5] |
uv[6] | uv[7] | uv[8] |
但因為uv坐標的原點在左下角,因此在計算uv[0]-uv[8]時,若依據uv[4]為原始像素點,則它們的偏移可以表示為如下情況:
(-1,1)uv[0] | (0,1)uv[1] | (1,1)uv[2] |
(-1,0)uv[3] | (0,0)uv[4] | (1,0)uv[5] |
(-1,-1)uv[6] | (0,-1)uv[7] | (1,-1)uv[8] |
2.通過偏移值可以很快計算出目標像素的周圍像素位置坐標信息,隨后與Gx和Gy對應元素分別進行橫向和縱向的梯度值計算,也就是分別進行縱向和橫向的邊緣檢測:
具體計算方法為:先對卷積核進行180度翻轉,得到新的矩陣,隨后各項對應元素相乘並相加,注意,不要與矩陣的乘法計算混淆。
但因為Sobel算子是否執行翻轉操作對計算結果沒有任何影響,故對於Sobel算子來說,翻轉操作可以省略。
Gx和Gy計算結束后再將它們開平方和;但往往為了簡化GPU的計算量,可以直接取各自的絕對值再相加,得到最終的梯度值G。
3.計算出梯度值后對原始的采樣結果進行關於G的插值操作以得到最終的圖像。
程序實現:
首先是參數調控的腳本:
1 using UnityEngine; 2 3 public class EdgeDetectionCtrl : ScreenEffectBase 4 { 5 private const string _EdgeOnly = "_EdgeOnly"; 6 private const string _EdgeColor = "_EdgeColor"; 7 private const string _BackgroundColor = "_BackgroundColor"; 8 9 [Range(0,1)] 10 public float edgeOnly = 0.0f; 11 12 public Color edgeColor = Color.black; 13 14 public Color backgroundColor = Color.white; 15 16 private void OnRenderImage(RenderTexture source, RenderTexture destination) 17 { 18 if (Material!=null) 19 { 20 Material.SetFloat(_EdgeOnly, edgeOnly); 21 Material.SetColor(_EdgeColor, edgeColor); 22 Material.SetColor(_BackgroundColor, backgroundColor); 23 Graphics.Blit(source, destination, Material); 24 } 25 else 26 Graphics.Blit(source, destination); 27 } 28 }
同樣是繼承自ScreenEffectBase基類,三個參數的意義分別如下:
edgeOnly(shader中:_EdgeOnly):邊緣線的疊加程度,0表示完全疊加,1表示只顯示邊緣線,不顯示原圖
edgeColor(shader中:_EdgeColor):邊緣線的顏色
backgroundColor(shader中:_BackgroundColor):背景顏色,當只顯示邊緣線時,可以很清晰看出
基類腳本見:
https://www.cnblogs.com/koshio0219/p/11131619.html
下面是Shader腳本:
1 Shader "MyUnlit/EdgeDetection" 2 { 3 Properties 4 { 5 _MainTex ("Texture", 2D) = "white" {} 6 } 7 SubShader 8 { 9 Tags { "RenderType"="Opaque" } 10 11 Pass 12 { 13 ZTest always 14 Cull off 15 ZWrite off 16 17 CGPROGRAM 18 #pragma vertex vert 19 #pragma fragment frag 20 21 #pragma multi_compile_fog 22 23 #include "UnityCG.cginc" 24 25 struct appdata 26 { 27 float4 vertex : POSITION; 28 float2 uv : TEXCOORD0; 29 }; 30 31 struct v2f 32 { 33 half2 uv[9] : TEXCOORD0; 34 UNITY_FOG_COORDS(1) 35 float4 pos : SV_POSITION; 36 }; 37 38 sampler2D _MainTex; 39 //紋理映射到[0,1]之后的大小,用於計算相鄰區域的紋理坐標 40 half4 _MainTex_TexelSize; 41 //定義控制腳本中對應的參數 42 fixed _EdgeOnly; 43 fixed4 _EdgeColor; 44 fixed4 _BackgroundColor; 45 46 v2f vert (appdata v) 47 { 48 v2f o; 49 o.pos = UnityObjectToClipPos(v.vertex); 50 51 half2 uv = v.uv; 52 half2 size = _MainTex_TexelSize; 53 //計算周圍像素的紋理坐標位置,其中4為原始點,右側乘積因子為偏移的像素單位,坐標軸為左下角原點,右上為+x,+y方向,與uv的坐標軸匹配 54 o.uv[0] = uv + size * half2(-1, 1); 55 o.uv[1] = uv + size * half2(0, 1); 56 o.uv[2] = uv + size * half2(1, 1); 57 o.uv[3] = uv + size * half2(-1, 0); 58 o.uv[4] = uv + size * half2(0, 0); 59 o.uv[5] = uv + size * half2(1, 0); 60 o.uv[6] = uv + size * half2(-1, -1); 61 o.uv[7] = uv + size * half2(0, -1); 62 o.uv[8] = uv + size * half2(1, -1); 63 64 UNITY_TRANSFER_FOG(o,o.pos); 65 return o; 66 } 67 //計算對應像素的最低灰度值並返回 68 fixed minGrayCompute(v2f i,int idx) 69 { 70 return Luminance(tex2D(_MainTex, i.uv[idx])); 71 } 72 //利用Sobel算子計算最終梯度值 73 half sobel(v2f i) 74 { 75 const half Gx[9] = { 76 - 1,0,1, 77 - 2,0,2, 78 - 1,0,1 79 }; 80 const half Gy[9] = { 81 -1,-2,-1, 82 0, 0, 0, 83 1, 2, 1 84 }; 85 //分別計算橫向和縱向的梯度值,方法為各項對應元素相乘並相加 86 half graX = 0; 87 half graY = 0; 88 89 for (int it = 0; it < 9; it++) 90 { 91 graX += Gx[it] * minGrayCompute(i, it); 92 graY += Gy[it] * minGrayCompute(i, it); 93 } 94 //絕對值相加近似模擬最終梯度值 95 return abs(graX) + abs(graY); 96 } 97 98 fixed4 frag (v2f i) : SV_Target 99 { 100 half gra = sobel(i); 101 fixed4 col = tex2D(_MainTex, i.uv[4]); 102 //利用得到的梯度值進行插值操作,其中梯度值越大,越接近邊緣的顏色 103 fixed4 withEdgeColor = lerp( col, _EdgeColor, gra); 104 fixed4 onlyEdgeColor = lerp( _BackgroundColor, _EdgeColor, gra); 105 fixed4 color = lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly); 106 107 UNITY_APPLY_FOG(i.fogCoord, color); 108 return color; 109 } 110 ENDCG 111 } 112 } 113 }
效果如下: