見出し画像

UIブラー対応事例紹介


実装・動作確認環境

Unity 2022.3.12f1
URP 14.0.3

はじめに

この記事は Colorful Palette アドベントカレンダー 12/13 の記事です。
株式会社Colorful Paletteでクライアントエンジニアをしている「さた」です。
グラフィックス関連の開発をメインに担当しています。

「UIブラー」と言われて、どのようなUI表現が思い浮かぶでしょうか?
このあたりについて、共通用語的なものがしっかり定まっていない気がしていますが(自分が知らないだけかもしれません)、一般的には以下の2つの表現のどちらかを指している場合が多いと思われます。

1.ダイアログのような特定の前面表示UI以外の背景全体をぼかす

著作者:Freepik

2.UIの一部が透過&透過部分がぼけて表示、すりガラスのような表現

著作者:Freepik

前者は「BackgroundBlur」(検索したらUnrealEngineの機能が出てきました)、後者は「グラスモーフィズム」、と言ったりするようです。

どちらの表現もよく見かけるため、最近のタイトルでは描画フローとして組み込むのがほぼ規定路線になっているように感じます。

今回は、これら2つの表現をUnity上でざっくり実装するとどうなるか、一つの例として紹介したいと思います。

出来上がったもの

動画冒頭で表示されているのがBackgroundBlurで、BackgroundBlurが無効になった後画面左右に配置されているのがグラスモーフィズム対応UIです。
これらは1つのRendererFeature、RenderPassの設定にて実現しています。
また、グラスモーフィズム対応UIには専用コンポーネントを付けることで専用シェーダが自動で使われるようにしています。

実装解説

機能要件&方針決め

今回のような表現を実装する上では、以下を必ず考慮する必要があります。

  • ブラー処理の対象となるUI、対象とならないUIをどう区別するか

  • グラスモーフィズム対応UIの下に描画されるUIは存在するか

    • 存在する場合、グラスモーフィズム対応UIが重なることを許容するか

グラスモーフィズム対応UIが重なるケースを考え始めるとかなり頭が痛くなってしまうため、今回は考慮しません。

また、この手の表現を行うためにはUIの描画の途中に画面キャプチャ等の特殊描画を挟むことになるため、URP標準レンダラの通常描画フローではUIを描画せず、専用描画フローのみで描画されるようにします。
最後に、グラスモーフィズム対応UIを描画するための機能も必要です。

まとめると、今回の機能要件と実装方針を以下のように定めます。

  • UIを3つのレイヤーに分けて描画

    • グラスモーフィズム対応UIの下に描画するレイヤー「UIBlurBelow」

    • 通常UIを描画するレイヤー「Default」

      • (グラスモーフィズム対応UIもこのレイヤーで描画)

    • BackgroundBlur有効時にもブラー対象とならないレイヤー「UIBlurAbove」

  • UIは専用RendererFeatureで登録される描画パスのみで描画

    • 標準描画フローではUIが描画されないように

  • グラスモーフィズム対応UIの描画機能

    • 専用シェーダ

    • 専用シェーダの自動設定&パラメータ設定のためのコンポーネント

描画フロー詳細

今回の表現は以下のような順で描画すると意図通り表示されそうです。

  1. カメラカラーバッファにUIBlurBelowレイヤーのUI描画

  2. カメラカラーバッファをブラーキャプチャバッファ(グラスモーフィズム用)にコピー

  3. ブラーキャプチャバッファにブラー適用

  4. カメラカラーバッファにDefaultレイヤーのUI描画

  5. カメラカラーバッファにブラー適用

  6. カメラカラーバッファにUIBlurAboveレイヤーのUI描画

上記の描画処理のうち、2, 3はグラスモーフィズム対応UIのための処理、5はBackgroundBlurのための処理なので、機能が有効になった時だけ処理されるようにします。

RendererFeature実装

