-->

【Unity Shader】UIモザイクシェーダー

【Unity Shader】UIモザイクシェーダー
 ※コードが古くなっていたのでURP用にコードを改修しました。(2026/08/14) 


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

モザイクシェーダーの概要

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

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


マテリアルをアタッチしているImageの範囲を絞れば、局所的なモザイクも可能です。

今回作るモザイクシェーダーは、画像単体に対してでなく、カメラに映る最終的な描画結果をまとめてモザイク化する技術になります。
URP環境以前のビルトインシェーダー時代は、
 GrabPass 
というのをシェーダーに一行追加するだけで簡単にこういったシェーダーが作れました。
しかしモダンなURP環境下では、事前に Reder Graphを登録する必要があります。

RenderFeatureの登録

以下はURP環境下でもGrabPassに近い実装を可能にするRender Graphのコードです。
 URPGrabRenderFeature.cs 
というUnityのC#スクリプトを作り中身を丸コピしてください。
コードの中身は以前公開した Render Graphの解説ページと全く一緒になります。
Copy Code

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.RenderGraphModule.Util;
using UnityEngine.Rendering.Universal;

/// <summary>
/// Universal Render Dataに登録させるポストプロセス処理のクラス
/// </summary>
public class URPGrabRenderFeature : ScriptableRendererFeature
{
    //介入する描画処理がGameViewのカメラかどうか。シェーダー側では基本GameViewのカメラにだけ処理を実行させる
    //(後述の_URPGrabTextureはGameViewから画面をキャプチャするので、それ以外のViewで使用するとおかしくなる)
    private readonly int _isGameViewId = Shader.PropertyToID("_IsGameView");

    private URPGrabRenderPass _pass;

    /// <summary>
    /// URPGrabRenderPassのインスタンスを生成
    /// </summary>
    public override void Create() => _pass = new URPGrabRenderPass();

    /// <summary>
    /// Unityに対しURPGrabRenderPass.RecordRenderGraphのロジックをカメラ描画工程に追加するように依頼
    /// </summary>
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        // この描画工程がGameViewのカメラかそれ以外かの情報だけは、この段階で全シェーダーに即時送信する
        bool isGameViewCamera = renderingData.cameraData.cameraType == CameraType.Game;
        Shader.SetGlobalInt(_isGameViewId, isGameViewCamera? 1 : 0);

        //描画工程を追加
        renderer.EnqueuePass(_pass);
    }
}

/// <summary>
///具体的なロジック処理クラス
/// </summary>
public class URPGrabRenderPass : ScriptableRenderPass
{
    private readonly int _textureId = Shader.PropertyToID("_URPGrabTexture");
    private readonly int _texelSizeId = Shader.PropertyToID("_URPGrabTexture_TexelSize");

    /// <summary>
    /// 初期化処理、このポストプロセスの実行タイミングを定義しているだけ
    /// </summary>
    public URPGrabRenderPass()
    {
        //カメラ描画処理が全て終わってからURPGrabRenderPass.RecordRenderGraphを実行させる事をUnityに伝える
        renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
    }

    /// <summary>
    /// 追加される新しい工程に必要なデータ(Context)を受け渡すのためクラス
    /// </summary>
    private class PassData
    {
        public TextureHandle destination; //テクスチャコピー先のメモリ領域
        public int textureId;
        public int texelSizeId;
        public Vector4 texelSize;
    }

