Unity Shader 屏幕后處理-基于法線和深度紋理的邊緣檢測

Unity Shader系列文章:Unity Shader目錄-初級篇

Unity Shader系列文章:Unity Shader目錄-中級篇

效果:
左圖:在原圖上描邊的效果卒落。右圖:只顯示描邊的效果
左圖:原效果趟薄。右圖:直接對顏色圖像進行邊緣檢測的結(jié)果
原理:

使用Roberts 算子進行邊緣檢測,Roberts 算子的本質(zhì)就是計算左上角和右下角的差值,乘以右上角和左下角的差值资柔,作為評估邊緣的依據(jù)。按這樣的方式,取對角方向的深度或法線值壶栋,比較它們之間的差值结胀,如果超過某個闕值(可由參數(shù)控制)赞咙,就認為它們之間存在一條邊。


Roberts 算子

ScreenPostEffectsBase基類代碼:

using UnityEngine;

/// <summary>
/// 屏幕后處理效果基類
/// </summary>
[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
public class ScreenPostEffectsBase : MonoBehaviour
{
    public Shader Shader;
    public Material Material
    {
        get
        {
            return CheckAndCreateMaterial();
        }
    }
    private Material _material;

    protected void Start()
    {
        CheckResources();
    }

    /// <summary>
    /// 檢查資源
    /// </summary>
    protected void CheckResources()
    {
        if (!CheckSupport())
        {
            NotSupported();
        }
    }

    /// <summary>
    /// 檢查支持
    /// </summary>
    /// <returns></returns>
    protected bool CheckSupport()
    {
        bool isSupported = SystemInfo.supportsImageEffects;
        return isSupported;
    }

    /// <summary>
    /// 不支持
    /// </summary>
    protected void NotSupported()
    {
        enabled = false;
    }

    /// <summary>
    /// 檢查和創(chuàng)建Material
    /// </summary>
    /// <returns></returns>
    protected Material CheckAndCreateMaterial()
    {
        if (!Shader || !Shader.isSupported)
        {
            return null;
        }

        if (_material && _material.shader == Shader)
        {
            return _material;
        }

        _material = new Material(Shader);
        _material.hideFlags = HideFlags.DontSave;
        return _material;
    }
}

ScreenEdgeDetectNormalsAndDepth派生類代碼:

using UnityEngine;

/// <summary>
// 屏幕后處理-基于法線和深度紋理的邊緣檢測
/// </summary>
public class ScreenEdgeDetectNormalsAndDepth : ScreenPostEffectsBase
{
    [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; // 法線敏感度意蛀,影響當鄰域的法線值相差多少時耸别,會被認為存在一條邊界

    private void OnEnable()
    {
        // 要獲取攝像機的深度+法線紋理
        GetComponent<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals;
    }

    // ImageEffectOpaque含義:
    // 在默認情況下 OnRenderlmage 函數(shù)會 所有的不透明和透明的 Pass 執(zhí)行完畢后被調(diào)用 ,
    // 前添加 ImageEffectOpaque 屬性后县钥,可以在不透明 Pass (即渲染隊列小于等于 500 Pass, 內(nèi)置的 Background Geometry AlphaTest 渲染隊列均在此范圍內(nèi))
    // 執(zhí)行完畢后立即調(diào)用該函數(shù)秀姐,而不對透明物體(渲染隊列為 Transparent Pass 產(chǎn)生影響,
    [ImageEffectOpaque]
    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        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(src, dest, Material);
        }
        else
        {
            Graphics.Blit(src, dest);
        }
    }
}

shander代碼:

