【Unity】【シェーダ】ソーベルフィルタでエッジを抽出する

f:id:halya_11:20180513160733p:plain

テクスチャやレンダリング結果などからエッジを抽出するにはいくつかの方法があります。
ここではソーベルフィルタというフィルタをかけることで抽出する方法を紹介します。

ソーベルフィルタとは?

まず前提としてこの方法では、色の差が大きい部分をエッジとみなします。

f:id:halya_11:20180513154831p:plain

この前提を以ってソーベルフィルタの考え方を説明します。
ソーベルフィルタではまず各ピクセルに対して下記のコンボリューション行列を適用します。

f:id:halya_11:20180513154123p:plain

この結果、たとえば垂直方向について、当該ピクセルの上にあるピクセルと下にあるピクセルの色の差が大きければ大きいほど、行列適用結果の値が大きな負の値、あるいは大きな正の値となります。

要は行列適用結果の値の絶対値が大きいほど、そこはエッジであるだろうとみなせるということです。

そして垂直方向と水平方向の行列適用結果をうまく組み合わせればエッジが抽出できます。
(この方法は次節で説明します)

シェーダ

ソーベルフィルタを使ってテクスチャから輪郭を抽出してみます。

Shader "Outline_Sobel"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _OutlineThick ("Outline Thick", float) = 1.0
        _OutlineThreshold ("Outline Threshold", float) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
            
           #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _MainTex_TexelSize;
            float _OutlineThick;
            float _OutlineThreshold;
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                // 近隣のテクスチャ色をサンプリング
                float diffU = _MainTex_TexelSize.x * _OutlineThick;
                float diffV = _MainTex_TexelSize.y * _OutlineThick;
                half3 col00 = tex2D(_MainTex, i.uv + half2(-diffU, -diffV));
                half3 col01 = tex2D(_MainTex, i.uv + half2(-diffU, 0.0));
                half3 col02 = tex2D(_MainTex, i.uv + half2(-diffU, diffV));
                half3 col10 = tex2D(_MainTex, i.uv + half2(0.0, -diffV));
                half3 col12 = tex2D(_MainTex, i.uv + half2(0.0, diffV));
                half3 col20 = tex2D(_MainTex, i.uv + half2(diffU, -diffV));
                half3 col21 = tex2D(_MainTex, i.uv + half2(diffU, 0.0));
                half3 col22 = tex2D(_MainTex, i.uv + half2(diffU, diffV));

                // 水平方向のコンボリューション行列適用後の色を求める
                half3 horizontalColor = 0;
                horizontalColor += col00 * -1.0;
                horizontalColor += col01 * -1.0;
                horizontalColor += col02 * -1.0;
                horizontalColor += col20;
                horizontalColor += col21;
                horizontalColor += col22;
                
                // 垂直方向のコンボリューション行列適用後の色を求める
                half3 verticalColor = 0;
                verticalColor += col00;
                verticalColor += col10;
                verticalColor += col20;
                verticalColor += col02 * -1.0;
                verticalColor += col12 * -1.0;
                verticalColor += col22 * -1.0;
                
                // この値が大きく正の方向を表す部分がアウトライン
                // ※1
                half3 outlineValue = horizontalColor * horizontalColor + verticalColor * verticalColor;
                return half4(outlineValue - _OutlineThreshold, 1);
            }
            ENDCG
        }
    }
}

大枠の説明はコメントに書きました。

元のテクスチャ色に前節で説明した二つの行列を適用したものがそれぞれhorizontalColorとverticalColorとなります。
前節で説明した通り、この値の絶対値が大きい部分をアウトラインとします。

コメントの※1でこれらをそれぞれ二乗してから足し合わせることで、符号を考慮することなく値が大きい部分をアウトラインとみなせるようにしています。
(本当は二乗して足した後に平方根を求めたほうがいいのかもしれませんが、今回の用途では不要なのでやっていません)

結果

今回はこのテクスチャの輪郭を抽出してみます。

f:id:halya_11:20180513160611p:plain

シェーダを適用すると、

f:id:halya_11:20180513160733p:plain

輪郭が抽出されました。

マテリアルのパラメータをいじると輪郭を出やすくしたり太さを変えられたりします。

余談

GRAVITY DAZEの輪郭線に使われてるようです。

西川善司の3Dゲームファンのための「GRAVITY DAZE」グラフィックス講座(後編) - GAME Watch

参考

www.keyence.co.jp

関連

light11.hatenadiary.com