描画設定関連をRendererFeatureにて行えるようにし、それを描画パス側に渡す形にします。メインの実装は描画パスなので、RendererFeatureはガワだけのような実装となります。

  • BackgroundBlurのONOFF

  • グラスモーフィズムのONOFF

  • ブラー適用率

  • ブラー幅

  • ブラーに使用する一時テクスチャのスケール

今回用意したRendererFeature

上記を描画設定として用意します。
ついでに、描画パスで使用するブラー用マテリアル生成のためのシェーダ、描画対象とするLayerMaskの設定もRendererFeatureに用意しておきます。

描画パス実装

まず、コンストラクタにてUIのレイヤー分け描画に必要な情報をあらかじめ構築しておきます。
FilteringSettings、RenderStateBlock、ShaderTagIdリストなどです。
renderPassEvent(描画タイミング)の設定としては、UIなのでAfterRenderingPostProcessingが妥当だと思われます。この辺は状況に応じて変えて下さい。

_belowFilteringSettings = new FilteringSettings(RenderQueueRange.transparent, layerMask);
var belowSortingLayer = (short)SortingLayer.GetLayerValueFromName("UIBlurBelow");
_belowFilteringSettings.sortingLayerRange = new SortingLayerRange(belowSortingLayer, belowSortingLayer);

_defaultFilteringSettings = new FilteringSettings(RenderQueueRange.transparent, layerMask);
var defaultSortingLayer = (short)SortingLayer.GetLayerValueFromName("Default");
_defaultFilteringSettings.sortingLayerRange = new SortingLayerRange(defaultSortingLayer, defaultSortingLayer);

_aboveFilteringSettings = new FilteringSettings(RenderQueueRange.transparent, layerMask);
var aboveSortingLayer = (short)SortingLayer.GetLayerValueFromName("UIBlurAbove");
_aboveFilteringSettings.sortingLayerRange = new SortingLayerRange(aboveSortingLayer, aboveSortingLayer);

UIのレイヤー別描画はFilteringSettingsの設定によって可能になります。
今回はuGUIのCanvasに設定可能なSortingLayerを使用して区別します。
GameObject上で設定するレイヤーを使用して区別することも可能ですが、SortingLayerは設定上限がなく、何よりCanvas設定と連動しているため各UIがどのレイヤーに属しているかが分かりやすいのでオススメです。

次に、OnCameraSetupにて今回の描画パスで使用する各種バッファを確保します。上記のフロー通りに描画するとなると、まずグラスモーフィズム用キャプチャバッファ、ブラー処理のための一時バッファが必要になります。
RenderingUtils.ReAllocateIfNeededを使用してRTHandleとして確保します。

最後に実際の描画処理(Execute)ですが、想定フロー通りに描画処理を記述していきます。

// UIBlurBelowのUI描画
using (new ProfilingScope(cmd, _uiBlurBelowSampler))
{
    context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref _belowFilteringSettings, ref _stateBlock);
}

各レイヤーのUIはScriptableRenderContext.DrawRenderersの引数に適切なFilteringSettingsを渡して呼び出せば描画できるので、あとは必要なタイミングで各バッファにブラーを適用するだけです。

グラスモーフィズム対応UIで使用するブラー済みキャプチャバッファは、対応シェーダから参照できるように特定の名前(ここでは「_BlurCaptureTex」)を付けてグローバルテクスチャとして登録しておきます。

int BlurCaptureTex = Shader.PropertyToID("_BlurCaptureTex");
cmd.SetGlobalTexture(BlurCaptureTex, _blurCaptureRT);

グラスモーフィズム対応

まずシェーダの実装をしますが、このシェーダはuGUIにおけるデフォルトシェーダのUI/Defaultシェーダの一部をカスタマイズする形で用意します。
(こうしないとマスク等のuGUI機能が使えなくなってしまうため)
unityのビルトインシェーダはhttps://unity.com/releases/editor/archiveからダウンロード可能です。

  • 頂点シェーダ上でスクリーンスペースにおける位置を計算、フラグメントシェーダに渡す

