6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-10-19

概要

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

▼ 元画像
Mosaic-Nop.png

▼ スクリーンスペースのモザイク
Mosaic-ScreenSpace.png

▼ オブジェクトスペースのモザイク
Mosaic-UvSpace.png

Screen Space のモザイクシェーダー

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

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

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パネルシェーダー を改造して,向こう側が LCD で見えるフィルターをつくりたい,というものでした.いい感じのシェーダーができたので満足しています.

▼ こんな感じ
LCD.png

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

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?