    /// <summary>
    ///カメラの画角に映っている情報をテクスチャとしてシェーダーに渡す処理
    /// </summary>
    public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
    {
        //今この瞬間にカメラに書き込んでいる画面の色や深度のデータを取得
        UniversalResourceData resourceData = frameData.Get<UniversalResourceData>();
        //resourceDataからカメラが描画しているテクスチャのメモリ領域を取得
        TextureHandle originalTextureHandle = resourceData.activeColorTexture;

        //これから生成するテクスチャの設計図を作成
        TextureDesc grabTextureDesc;
        try
        {
            //テクスチャの設計図をコピー
            grabTextureDesc = renderGraph.GetTextureDesc(originalTextureHandle);
        }
        catch
        {
            return;
        }
        //設計図を出力用に上書き
        grabTextureDesc.name = "URPGrabTexture";
        grabTextureDesc.clearBuffer = false;
        grabTextureDesc.msaaSamples = MSAASamples.None;
        grabTextureDesc.depthBufferBits = 0;
        var texelSize = new Vector4(1.0f / grabTextureDesc.width, 1.0f / grabTextureDesc.height,
                                            grabTextureDesc.width, grabTextureDesc.height);
        //設計図から新しいメモリ領域を作成
        TextureHandle grabTextureHandle = renderGraph.CreateTexture(grabTextureDesc);

        //Blitで実物のテクスチャデータを丸々コピー
        renderGraph.AddBlitPass(originalTextureHandle, grabTextureHandle, Vector2.one, Vector2.zero, passName: "BlitCopyToGrabTexture");

        using (var builder = renderGraph.AddRasterRenderPass("SetGrabTextureToGlobalState", out PassData passData))
        {
            //読み込み対象をbuilderに登録
            builder.UseTexture(grabTextureHandle, AccessFlags.Read);
            //この描画工程の中でのみ「グローバル変数(全シェーダー共通の設定)」を書き換えることを許可する
            builder.AllowGlobalStateModification(true);

            //コピーしたを画像をpassDataに代入
            passData.destination = grabTextureHandle;
            passData.textureId = _textureId;
            passData.texelSizeId = _texelSizeId;
            passData.texelSize = texelSize;

            //予約されたスケジュールが実行された時の処理
            builder.SetRenderFunc((PassData data, RasterGraphContext context) =>
            {
                //GPUに対して「このテクスチャ(destination)を、このIDの名前で公開せよ」と命令を出す
                //これにより、全シェーダーの _URPGrabTexture に中身が流し込まれる
                context.cmd.SetGlobalTexture(data.textureId, data.destination);
                context.cmd.SetGlobalVector(data.texelSizeId, data.texelSize);
            });
        }
    }
}
ファイルを作成したら、プロジェクトで使用している
 Scriptable Renderer Data 
のインスペクターを確認してください。
確認方法はこちらになります。 p.74 : 【Unity Shader 基礎】URP環境の確認の仕方

インスペクターの一番下、 Add Renderer Feature から作成したURPGrabRenderFeatureを追加してください。


これでURPのポストプロセス処理に、カメラの全画面の色情報をシェーダーに流し込む工程が追加されました。
あらゆるシェーダーコードで、
 _URPGrabTexture 
というテクスチャを参照することが可能です。

モザイクシェーダーの実装

以下シェーダー側のコード全文です。
名前は何でもいいんですが、とりあえず
 Mosaic.shader 
というファイルを作り、中身を全てコピペしてください。
Copy Code