OUT.screenPos = ComputeScreenPos(OUT.vertex);
  • フラグメントシェーダにて_BlurCaptureTexをスクリーン空間にマッピング、カラーとして使用

    • _BlurCaptureTexの透け具合を_BlurBlendRateとしてパラメータ追加

float2 screenUv = IN.screenPos.xy / IN.screenPos.w;
half3 blurTexColor = tex2D(_BlurCaptureTex, screenUv).rgb;
color.rgb *= lerp(1.0h, blurTexColor, _BlurBlendRate);

上記のカスタマイズだけを入れてグラスモーフィズム対応UIに適用させるのですが、専用パラメータがあるため簡単なコンポーネントを用意し、ついでにこのシェーダも自動設定されるようにしました。

BlurBackgroundコンポーネント

各種設定

ここまで実装できたら、あとは描画に必要な各種設定だけです。まずはSortingLayerを定義します。

CanvasのSortingLayer設定からレイヤー設定に

CanvasのSortingLayer項目を選択し、Add Sorting Layer…を選択します。

SortingLayer設定

Tags & Layers設定のSortingLayerリストを上記のように設定します。
Defaultレイヤーは0でも構わないですが、描画順通りに並べた方が分かりやすいと思ったためこうしています。

次にRendererDataのFiltering設定を変更し、通常描画フローにてUIが描画されないようにします。

RendererDataのLayerMask設定

UIは半透明として描画されるため、上記のようにTransparentLayerMaskの設定からUIレイヤーを外します。

最後に、専用RendererFeatureのレイヤー設定を行います。

RendererFeatureのLayerMask設定

描画対象をUIレイヤーだけにすることで、このRendererFeatureの描画をUIだけに限定します。

実装結果詳細

全体ブラー(BackgroundBlur)を有効にした際の動画です。
画面中央に雑なダイアログ風UIがあり、ブラーが掛かっていません。これはブラー対象外のレイヤーUIBlurAboveに配置されているためなので、レイヤーを変更するとブラー対象になります。
ブラーのボケの大きさやブラーの掛かり具合なども調整できます。

次はグラスモーフィズム対応UIを配置しているときの動画です。
こちらもグラスモーフィズムUIの上に配置されているUIにはブラーが掛かりません。当然、レイヤー設定を変更するとグラスモーフィズムUIの下に入りブラーが掛かります。
全体ブラー同様、こちらもボケの大きさ&かかり具合を調整できます。

おわりに

URPをある程度触っている方から見れば当たり前のことしかやっていませんが、一例として参考になれば幸いです。
結局はコードを見るのがなにより手っ取り早いと思うので、最後に今回の記事で説明した実装コードの全体を載せておきます。興味のある方は参照して下さい。

改善点についても挙げておきたいと思います。

  • BackgroundBlur有効時、ブラーが掛かっている背景は動くか?

    • 動く必要がないならブラーキャプチャを一度だけ作って使いまわせるようにする

    • さらに有効の間その背景部分の描画はスキップされるようにする

  • BackgroundBlur有効時のグラスモーフィズム対応UIの扱い

  • 各種ブラー有効無効を外部からランタイム制御できるようにする

    • グラスモーフィズム対応UIが存在するときだけ必要なブラー処理が実行されるようにする

  • ブラー処理そのものの最適化

最後まで読んでいただきありがとうございました!

実装物

