Keep Learning and Creative

Unity, 技術解説

油絵っぽくするKuwahara FilterをUnityで実装する

下の例のように、油絵っぽい見た目にするポストエフェクト用のシェーダーをUnityで実装します。

元ネタは、こちらの記事で、桑原さんという方が考案した画像フィルターだそうです。この記事では、pythonを使って実装されているので、Unityのshaderで実装したものを紹介します。

内容の詳細は上述の記事に書いてありますので、ここでは、実際のコードと簡単な解説コメントを載せておきます。(記事を読んでいることを前提に解説コメントは書いてあります)

Shader "Filter/KuwaharaFilter"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        // 大きな値になる程、油絵感が増す
        _Size ("Filter Size", Range(1, 25)) = 5
    }
    SubShader
    {
        // No culling or depth
        Cull Off ZWrite Off ZTest Always

        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;
            };

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

            sampler2D _MainTex;
            uniform float4 _MainTex_TexelSize;
            fixed _Size;

            fixed4 frag (v2f i) : SV_Target
            {
                // uvのテクセルの座標を取得
                float uTexel = _MainTex_TexelSize.x;
                float vTexel = _MainTex_TexelSize.y;
                
        // 4エリアの正方形の1辺の長さを求める
                int areaSize = floor(_Size / 2);

        // 1辺の長さが0なら何もしない
                if (areaSize == 0) {
                    return tex2D(_MainTex, i.uv);
                }

                // 各エリアの全ピクセルのrgb値から平均と分散を求める
                // a : 左上
                fixed3 aavg = fixed3(0,0,0);
                fixed3 avar = fixed3(0,0,0);
                for (int av = 1; av <= areaSize; av++) {
                    for (int au = 1; au <= areaSize; au++) {
                        fixed3 pick = tex2D(_MainTex, i.uv + float2(-au*uTexel, av*vTexel));
                        aavg += pick;
                        avar += pick * pick;
                    }
                }
                aavg /= areaSize * areaSize;
                avar = avar / (areaSize * areaSize) - aavg * aavg;

                // b : 右上
                fixed3 bavg = fixed3(0,0,0);
                fixed3 bvar = fixed3(0,0,0);
                for (int bv = 1; bv <= areaSize; bv++) {
                    for (int bu = 1; bu <= areaSize; bu++) {
                        fixed3 pick = tex2D(_MainTex, i.uv + float2(bu*uTexel, bv*vTexel));
                        bavg += pick;
                        bvar += pick * pick;
                    }
                }
                bavg /= areaSize * areaSize;
                bvar = bvar / (areaSize * areaSize) - bavg * bavg;

                // c : 左下
                fixed3 cavg = fixed3(0,0,0);
                fixed3 cvar = fixed3(0,0,0);
                for (int cv = 1; cv <= areaSize; cv++) {
                    for (int cu = 1; cu <= areaSize; cu++) {
                        fixed3 pick = tex2D(_MainTex, i.uv + float2(-cu*uTexel, -cv*vTexel));
                        cavg += pick;
                        cvar += pick * pick;
                    }
                }
                cavg /= areaSize * areaSize;
                cvar = cvar / (areaSize * areaSize) - cavg * cavg;

                // d : 右下
                fixed3 davg = fixed3(0,0,0);
                fixed3 dvar = fixed3(0,0,0);
                for (int dv = 1; dv <= areaSize; dv++) {
                    for (int du = 1; du <= areaSize; du++) {
                        fixed3 pick = tex2D(_MainTex, i.uv + float2(du*uTexel, -dv*vTexel));
                        davg += pick;
                        dvar += pick * pick;
                    }
                }
                davg /= areaSize * areaSize;
                dvar = dvar / (areaSize * areaSize) - davg * davg;

                // 各rgb値について、4エリアで最も分散が小さいエリアの平均値を取得
                // r
                fixed r = lerp(aavg.r, bavg.r, step(bvar.r, avar.r));
                r = lerp(r, cavg.r, step(cvar.r, min(avar.r, bvar.r)));
                r = lerp(r, davg.r, step(dvar.r, min(cvar.r, min(avar.r, bvar.r))));

                // g
                fixed g = lerp(aavg.g, bavg.g, step(bvar.g, avar.g));
                g = lerp(g, cavg.g, step(cvar.g, min(avar.g, bvar.g)));
                g = lerp(g, davg.g, step(dvar.g, min(cvar.g, min(avar.g, bvar.g))));

                // b
                fixed b = lerp(aavg.b, bavg.b, step(bvar.b, avar.b));
                b = lerp(b, cavg.b, step(cvar.b, min(avar.b, bvar.b)));
                b = lerp(b, davg.b, step(dvar.b, min(cvar.b, min(avar.b, bvar.b))));

        // 取得したrgb値を最終的な色としてて出力
                fixed4 col = fixed4(r,g,b,1);
                return col;
            }
            ENDCG
        }
    }
}

fragmentシェーダー内で、Kuwahara Filterの処理が実装されています。アルゴリズムとしてはかなり単純な内容ですが、見た目の変化は大きくいい感じです。

Sizeの値を大きくするほど、油絵感は増しますが、サンプリングするピクセルの個数も増えて処理的には重くなっていくので注意が必要です。

以上、桑原さんのKuwahara Filterの紹介でした!

Leave a Reply