// 屏幕后處理-基于法線和深度紋理的邊緣檢測
Shader "Custom/EdgeDetectNormalsAndDepth"
{
    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;
            
            // 下面要對這個紋理坐標進行平臺差異化處理若贮,因為OpenGL省有,(0, 0)點對應(yīng)了屏幕的左下角,DirectX中對應(yīng)了屏幕左上角
            // 大多數(shù)時候這都無關(guān)緊要谴麦,除了渲染到渲染紋理時蠢沿。在此情況下,Unity 渲染到 Direct3D 上的紋理時匾效,會自動在內(nèi)部翻轉(zhuǎn)渲染舷蟀,以便平臺之間的慣例匹配。
            // 如果我們做的屏幕后期特效簡單(一次處理一個紋理)面哼,這無關(guān)緊要野宜,因為 Graphics.Blit 方法會自動進行處理。
            // 然而魔策,如果在屏幕后期特效中同時處理一個以上的 RenderTexture匈子,它們很可能會在不同的垂直方向出現(xiàn)(僅在類似 Direct3D 的平臺上,并且僅在使用抗鋸齒選項時)
            
            // UNITY_UV_STARTS_AT_TOP闯袒,紋理的坐標系原點在紋理頂部的平臺上值:Direct3D類似平臺是1虎敦;OpenGL類似平臺是0
            #if UNITY_UV_STARTS_AT_TOP
                // 在Direct3D平臺下,如果我們開啟了抗鋸齒政敢,則xxx_TexelSize.y 會變成負值其徙,好讓我們能夠正確的進行采樣。
                // 所以if (_MainTex_TexelSize.y < 0)的作用就是判斷我們當前是否開啟了抗鋸齒堕仔。
                if (_MainTex_TexelSize.y < 0)
                    uv.y = 1 - uv.y;
            #endif
            
            // 存儲了使用 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;
        }
        
        // 計算對角線上兩個紋理值的差值擂橘,返回0表明這兩點之間存在一條邊界,反之則返回1
        half CheckSame(half4 center, half4 sample)
        {
            // 獲取兩個采樣點的法線和深度值
            // 這里并沒有解碼得到真正的法線值 而是直接使用了 xy 分量摩骨,是因為只需要比較兩個采樣值之間的差異度通贞,
            // 而并不需要知道它們真正的法線值朗若。
            half2 centerNormal = center.xy;
            float centerDepth = DecodeFloatRG(center.zw);
            half2 sampleNormal = sample.xy;
            float sampleDepth = DecodeFloatRG(sample.zw);
            
            // 把兩個采樣點的對應(yīng)值相減并取絕對值,再乘以靈敏度參數(shù)昌罩,把差異值的每個分量相加再和一個闕值比較哭懈,
            // 如果它們的和小于閾值, 則返回1, 說明差異不明顯茎用,不存在一條邊界遣总;否則返回0,說明存在一條邊界轨功。
            half2 diffNormal = abs(centerNormal - sampleNormal) * _Sensitivity.x;
            int isSameNormal = (diffNormal.x + diffNormal.y) < 0.1;
            float diffDepth = abs(centerDepth - sampleDepth) * _Sensitivity.y;
            int isSameDepth = diffDepth < 0.1 * centerDepth;
            
            // 最后旭斥, 把法線和深度的檢查結(jié)果相乘,作為組合后的返回值古涧。
            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
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末垂券,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子羡滑,更是在濱河造成了極大的恐慌菇爪,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件柒昏,死亡現(xiàn)場離奇詭異凳宙,居然都是意外死亡,警方通過查閱死者的電腦和手機职祷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進店門氏涩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人堪旧,你說我怎么就攤上這事削葱。” “怎么了淳梦?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵析砸,是天一觀的道長。 經(jīng)常有香客問我爆袍,道長首繁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任陨囊,我火速辦了婚禮弦疮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蜘醋。我一直安慰自己胁塞,他們只是感情好,可當我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著啸罢,像睡著了一般编检。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上扰才,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天允懂,我揣著相機與錄音,去河邊找鬼衩匣。 笑死蕾总,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的琅捏。 我是一名探鬼主播生百,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼午绳!你這毒婦竟也來了置侍?” 一聲冷哼從身側(cè)響起映之,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤拦焚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后杠输,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赎败,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年蠢甲,在試婚紗的時候發(fā)現(xiàn)自己被綠了僵刮。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡鹦牛,死狀恐怖搞糕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情曼追,我是刑警寧澤窍仰,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站礼殊,受9級特大地震影響驹吮,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜晶伦,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一碟狞、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧婚陪,春花似錦族沃、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽智润。三九已至,卻和暖如春未辆,著一層夾襖步出監(jiān)牢的瞬間窟绷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工咐柜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留兼蜈,地道東北人。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓拙友,卻偏偏與公主長得像为狸,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子遗契,可洞房花燭夜當晚...
    茶點故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內(nèi)容