UIRendererFeature.cs

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class UIRendererFeature : ScriptableRendererFeature
{
    [SerializeField]
    private LayerMask _layerMask;

    [SerializeField]
    private bool _execblur = false;
    [SerializeField]
    private bool _execGlassMorphism = false;
    
    [SerializeField][Range(0.0f, 1.0f)]
    private float _blurRate = 1.0f;
    [SerializeField][Range(0.5f, 3.0f)]
    private float _blurWidth = 2.0f;
    [SerializeField][Range(0.1f, 1.0f)]
    private float _blurRenderScale = 0.5f;

    [SerializeField]
    private Shader _blurShader;

    private UIRenderPass _uiRenderPass;

    public override void Create()
    {
        if(_blurShader == null)
        {
            return;
        }

        Material blurMaterial = CoreUtils.CreateEngineMaterial(_blurShader);
        _uiRenderPass = new UIRenderPass(blurMaterial, _layerMask);
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        if (_uiRenderPass == null)
        {
            return;
        }
 
        renderer.EnqueuePass(_uiRenderPass);
    }

    public override void SetupRenderPasses(ScriptableRenderer renderer, in RenderingData renderingData)
    {
        _uiRenderPass?.Setup(_execblur, _execGlassMorphism, _blurRate, _blurWidth, _blurRenderScale);
    }
}