Shader "URPGrab/Mosaic"
{
    Properties
    {
        [HideInInspector]_MainTex("-",2D)="white"{}

        [KeywordEnum(one, equalize)] _Pick("ColorPick", Int) = 1
        _MosaicPixelSize ("MosaicPixelSize", Range(1, 50)) = 1
    }

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

        Pass
        {
            HLSLPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                #pragma multi_compile _PICK_ONE _PICK_EQUALIZE
                #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

                struct Attributes
                {
                    half2 uv : TEXCOORD0;
                    float4 positionOS : POSITION; //オブジェクト空間座標(対象の大元の座標と寸法)
                };

                struct Varyings
                {
                    half2 uv : TEXCOORD0;
                    float4 positionCS : SV_POSITION; //クリップ空間座表(カメラの四角い画角内での対象の座標)
                    float4 positionVP : TEXCOORD1; //ビューポート空間座標(クリップ空間座表 -w ~ +w を 0 ~ w の範囲に修正した座標)
                };

                //URPGrabRenderFeatureから全画面の色情報などを受け取る
                int _IsGameView;
                TEXTURE2D(_URPGrabTexture);
                SAMPLER(sampler_URPGrabTexture);
                half4 _URPGrabTexture_TexelSize;

                //Propertiesに対応する変数
                int _MosaicPixelSize;

                //受け取ったUV座標を大雑把にして返す
                half2 get_mosaic_uv(half2 uv)
                {
                    // モザイク1ブロックのUVサイズ、ピクセルサイズから正規化された0~1のUVサイズに変換する
                    float2 mosaicUVSize = _MosaicPixelSize * _URPGrabTexture_TexelSize.xy;
                    // 実際の大雑把にする処理
                    float2 mosaicUV = floor(uv / mosaicUVSize) * mosaicUVSize;
                    // モザイクブロック中央の座標を返す
                    return mosaicUV + mosaicUVSize / 2;
                }

                //渡したUV座標からGrabした画像のcolorを抜き出す
                half4 get_tex_color(half2 uv)
                {
                    #ifdef _PICK_ONE
                        //指定したuv座標のcolorをそのまま返す
                        return SAMPLE_TEXTURE2D(_URPGrabTexture, sampler_URPGrabTexture, uv);
                    #elif _PICK_EQUALIZE
                        //指定したuv座標から、省略する予定のpixel全体を平均化したcolor値を返す
                        half4 col = 0;
                        int count = 0;

                        // ブロックの中心から、ブロック内を左右・上下対称にサンプリングする
                        float2 halfSize = (_MosaicPixelSize - 1) * 0.5;

                        for(int jx = 0; jx < _MosaicPixelSize; jx++)
                        {
                            for(int jy = 0; jy < _MosaicPixelSize; jy++)
                            {
                                //座標をズラしてピクセル色を取得
                                float2 offset = (float2(jx, jy) - halfSize) * _URPGrabTexture_TexelSize.xy;
                                col += SAMPLE_TEXTURE2D(_URPGrabTexture, sampler_URPGrabTexture, uv + offset);
                                count++;
                            }
                        }
                        return col /count;
                    #endif
                }

                //頂点シェーダー
                Varyings vert(Attributes input)
                {
                    Varyings output;

                    //uv座標を代入
                    output.uv = input.uv;
                    //オブジェクト空間座標をクリップ空間、ビューポート空間座標にそれぞれ変換
                    output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
                    output.positionVP = ComputeScreenPos(output.positionCS);

                    return output;
                }

                //ピクセルシェーダー
                half4 frag (Varyings input) : SV_Target
                {
                    //GameViewのカメラ以外に対しては半透明ピンクを返すだけ
                    if (_IsGameView == 0)
                    {
                        return half4(1, 0, 1, 0.2);
                    }

                    //ビューポート空間座標から奥行を省いて座標を平面化する
                    float2 screenUV = input.positionVP.xy / input.positionVP.w;

                    //もし_MosaicPixelSizeが1より大きいなら1を返し、それ以外は0を返す
                    half isUseMosaic = 1 - step(_MosaicPixelSize, 1);

                    //大雑把にしたUV座標。
                    half2 mosaicUV = get_mosaic_uv(screenUV);

                    //isUseMosaic==1ならモザイク処理を実行、そうでないならうけとった_URGGrabTextureの内容をそのまま返す
                    return lerp(
                        SAMPLE_TEXTURE2D(_URPGrabTexture, sampler_URPGrabTexture, screenUV),
                        get_tex_color(mosaicUV),
                        isUseMosaic
                    );
                }
            ENDHLSL
        }
    }
}
Mosaic.shaderを作成したら、Materialを新規作成し、Shaderの欄に
 URPGrab/Mosaic 
を指定します。
(やり方が分からない場合はこちら。 p.63 : 【Unity Shader 基礎】Imageにシェーダーを反映させる方法)

Unityにシェーダーを反映

Hierarchy上で_URPGrabTexture専用のOverlay CameraとCanvasを追加します。
作成したカメラは、Base CameraのStackの一番下に登録してください。


作成したCanvasにuGUI Imageを配置して、ストレッチで全画面に伸ばします。
(サンプル画像では
 2DGrabCanvas/GrabImage 
でそれをやっています)
Image ComponentのMaterialには先ほど作ったURPGrab/Mosaicを指定したマテリアルをアタッチします。


これで実装完了です。
マテリアルのインスペクターの値を操作することでモザイク度合を制御できます。

プロパティを解説

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

ColorPick

 ColorPick 
に関しては Variantを使ってコード内で遷移を分けています。

MosaicPixelSize

1~50の範囲で変更が可能です。

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

モザイクシェーダーは一見複雑なことをやっていそうだけど、理屈はとてもシンプルになっています。
まずよりシンプルな
 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