【Unity】オブジェクトスペースでモザイクをかける


# 概要
GrabPass を使うとモザイクのオーバーレイをかけるシェーダーを作成できます．スクリーンスペースでモザイクをかける場合は，フラグメントシェーダーで離散化すればできますが，オブジェクトスペースでかけようとしたときに困ったので，その記録です．

▼ 元画像
![Mosaic-Nop.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3825773/bdb05d41-05d4-6fba-efa0-c5b0c6f5bf64.png)

▼ スクリーンスペースのモザイク
![Mosaic-ScreenSpace.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3825773/8e065a5b-85df-3106-5c9e-589b9c355e96.png)

▼ オブジェクトスペースのモザイク
![Mosaic-UvSpace.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3825773/39c544a8-933b-8739-e3d0-ad4efe9622e2.png)



# Screen Space のモザイクシェーダー
スクリーンスペースでモザイクをかける場合は，頂点シェーダーでスクリーン座標を計算しておき，フラグメントシェーダーで離散化します．

```hlsl:Mosaic-Overlay-ScreenSpace.shader
Shader "Test/Mosaic/Mosaic-Overlay-ScreenSpace"
{
    Properties
    {
        _CellPix ("Cell Pixel Size", Int) = 40
    }
    SubShader
    {
        Tags { "Queue"="Transparent+1" "RenderType"="Opaque" }
        Cull Off
        ZWrite Off

        GrabPass { "_TransparentPlus1GrabTex" }

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.5

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float4 grabPos : TEXCOORD1;
                UNITY_VERTEX_INPUT_INSTANCE_ID
                UNITY_VERTEX_OUTPUT_STEREO
            };

            float _CellPix;

            v2f vert (appdata v)
            {
                v2f o;

                // VR 対応
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_TRANSFER_INSTANCE_ID(v, o);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);

                // 通常
                o.pos = UnityObjectToClipPos(v.vertex);

                // クリップ空間座標
                o.grabPos = ComputeGrabScreenPos(o.pos);

                return o;
            }

            UNITY_DECLARE_SCREENSPACE_TEXTURE(_TransparentPlus1GrabTex);

            float4 frag (v2f i) : SV_Target
            {
                // VR 対応
                UNITY_SETUP_INSTANCE_ID(i);
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i);

                // スクリーン座標で離散化 (セルの中央の座標にする)
                float2 grab_uv = i.grabPos.xy / i.grabPos.w;
                float2 cell_uv_size = _CellPix / _ScreenParams.xy;
                float2 digit_grab_uv = (floor(grab_uv / cell_uv_size) + 0.5) * cell_uv_size;

                // マクロを使ってサンプリングしないと環境によって壊れるらしい．
                float4 tex = UNITY_SAMPLE_SCREENSPACE_TEXTURE(_TransparentPlus1GrabTex, digit_grab_uv);

                return tex;
            }
            ENDHLSL
        }
    }
}
```

# Object Space のモザイクシェーダー
オブジェクトスペースと書いていますが，正確には UV 空間でのモザイクです．Quad のメッシュはメッシュ頂点のローカル座標と UV 座標の対応が明白なので，それを利用して離散化を行います．オブジェクトスペースで汎用的に離散化を行う方法はわかりませんでした．
Quad メッシュの4頂点のクリップ空間座標を計算しておき，それを UV 座標で線形補間することにより，オブジェクト空間座標とスクリーン座標とを対応付けます．

![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3825773/bad614b6-7728-db99-ba7e-cc6bd390793a.png)


```hlsl:Mosaic-Overlay-UvSpace.shader
Shader "Test/Mosaic/Mosaic-Overlay-UvSpace"
{
    Properties
    {
        _CellCnt ("Cell Count", Int) = 40
    }
    SubShader
    {
        Tags { "Queue"="Transparent+1" "RenderType"="Opaque" }
        Cull Off
        ZWrite Off

        GrabPass { "_TransparentPlus1GrabTex" }

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.5

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float4 grabUvPos00 : TEXCOORD1;
                float4 grabUvPos10 : TEXCOORD2;
                float4 grabUvPos01 : TEXCOORD3;
                float4 grabUvPos11 : TEXCOORD4;
                UNITY_VERTEX_INPUT_INSTANCE_ID
                UNITY_VERTEX_OUTPUT_STEREO
            };

            float _CellCnt;

            v2f vert (appdata v)
            {
                v2f o;

                // VR 対応
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_TRANSFER_INSTANCE_ID(v, o);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);

                // 通常
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord;
                
                // Quad の 4 頂点のクリップ空間座標を求める．
                o.grabUvPos00 = ComputeGrabScreenPos(UnityObjectToClipPos(float4(-0.5,-0.5, 0, 1)));
                o.grabUvPos10 = ComputeGrabScreenPos(UnityObjectToClipPos(float4( 0.5,-0.5, 0, 1)));
                o.grabUvPos01 = ComputeGrabScreenPos(UnityObjectToClipPos(float4(-0.5, 0.5, 0, 1)));
                o.grabUvPos11 = ComputeGrabScreenPos(UnityObjectToClipPos(float4( 0.5, 0.5, 0, 1)));

                return o;
            }

            UNITY_DECLARE_SCREENSPACE_TEXTURE(_TransparentPlus1GrabTex);

            float4 frag (v2f i) : SV_Target
            {
                // VR 対応
                UNITY_SETUP_INSTANCE_ID(i);
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i);

                // UV 空間で離散化 (セルの中央の座標にする)
                float2 digit_uv = (floor(i.uv * _CellCnt) + 0.5) / _CellCnt;

                // UV 座標で補完し，digit_uv に対応するクリップ空間座標を計算する．
                // クリップ空間座標は w で割る前なら線形なので線形補間できる．
                float4 digit_grab_pos = lerp(
                    lerp(i.grabUvPos00, i.grabUvPos10, digit_uv.x),
                    lerp(i.grabUvPos01, i.grabUvPos11, digit_uv.x),
                    digit_uv.y
                );
                // スクリーン座標
                float2 digit_grab_uv = digit_grab_pos.xy / digit_grab_pos.w;

                // マクロを使ってサンプリングしないと環境によって壊れるらしい．
                float4 tex = UNITY_SAMPLE_SCREENSPACE_TEXTURE(_TransparentPlus1GrabTex, digit_grab_uv);

                return tex;
            }
            ENDHLSL
        }
    }
}
```

# 雑記
モチベーションとしては，noriben さんの [LEDパネルシェーダー](https://booth.pm/ja/items/3654920) を改造して，向こう側が LCD で見えるフィルターをつくりたい，というものでした．いい感じのシェーダーができたので満足しています．

▼ こんな感じ
![LCD.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3825773/d712839e-9b61-8e8d-6cba-c4a673116489.png)


最初は ddx, ddy を使ってフラグメントシェーダー内で UV 座標とスクリーン座標の変換をしていましたが，ノイズがひどく，代替案として思いついたのがクリップ空間座標の補間です．線形って便利ですね．