UIRenderPass.cs

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class UIRenderPass : ScriptableRenderPass
{
    private readonly ProfilingSampler _uiBlurBelowSampler = new ProfilingSampler("UIBlurBelow");
    private readonly ProfilingSampler _uiDefaultSampler = new ProfilingSampler("UIDefault");
    private readonly ProfilingSampler _uiBlurAboveSampler = new ProfilingSampler("UIBlurAbove");
    private readonly ProfilingSampler _captureAndBlurSampler = new ProfilingSampler("CaptureAndBlur");
    private readonly ProfilingSampler _blurSampler = new ProfilingSampler("Blur");

    public static class ShaderID
    {
        public static readonly int SimpleBlurParams = Shader.PropertyToID("_SimpleBlurParams");
        public static readonly int SourceTex = Shader.PropertyToID("_SourceTex");
        public static readonly int BlurCaptureTex = Shader.PropertyToID("_BlurCaptureTex");

        public static readonly string BlurCaptureRTName = "BlurCaptureRT";
        public static readonly string TemporaryBlurRT1Name = "TemporaryBlurRT1";
        public static readonly string TemporaryBlurRT2Name = "TemporaryBlurRT2";
        public static readonly string TemporaryBlurRT3Name = "TemporaryBlurRT3";
    }

    private Material _blurMaterial;
    private RenderStateBlock _stateBlock;
    private FilteringSettings _belowFilteringSettings;
    private FilteringSettings _defaultFilteringSettings;
    private FilteringSettings _aboveFilteringSettings;
    private List<ShaderTagId> _shaderTagIds;

    private RTHandle _blurCaptureRT;
    private RTHandle _blurTemporaryRT1;
    private RTHandle _blurTemporaryRT2;
    private RTHandle _blurTemporaryRT3;

    private float _blurBlendRate;
    private float _blurSize;
    private float _blurRenderScale;
    private bool _isExecBlur;
    private bool _isExecGlassMorphism;

    public UIRenderPass(Material blurMaterial, LayerMask layerMask)
    {
        renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing;
        _blurMaterial = blurMaterial;

        _belowFilteringSettings = new FilteringSettings(RenderQueueRange.transparent, layerMask);
        var belowSortingLayer = (short)SortingLayer.GetLayerValueFromName("UIBlurBelow");
        _belowFilteringSettings.sortingLayerRange = new SortingLayerRange(belowSortingLayer, belowSortingLayer);

        _defaultFilteringSettings = new FilteringSettings(RenderQueueRange.transparent, layerMask);
        var defaultSortingLayer = (short)SortingLayer.GetLayerValueFromName("Default");
        _defaultFilteringSettings.sortingLayerRange = new SortingLayerRange(defaultSortingLayer, defaultSortingLayer);

        _aboveFilteringSettings = new FilteringSettings(RenderQueueRange.transparent, layerMask);
        var aboveSortingLayer = (short)SortingLayer.GetLayerValueFromName("UIBlurAbove");
        _aboveFilteringSettings.sortingLayerRange = new SortingLayerRange(aboveSortingLayer, aboveSortingLayer);

        _stateBlock = new RenderStateBlock();

        _shaderTagIds = new List<ShaderTagId>()
        {
            new ShaderTagId("UniversalForward"),
            new ShaderTagId("SRPDefaultUnlit"),
        };
    }

    public void Setup(
        bool isExecBlur,
        bool isExecGlassMorphism,
        float blurBlendRate,
        float blurSize,
        float blurRenderScale)
    {
        _isExecBlur = isExecBlur;
        _isExecGlassMorphism = isExecGlassMorphism;
        _blurBlendRate = blurBlendRate;
        _blurSize = blurSize;
        _blurRenderScale = blurRenderScale;
    }

    public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
    {
        base.OnCameraSetup(cmd, ref renderingData);

        // ブラー適用のための一時バッファ確保
        var rtDescriptor = renderingData.cameraData.cameraTargetDescriptor;
        rtDescriptor.depthBufferBits = 0;
        RenderingUtils.ReAllocateIfNeeded(ref _blurTemporaryRT3, rtDescriptor, FilterMode.Bilinear, TextureWrapMode.Clamp, name: ShaderID.TemporaryBlurRT3Name);

        rtDescriptor.width = (int)(rtDescriptor.width * _blurRenderScale);
        rtDescriptor.height = (int)(rtDescriptor.height * _blurRenderScale);
        RenderingUtils.ReAllocateIfNeeded(ref _blurTemporaryRT1, rtDescriptor, FilterMode.Bilinear, TextureWrapMode.Clamp, name: ShaderID.TemporaryBlurRT1Name);
        RenderingUtils.ReAllocateIfNeeded(ref _blurTemporaryRT2, rtDescriptor, FilterMode.Bilinear, TextureWrapMode.Clamp, name: ShaderID.TemporaryBlurRT2Name);
        RenderingUtils.ReAllocateIfNeeded(ref _blurCaptureRT, rtDescriptor, FilterMode.Bilinear, TextureWrapMode.Clamp, name: ShaderID.BlurCaptureRTName);
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        /*
         描画フローとしては、以下を想定
         ・カメラカラーバッファ
            ・UIBlurBelowのUI描画
         ・ぼかしキャプチャバッファ(グラスモーフィズム(すりガラス)的なUIが存在する場合に実行)
            ・この時点のカメラカラーバッファをコピー → ブラー適用
         ・カメラカラーバッファ
            ・DefaultのUI描画 → ブラー適用(全体ブラー有効時に実行) → UIBlurAboveのUI描画
         
         全てのUIはこのパス上で上記のように描画される想定
        */

        var cameraColorRT = renderingData.cameraData.renderer.cameraColorTargetHandle;
        if (cameraColorRT == null || cameraColorRT.rt == null)
        {
            return;
        }

        const SortingCriteria sortFlags = SortingCriteria.CommonTransparent;
        DrawingSettings drawingSettings = CreateDrawingSettings(_shaderTagIds, ref renderingData, sortFlags);
        CommandBuffer cmd = CommandBufferPool.Get();

        // UIBlurBelowのUI描画
        using (new ProfilingScope(cmd, _uiBlurBelowSampler))
        {
            // ProfilingScope内でコマンド追加されているのでまず実行、コマンドを空にしておく
            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();

            context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref _belowFilteringSettings, ref _stateBlock);
        }

        context.ExecuteCommandBuffer(cmd);
        cmd.Clear();

        // グラスモーフィズム(すりガラス)効果有効時、カメラカラーバッファをコピー → 縦横ブラー適用
        if (_isExecGlassMorphism)
        {
            using (new ProfilingScope(cmd, _captureAndBlurSampler))
            {
                // ProfilingScope内でコマンド追加されているのでまず実行、コマンドを空にしておく
                context.ExecuteCommandBuffer(cmd);
                cmd.Clear();

                ApplyBlur(
                    cmd,
                    ref renderingData,
                    cameraColorRT,
                    _blurCaptureRT,
                    _blurRenderScale,
                    _blurSize,
                    _blurBlendRate);
            }

            CoreUtils.SetRenderTarget(cmd, cameraColorRT);
            cmd.SetGlobalTexture(ShaderID.BlurCaptureTex, _blurCaptureRT);
        }

        context.ExecuteCommandBuffer(cmd);
        cmd.Clear();

        // DefaultのUI描画
        using (new ProfilingScope(cmd, _uiDefaultSampler))
        {
            // ProfilingScope内でコマンド追加されているのでまず実行、コマンドを空にしておく
            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();

            context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref _defaultFilteringSettings, ref _stateBlock);
        }

        context.ExecuteCommandBuffer(cmd);
        cmd.Clear();

        // 全体ブラー有効時、カメラカラーバッファに縦横ブラー適用
        if (_isExecBlur)
        {
            using (new ProfilingScope(cmd, _blurSampler))
            {
                // ProfilingScope内でコマンド追加されているのでまず実行、コマンドを空にしておく
                context.ExecuteCommandBuffer(cmd);
                cmd.Clear();

                ApplyBlur(
                    cmd,
                    ref renderingData,
                    cameraColorRT,
                    cameraColorRT,
                    _blurRenderScale,
                    _blurSize,
                    _blurBlendRate);
            }

            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();
        }

        // UIBlurAboveのUI描画
        using (new ProfilingScope(cmd, _uiBlurAboveSampler))
        {
            // ProfilingScope内でコマンド追加されているのでまず実行、コマンドを空にしておく
            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();

            context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref _aboveFilteringSettings, ref _stateBlock);
        }

        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);
    }

    public void Dispose()
    {
        ReleaseTemporaryRT();
    }

    private void ApplyBlur(
        CommandBuffer cmd,
        ref RenderingData renderingData,
        RTHandle source,
        RTHandle destination,
        float renderScale,
        float blurSize,
        float blendRate)
    {
        if (_blurMaterial == null)
        {
            return;
        }

        Vector4 blurParams = Vector4.zero;
        blurParams.x = blurSize;
        blurParams.y = blendRate;
        const int blurPass = 0;
        const int blurFinalPass = 1;

        // ブラー適用し一時バッファに描画
        _blurMaterial.SetVector(ShaderID.SimpleBlurParams, blurParams);
        Blitter.BlitCameraTexture(cmd, source, _blurTemporaryRT1, _blurMaterial, blurPass);
        Blitter.BlitCameraTexture(cmd, _blurTemporaryRT1, _blurTemporaryRT2, _blurMaterial, blurFinalPass);

        // 描画元と描画先が同じ場合、一旦別バッファに逃がして使用
        if (source == destination)
        {
            Blitter.BlitCameraTexture(cmd, source, _blurTemporaryRT3);
            cmd.SetGlobalTexture(ShaderID.SourceTex, _blurTemporaryRT3);
        }
        else
        {
            cmd.SetGlobalTexture(ShaderID.SourceTex, source);
        }

        // 描画先に反映
        Blitter.BlitCameraTexture(cmd, _blurTemporaryRT2, destination, _blurMaterial, blurFinalPass);
    }

    private void ReleaseTemporaryRT()
    {
        _blurCaptureRT?.Release();
        _blurTemporaryRT1?.Release();
        _blurTemporaryRT2?.Release();
        _blurTemporaryRT3?.Release();
    }
}

