再談邊緣檢測【Unity Shader入門精要13.4】
阿新 • • 發佈:2019-01-07
12.3節中的邊緣檢測使用的是Sobel運算元,但是這種直接對顏色資訊進行邊緣檢測的方法會產生很多我們不希望得到的邊緣線,
本節學習如何在深度和法線紋理上進行邊緣檢測,這些影象不會受紋理和光照影響,而僅僅儲存了當前渲染物體的模型資訊。
Roberts運算元的卷積核
-1 | 0 |
0 | 1 |
0 | -1 |
1 |
0 |
本質上是計算左上角和右下角的差值,乘以右上角和左下角的差值,作為評估邊緣的依據,。
取對角方向的深度和法線值,比較他們之間的差值,如果超過某個閥值(可自由控制閥值),就認為他們之間存在一條邊
//13.4 再談邊緣檢測 // Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)' Shader "Unlit/Chapter13-EdgeDeterNormalAndDepth" { 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)//XY分量分別對應了法線和深度的檢測靈敏度,ZW分量擇沒有實際作用 } 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; }; /*定義了一個維度為5的紋理座標陣列,這個陣列的第一個座標儲存了螢幕顏色影象的取樣紋理, 我們對深度紋理的取樣座標進行了平臺化差異處理,在必要情況下對它的豎座標進行翻轉, 陣列中剩餘的4個座標則儲存了使用Roberts運算元需要取樣的紋理座標。 _SampleDistance 用來控制取樣距離。*/ 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 /* 【-1.1】3-----(0.1)------【1.1】1 | | | (-1.0)------(0.0)------(0.1) | | | 【-1.-1】2---(0.-1)-----【1.-1】4 */ /* 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; } /*首先用四個紋理座標對深度進行取樣*/ /*再呼叫CheckSame函式來分別計算對角線上兩個紋理值的差值。 CheckSame的返回值要麼是0要麼是1,返回0時表明這兩個點之間存在一條邊界,反之則返回1。 */ half CheckSame(half4 center, half4 sample) { /*CheckSame首先對輸入引數進行處理,得到兩個取樣點的法線和深度值 值得注意的是,這裡並沒有解碼得到真正的法線值,而是直接使用了XY分量,這是因為我們只需要比較兩個取樣值之間的差異度, 而並不需要知道他們的真正法線*/ 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; /*把差異值的每個分量相加再和一個閥值比較, 如果他們的和小於閥值,則返回1,說明差異不明顯,不存一條邊界,否則返回0*/ 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 }
//13.4 再談邊緣檢測 using System.Collections; using System.Collections.Generic; using UnityEngine; public class EdgeDetectNormalsAndDepth : PostEffectsBase { public Shader edgeDetectShader; private Material edgeDetectMaterial; private Material material { get { edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial); return edgeDetectMaterial; } } [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;//同上 //由於本例需要獲取攝像機的深度+法線紋理,在指令碼的OnEnable函式中設定攝像機的相應狀態 private void OnEnable() { GetComponent<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals; } //實現OnRender函式,把各個引數傳遞給材質 [ImageEffectOpaque] /*在預設情況下,OnRender函式會在所有的不透明的Pass * (即渲染佇列小於等於2500的PSS,內建的back,Geometry,Alpha渲染佇列均在此範圍內) * 執行完畢後立即呼叫該函式而不對透明物體(渲染佇列為Transform)產生影響, * 此時可以在OnRenderImage函式前新增ImageEffectOpaque屬性來實現*/ private void OnRenderImage(RenderTexture source, RenderTexture destination) { if (material !=null) { material.SetFloat("_EdgeOnly", edgesOnly); material.SetColor("_EdgeColor", edgeColor); material.SetColor("_BackgroundColor", backgroundColor); material.SetFloat("_SampleDistance", sampleDistance); material.SetVector("_Sensitivity", new Vector4(sensitivityNormals, sensitivityDepth, 0.0f, 0.0f)); Graphics.Blit(source, destination, material); }else { Graphics.Blit(source, destination); } } }
本節實現的描邊效果是基於整個螢幕空間進行,例如當玩家選中某個物體後,我們想要在該物體周圍新增一層描邊效果,這時可以使用Unity提供的Graphics.DrawMesh或者Graphics.DrawMeshNow 函式把需要描邊的物體再渲染一遍(在所有不透明物體渲染完畢以後),然後再使用本節提到的邊緣檢測演算法計算深度或法線紋理中每個畫素的梯度值,判斷他們是否小於某個閥值如果是,就在shader中使用Clip()函式將該畫素剔除,從而顯示出原來的顏色
。。。。稍微試了一下。。沒試出來,擼第二遍的時候在來補上吧
擴充套件閱讀;
我們可以在Unity中建立任何需要的快取紋理,這可以通過使用Unity的著色器轉換(ShaderReplacement)功能(即呼叫Camera。RenderWithShader(shader,replacementTag)函式)把整個場景再渲染一遍來得到,而在很多時候,這實際也是Unity建立深度和法線紋理時使用的方法
P287