天天看點

UnityShader-螢幕後處理之邊緣檢測

一、螢幕後處理原理

我的另外一篇文章有詳細的介紹:

UnityShader-螢幕後處理(調節亮度,飽和度,對比度)

二、卷積與卷積核

卷積核通常是一個四方形網格結構(例如2x2,3x3的方形區域),該方形區域每個方格都有一個權重值。當對圖像中的某個像素進行卷積時,我們會把卷積核的中心位置放在該像素上,翻轉核之後再依次計算核中每個元素和其覆寫的圖形像素值的乘積并求和,得到的結果就是該位置的像素值。

卷積操作指的是使用一個卷積核對圖像中的每個像素進行一系列的操作。

UnityShader-螢幕後處理之邊緣檢測

卷積核.png

三、邊緣檢測原理

(1)怎麼确定形成了一條邊呢?

如果相鄰的像素之間存在差别明顯的顔色、亮度、紋理等屬性,我們就會認為他們之間應該是有一條邊界的,這種相鄰像素之間的內插補點可以用梯度來表示。

(2)常見的的幾種邊緣檢測的算子

UnityShader-螢幕後處理之邊緣檢測

常見的邊緣檢測算子.png  

在本例子中,我們使用Sobel算子。

(3)梯度計算公式

通常包括兩個方向的卷積核,一個是水準方向,一個是豎直方向,我們需要對每個像素進行一個卷積計算,得到兩個方向的梯度值Gx和Gy。整體公式:

UnityShader-螢幕後處理之邊緣檢測

出于性能考慮,用絕對值代替開根号:

UnityShader-螢幕後處理之邊緣檢測

(4)代碼實作

C#代碼:

using UnityEngine;
using System.Collections;

public class EdgeDetection : PostEffectsBase {

    public Shader edgeDetectShader;
    private Material edgeDetectMaterial = null;
    public 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;

    void OnRenderImage (RenderTexture src, RenderTexture dest) {
        if (material != null) {
            material.SetFloat("_EdgeOnly", edgesOnly);
            material.SetColor("_EdgeColor", edgeColor);
            material.SetColor("_BackgroundColor", backgroundColor);

            Graphics.Blit(src, dest, material);
        } else {
            Graphics.Blit(src, dest);
        }
    }
}
           

shader代碼:

Shader "Unity Shaders Book/Chapter 12/Edge Detection" {
    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)
    }
    SubShader {
        Pass {  
            //設定渲染狀态
            ZTest Always Cull Off ZWrite Off
            
            CGPROGRAM
            
            #include "UnityCG.cginc"
            
            #pragma vertex vert  
            #pragma fragment fragSobel
            
            sampler2D _MainTex;
            //_MainTex_TexelSize:xxx_TexelSize是unity為我們提供的通路xxx紋理對應每個紋素大小的變量。
            //例如,一張512x512大小的紋理,該值大約為0.001953的(即1/512)
            uniform half4 _MainTex_TexelSize;
            fixed _EdgeOnly;
            fixed4 _EdgeColor;
            fixed4 _BackgroundColor;
            
            struct v2f {
                float4 pos : SV_POSITION;
                half2 uv[9] : TEXCOORD0;
            };
              
            v2f vert(appdata_img v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                
                half2 uv = v.texcoord;
                //通過_MainTex_TexelSize.xy值,計算需要計算的矩陣的UV值
                o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);
                o.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1);
                o.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1);
                o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0);
                o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0);
                o.uv[5] = uv + _MainTex_TexelSize.xy * half2(1, 0);
                o.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1, 1);
                o.uv[7] = uv + _MainTex_TexelSize.xy * half2(0, 1);
                o.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1);
                         
                return o;
            }
            //計算亮度值
            fixed luminance(fixed4 color) {
                return  0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b; 
            }
            
            //通過Sobe算子算出這個像素點與周圍的梯度值
            half Sobel(v2f i) {
                //Sobe算子矩陣,分為X與Y兩個方向
                const half Gx[9] = {-1,  0,  1,
                                        -2,  0,  2,
                                        -1,  0,  1};
                const half Gy[9] = {-1, -2, -1,
                                        0,  0,  0,
                                        1,  2,  1};     
                
                half texColor;
                half edgeX = 0;
                half edgeY = 0;
                //累加周圍采樣點的梯度值
                for (int it = 0; it < 9; it++) {
                    texColor = luminance(tex2D(_MainTex, i.uv[it]));
                    edgeX += texColor * Gx[it];
                    edgeY += texColor * Gy[it];
                }
                //用絕對值代替開根号
                half edge = 1 - abs(edgeX) - abs(edgeY);
                
                return edge;
            }
            
            fixed4 fragSobel(v2f i) : SV_Target {
                half edge = Sobel(i);
                //根據梯度值,将原顔色與背景顔色、描邊顔色進行插值運算,得到最終的顔色值
                fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge);
                fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);
                return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
            }
            
            ENDCG
        } 
    }
    FallBack Off
}
           

繼續閱讀