SimpleBlur.shader(ブラー処理用シェーダ)

Shader "SimpleBlur"
{
    HLSLINCLUDE

        #pragma target 3.0
        #pragma exclude_renderers gles
        #pragma multi_compile _ _USE_DRAW_PROCEDURAL

        #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
        #include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"

        TEXTURE2D_X(_SourceTex);

        float4 _BlitTexture_TexelSize;
        float4 _SimpleBlurParams;
        #define TEXEL_OFFSET (_SimpleBlurParams.x)
        #define BLUR_BLEND_RATE (_SimpleBlurParams.y)

        static const int BLUR_SAMPLE_COUNT = 8;
        static const float2 BLUR_KERNEL[BLUR_SAMPLE_COUNT] =
        {
            float2(-1.0f, -1.0f),
            float2(-1.0f, 1.0f),
            float2(1.0f, -1.0f),
            float2(1.0f, 1.0f),
            float2(-0.70711f, 0.0f),
            float2(0.0f, 0.70711f),
            float2(0.70711f, 0.0f),
            float2(0.0f, -0.70711f),
        };
        static const float BLUR_KERNEL_WEIGHT[BLUR_SAMPLE_COUNT] =
        {
            0.100f,
            0.100f,
            0.100f,
            0.100f,
            0.150f,
            0.150f,
            0.150f,
            0.150f,
        };

        // ブラーサンプリング
        half4 Blur(half2 uv, half2 targetTexelSize)
        {
            float2 scale = TEXEL_OFFSET * 0.002f;
            scale.y *= targetTexelSize.y / targetTexelSize.x;
            half4 color = 0.0h;

            UNITY_UNROLL for(int i = 0; i < BLUR_SAMPLE_COUNT; i++)
            {
                color += SAMPLE_TEXTURE2D_X(
                    _BlitTexture,
                    sampler_LinearClamp,
                    uv + BLUR_KERNEL[i] * scale
                    ) * BLUR_KERNEL_WEIGHT[i];
            }

            color.a = 1.0h;
            return color;
        }

        half4 FragBlur(Varyings input) : SV_Target
        {
            UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

            const half2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);
            const half2 res = _BlitTexture_TexelSize.xy;
    
            return Blur(uv, res);
        }

        half4 FragBlurBlend(Varyings input) : SV_Target
        {
            UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

            const half2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);
            const half2 res = _BlitTexture_TexelSize.xy;

            half4 blurColor = Blur(uv, res);
            half4 sourceColor = SAMPLE_TEXTURE2D_X(_SourceTex, sampler_LinearClamp, uv);

            return lerp(sourceColor, blurColor, BLUR_BLEND_RATE);
        }

    ENDHLSL

    SubShader
    {
        Tags { "RenderPipeline" = "UniversalPipeline" }
        LOD 100
        ZTest Always ZWrite Off Cull Off

        Pass
        {
            Name "Simple Blur"

            HLSLPROGRAM
                #pragma vertex Vert
                #pragma fragment FragBlur
            ENDHLSL
        }

        Pass
        {
            Name "Simple Blur Blend"

            HLSLPROGRAM
                #pragma vertex Vert
                #pragma fragment FragBlurBlend
            ENDHLSL
        }
    }
}

