-100p

-10p

+10p

+100p

Unityモザイクシェーダーとモザイク処理の解説

Unity uGUI用モザイクシェーダー。モザイクの仕組みも解説。
(Mosaic Shader for Unity uGUI)

関連ページ
モザイクシェーダーなんてググればすぐヒットするんで敢えて自分が公開する必要もないけど、メモ書きのためにも公開。
一応 Variant を使って、粗いモザイクと精度の高いモザイクを選択できるようにした。
粗いモザイクもなかなか味があるのでお好みで。

モザイクシェーダー、コード全文

次の2つの動画がモザイクシェーダーの演出効果。1つ目が粗い精度、2つ目が細かい精度。

2つの処理の違いは、省略するピクセルの色情報を全て破棄するか、省略するピクセルの色の平均色を出力するかの違い。

以下コード全文。uGUI Imageへの反映方法は こちらの記事 参照。
今回の場合GrabPassを使っているので、 こちら の知識も必要になる。
Shader "UI/GrabMosaic" 
{
    Properties 
    {
        [HideInInspector]_MainTex("-",2D)="white"{} 
        [KeywordEnum(one, equalize)] _Pick("ColorPick", Int) = 1
        _MosaicNum ("Mosaic Num", Range(0.0, 50.0)) = 0
    }

    SubShader
    {
        Tags 
        { 
            "Queue" = "Transparent"
            "RenderType"="Transparent"
        }
        Cull Off
        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha

        GrabPass{}

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

            sampler2D _GrabTexture;
            float4 _GrabTexture_TexelSize;
            float _MosaicNum;

            struct appdata
            {
                fixed4 vertex : POSITION;
                fixed4 uv : TEXCOORD0;
            };

            struct v2f
            {
                fixed4 vertex : SV_POSITION;
                fixed2 uv : TEXCOORD0;
            };

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

            
//UV座標を大雑把にして、複数の隣り合うpixelのcolor値を同一色にする

            fixed2 get_mosaic_uv(fixed2 uv, fixed fixMosaicNum)
            {
                fixed2 mosaicUV = floor(uv / fixMosaicNum) * fixMosaicNum;
#ifdef _PICK_ONE
                
//座標の取得を大雑把にした結果画像が少し右下にずれる予定なので、左上に少しずらして修正

                return mosaicUV+ fixMosaicNum / 2;
#elif _PICK_EQUALIZE 
                
//座標の取得を大雑把にした結果画像が少し下にずれる予定なので、上に少しずらして修正

                return fixed2(mosaicUV.x, mosaicUV.y + fixMosaicNum);
#endif
            }

            
//渡したUV座標からGrabした画像のcolorを抜き出す

            fixed4 get_tex_color(fixed2 uv, fixed fixMosaicNum)
            {

#ifdef _PICK_ONE
                
//指定したuv座標のcolorをそのまま返す

                return tex2D(_GrabTexture, uv);
#elif _PICK_EQUALIZE
                
//指定したuv座標から、省略する予定のpixel全体を平均化したcolor値を返す

                fixed4 col;
                int count;
                for(int jx = 0; jx <= floor(_GrabTexture_TexelSize.z * fixMosaicNum); jx++) 
                {
                    for(int jy = 0; jy <= floor(_GrabTexture_TexelSize.w * fixMosaicNum); jy++) 
                    {
                        fixed pickX = jx * _GrabTexture_TexelSize.x;
                        fixed pickY = jy * _GrabTexture_TexelSize.y;
                        col = col + tex2D(_GrabTexture, uv + fixed2(pickX, pickY));
                        count++;
                    }
                }
                return col /count;
#endif
            }

            fixed4 frag(v2f i) : SV_Target
            {
                
//もし_MosaicNumが0より大きいなら1を返し、それ以外は0を返す

                
//shaderではif文が推奨されてないので、if文の代わりとして使う

                fixed isUseMosaic = 1 - step(_MosaicNum, 0);
                
//入力された_MosaicNumの値を、実際にモザイク処理をするのに使う値に修正

                fixed fixMosaicNum = _MosaicNum * 0.001f;

                
//大雑把にしたUV座標。

                fixed2 mosaicUV = get_mosaic_uv(i.uv, fixMosaicNum);

                
//元々のUV座標から摘出したcolor値。ただしisUserMosaicが1なら0が代入される。

                fixed4 originalCol = (1 - isUseMosaic) * tex2D(_GrabTexture, i.uv);
                
//大雑把にしたUV座標から摘出したcolor値。ただしisUserMosaicが0なら0が代入される。

                fixed4 mosaicCol = isUseMosaic * get_tex_color(mosaicUV, fixMosaicNum);

                
//colorの値を大きい方選択して返す

                return max(originalCol, mosaicCol);
            }
            ENDCG
        }
    }
}

プロパティを解説

用意してあるプロパティは2つのみ。
モザイクシェーダーのプロパティ
 ColorPick 
: モザイクの種類。oneで粗いモザイク、equalizeで細かいモザイク。
 Mosaic Num 
: モザイクの強さ。値を上げるほど強くモザイクが掛かる。

モザイクの仕組みについて

モザイクシェーダーは一見複雑なことをやっていそうだけど、理屈はとてもシンプルになっている。
まずよりシンプルなColorPick.oneでの挙動を解説していく。
ColorPick.oneの処理を端的に言うと、UV座標を大雑把にして取得するピクセル色をずらしているだけになる。

シェーダーに渡されるUV座標は、左下が(x=0, y=0)、右上が(x=1, y=1)となっている。
例えば縦も横も6ピクセルの下のような画像をシェーダーに渡したとすると、UV座標は次の通り。
6x6ピクセルは極端に小さい画像なので、UV座標は0.2ずつ移動する。
uv座標の説明
上の画像では、x軸が0から1に進むごとに色が赤色から黄色に変化している。
また黒いグリッド線が表示されているが、これは解説のためであり実際はないものとする。

さて、ここで渡されたUV座標のx軸に対して、次のような計算式を当てて改変したとする。
x = floor(x / 0.4) * 0.4
floorはHLSL言語の関数で、小数点以下切り捨てを意味する。
この計算を実行すると、0.2は0に、0.4は元と同じ0.4になる。
 x = 0.2 

floor(0.2 / 0.4) * 0.4 = floor(0.5) * 0.4 = 0 * 0.4 = 0
 x = 0.4 

floor(0.4 / 0.4) * 0.4 = floor(1) * 0.4 = 1 * 0.4 = 0.4

この調子で全てのx軸に改変を加えると下のような図になる。
uv座標をずらした結果
改変されたx座標を元に、取得するピクセル色が少しずつずれている。
これがColorPick.oneの概ねのモザイク処理になる。
実際はx軸だけでなくy軸に対してもこの改変処理を行っている。
またここでは係数として0.4を使っているか、この値を大きくすればするほどモザイクもより大雑把になる。

ColorPick.equalizeの方は、省略する座標のピクセル色を捨て去るのではなく、平均色を出力させる。
黄色から赤のグラデーションだと今一違いが分かりづらいので、下のようなパステル調のRGB色の画像を用意。
パステル調画像のuv座標

この画像に対し、係数を0.4としてx軸にColorPick.oneの処理を掛けると次のようになる。
パステル調画像のuv座標をずらした結果
一方でColorPick.equalizeを選択すると次の通り。
パステル調画像を混色した結果
パステル赤とパステル緑が混じってややくすんだ黄色に。パステル青とパステル赤が混じってパステル紫に。
そしてパステル緑とパステル青が混じってパステル青緑になっている。
1
1

-100p

-10p

+10p

+100p