【Unity】【シェーダ】SNNフィルタのポストエフェクトで絵画風に加工する

UnityでSNNフィルタのポストエフェクトを掛けてレンダリング結果を絵画風にする方法をまとめました。

Unity2019.2.6

SNNフィルタとは?

SNNフィルタとはSymmetric Nearest Neighborフィルタの略で、
もともとは画像のノイズを目立たなくするために考えられたフィルタのようです。

subsurfwiki.org

これを強めに適用することで結果を絵画風にすることができます。
本記事ではこのSNNフィルタのアルゴリズムを解説し、実際にシェーダを書いてみます。

アルゴリズムの解説

いま、以下のように中央のピクセルとその周辺のピクセルを考えます。

f:id:halya_11:20191016233320p:plain

これからSNNフィルタにより中央のピクセルに適用する色を求めます。
まず一番左上のピクセルと、それと対角線上にあるピクセルに注目します。

f:id:halya_11:20191016233441p:plain

これら二つのピクセルを比べて、より中央の色に近い方を「採用」とします。

f:id:halya_11:20191016233707p:plain

同様の考え方で対角線にあるピクセル同士を比較していき、「採用」かどうかを判定していきます。

f:id:halya_11:20191016233930p:plain

全部判定した結果、こんな感じになったとします。

f:id:halya_11:20191016234246p:plain

あとは「採用」となった色の平均値を求め、それを中央ピクセルの色とします。

f:id:halya_11:20191016234509p:plain

アルゴリズムは以上です。

シェーダを書く

それではSNNフィルタのシェーダを書いてみます。

Shader "SymmetricNearestNeighbor"
{
    Properties{
        _MainTex("Texture", 2D) = "white" {}
        _SampleCountFactor("Sample Count Factor", float) = 10
        _CellScale("Cell Scale", float) = 1
    }
    SubShader
    {
        Cull Off
        ZTest Always
        ZWrite Off

        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;
            float2 _MainTex_TexelSize;
            int _SampleCountFactor;
            float _CellScale;

            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                float4 color = float4(0, 0, 0, 1);

                // 解像度が違っても同じ見え方にする
                float2 cellSize = _CellScale / 1000;
                cellSize.y *= _MainTex_TexelSize.y / _MainTex_TexelSize.x;
                half3 color0 = tex2D(_MainTex, i.uv).rgb;

                int count = 0;
                float2 offset = -_SampleCountFactor * cellSize;
                for (int x = 0; x <= _SampleCountFactor; ++x)
                {
                    offset.y = -_SampleCountFactor * cellSize.y;

                    for (int y = -_SampleCountFactor; y <= _SampleCountFactor; ++y)
                    {
                        if (x == 0 && y <= 0) {
                            continue;
                        }

                        half3 color1 = tex2D(_MainTex, i.uv + offset).rgb;
                        half3 color2 = tex2D(_MainTex, i.uv - offset).rgb;
                        float3 diff1 = color1 - color0.rgb;
                        float3 diff2 = color2 - color0.rgb;
                        // 中心の色に近いほうを採用
                        color.rgb += dot(diff1, diff1) < dot(diff2, diff2) ? color1 : color2;
                        count++;

                        // cellSize分だけずらしていく
                        offset.y += cellSize.y;
                    }
                    offset.x += cellSize.x;
                }

                color.rgb /= count;
                return color;
            }
            ENDCG
        }
    }
}

コメントにも書きましたが、前節で説明したように「周辺3ピクセル」のように
範囲を決めてしまうと、解像度によって大きく結果が変わってしまいます。
これはポストエフェクトとして好ましくないので、範囲はUV座標ベースで決定しています。

また、中央の色に「近い」かどうかを明度で判定していますが、ここは輝度の方がいいのかもしれません。
あとガンマ空間なのかリニア空間なのかによっても結果が変わってきそうです。
今回はこのあたりはざっくり、シンプルな実装にしています。

ポストエフェクトとして適用する

次にこのシェーダをポストエフェクトとして適用します。
この方法に関しては以下の記事にまとめていますので、必要に応じて参照してください。

light11.hatenadiary.com

結果

さてそれではこのポストエフェクトを反映してみます。
まず適用前のレンダリング結果は以下の通りです。

f:id:halya_11:20191003174020p:plain
適用前

これにSNNフィルタを適用すると以下のようなレンダリング結果となります。

f:id:halya_11:20191003173958p:plain Sample Count Factor = 3
Cell Size = 3

絵画風になったことが確認できました。

関連

light11.hatenadiary.com

参考

subsurfwiki.org