BlurBackground.cs

using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// ぼかし背景適用UIに付けるコンポーネント
/// Imageコンポーネントと同時に設定する前提
/// </summary>
[RequireComponent(typeof(Image))]
[ExecuteAlways] // 起動してない状態でも表示更新されてほしいため
public class BlurBackground : MonoBehaviour
{
    private static readonly string BlurBackgroundUIShaderPath = "BlurBackgroundUI";
    private static readonly int ShaderPropertyIdBlurBlendRate = Shader.PropertyToID("_BlurBlendRate");

    [SerializeField] [Range(0.0f, 1.0f)] private float _blurBlendRate = 1.0f;

    private Material _material;

#if UNITY_EDITOR
    // blurBlendRateを更新した際にEditor上で即反映されて欲しいため
    // Editro時のみ定義
    private void Update()
    {
        BlendRate = _blurBlendRate;
    }
#endif

    private void OnEnable()
    {
        SetImageMaterial();
    }

    private void OnDestroy()
    {
        if (_material != null)
        {
            Destroy(_material);
            _material = null;
        }
    }

    public float BlendRate
    {
        get => _blurBlendRate;
        set
        {
            _blurBlendRate = Mathf.Clamp01(value);

            if (_material != null)
            {
                _material.SetFloat(ShaderPropertyIdBlurBlendRate, _blurBlendRate);
            }
        }
    }

    /// <summary>
    /// マテリアルの構築/設定
    /// 個別のパラメータ変更を可能とするため、インスタンス毎にマテリアルを構築する
    /// </summary>
    private void SetImageMaterial()
    {
        // 専用マテリアルの設定
        if (_material == null)
        {
            _material = new Material(Shader.Find(BlurBackgroundUIShaderPath));
        }

        _material.SetFloat(ShaderPropertyIdBlurBlendRate, _blurBlendRate);

        Image targetImage = GetComponent<Image>();
        targetImage.material = _material;
    }
}

BlurBackgroundUI.shader

Shader "BlurBackgroundUI"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)

        _BlurBlendRate ("Blur Blend Rate", Float) = 1

        _StencilComp ("Stencil Comparison", Float) = 8
        _Stencil ("Stencil ID", Float) = 0
        _StencilOp ("Stencil Operation", Float) = 0
        _StencilWriteMask ("Stencil Write Mask", Float) = 255
        _StencilReadMask ("Stencil Read Mask", Float) = 255

        _ColorMask ("Color Mask", Float) = 15

        [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
    }

    SubShader
    {
        Tags
        {
            "Queue"="Transparent"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Stencil
        {
            Ref [_Stencil]
            Comp [_StencilComp]
            Pass [_StencilOp]
            ReadMask [_StencilReadMask]
            WriteMask [_StencilWriteMask]
        }

        Cull Off
        Lighting Off
        ZWrite Off
        ZTest [unity_GUIZTestMode]
        Blend SrcAlpha OneMinusSrcAlpha
        ColorMask [_ColorMask]

        Pass
        {
            Name "Default"
        CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0

            #include "UnityCG.cginc"
            #include "UnityUI.cginc"

            #pragma multi_compile_local _ UNITY_UI_CLIP_RECT
            #pragma multi_compile_local _ UNITY_UI_ALPHACLIP

            struct appdata_t
            {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                fixed4 color    : COLOR;
                float2 texcoord  : TEXCOORD0;
                float4 worldPosition : TEXCOORD1;
                float4 screenPos : TEXCOORD2;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            sampler2D _MainTex;
            fixed4 _Color;
            fixed4 _TextureSampleAdd;
            float4 _ClipRect;
            float4 _MainTex_ST;

            sampler2D _BlurCaptureTex;
            uniform half _BlurBlendRate;

            v2f vert(appdata_t v)
            {
                v2f OUT;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
                OUT.worldPosition = v.vertex;
                OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);
                OUT.screenPos = ComputeScreenPos(OUT.vertex);

                OUT.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);

                OUT.color = v.color * _Color;
                return OUT;
            }

            fixed4 frag(v2f IN) : SV_Target
            {
                half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;

                // デバイス正規化座標系とするため`w`で除算する
                // が、ComputeScreenPosの段階で正常な値が入っているっぽいが、
                // シーンビューでちらつくのでこうしておく
                float2 screenUv = IN.screenPos.xy / IN.screenPos.w;

                // キャプチャテクスチャのアルファチャンネルは参照しない
                // ぼかしの透け具合は_BlurBlendRateにて制御
                half3 blurTexColor = tex2D(_BlurCaptureTex, screenUv).rgb;
                color.rgb *= lerp(1.0h, blurTexColor, _BlurBlendRate);

                #ifdef UNITY_UI_CLIP_RECT
                color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
                #endif

                #ifdef UNITY_UI_ALPHACLIP
                clip (color.a - 0.001);
                #endif

                return color;
            }
        ENDCG
        }
    }
}

みんなにも読んでほしいですか?

オススメした記事はフォロワーのタイムラインに表示されます!