URPですりガラス表現

はじめに
URPですりガラス表現を実現するためには、カスタムパスを作成する必要があるみたいです。
TL;DR
URPでのすりガラス表現にカスタムパスが必要な理由
✅ URPの標準レンダリングフローでは描画順序を制御できない
✅ 画面キャプチャ&ブラー処理を適用する必要がある
✅ ScriptableRendererFeature & ScriptableRenderPassを作成することで解決可能
用語の補足
レンダリングパイプラインの中核:低レベル描画とコマンド発行
ScriptableRenderContext
- カスタムレンダリング処理を実行するためのコンテキスト
- CommandBufferの発行やDrawRenderersの呼び出しなど、低レベルな描画処理にアクセス
DrawRenderers
- ScriptableRenderContextのメソッドの1つで、指定したFilteringSettingsに基づいてシーン内のレンダラーを描画するために使用
CommandBuffer
- 複数のレンダリング命令をまとめて実行するためのバッファ
- カスタム描画処理やブラー処理の際に、効率的な描画のために利用
CommandBufferPool
- CommandBufferを効率的に再利用するためのプール機構
- 頻繁に生成・破棄することを防ぎ、パフォーマンスの最適化に寄与
オブジェクトの選別と描画順制御
SortingLayer
- UIやスプライトの描画順序を決定するためのレイヤー
LayerMask
- オブジェクトの描画や処理対象となるレイヤーを指定する仕組み
- RendererFeatureや描画パスで、どのUI要素に処理を適用するかを制御するのに利用
RenderQueue
- 描画順序を管理するためのキュー
- 特に透明なオブジェクトの描画順を正しく制御するために重要
FilteringSettings
- ScriptableRenderContext.DrawRenderersなどで、どのオブジェクトを描画対象とするかをフィルタリングするための設定
- UIを各レイヤーごとに分けて描画する際に使用
ShaderTagId
- シェーダ内で定義された特定のパスやタグを識別するためのID
- 描画パスで使用するシェーダバリアントを指定する際に利用
レンダリング状態の設定とパフォーマンス計測
RenderStateBlock
- 描画時に適用されるレンダリング状態(ブレンドモード、ステンシルテストなど)をまとめた設定
- 特定の描画条件下での挙動を制御
ProfilingScope
- 描画処理のパフォーマンス計測を行うための仕組み
- どの処理がどれだけ時間を消費しているかを測定するためにコード内で利用
リソース管理とユーティリティ
RTHandle
- URPにおいて、動的にレンダリングターゲット(Render Texture)を管理するためのハンドル
- ブラー処理用の一時バッファの管理などに利用
RenderingUtils.ReAllocateIfNeeded
必要に応じてレンダーテクスチャ(RT)の再割り当てを行うユーティリティ関数
画面サイズやレンダリング設定が変わったときに、適切なサイズのバッファを確保するために使用
Blitter
- Unity内のユーティリティで、テクスチャのコピーなどを行う
- ブラー処理で中間レンダーテクスチャ間のデータ転送に利用
まとめ
レンダリング実行の中核
ScriptableRenderContextを中心に、DrawRenderersやCommandBuffer(およびCommandBufferPool)が描画命令の発行と実行を担う
描画対象の選別と順序決定
FilteringSettingsとShaderTagIdが描画対象を絞り込み、SortingLayer、LayerMask、RenderQueueがその描画順序や対象レイヤーの管理に寄与
描画状態の管理と最適化
RenderStateBlockで描画状態を統一管理し、ProfilingScopeで処理のパフォーマンスを計測RendererFeatureがこれらを組み合わせたカスタム処理の実装を可能にする
リソース管理と補助処理
RTHandleとRenderingUtils.ReAllocateIfNeededがレンダーテクスチャなどのリソース管理を行い、Blitterがテクスチャ転送などの補助処理を担う

URPとビルトインレンダーパイプラインの違い
- ビルトインレンダーパイプラインでは、CommandBufferを使用して描画処理を挿入できた
- URPでは スクリプタブルレンダーパイプライン(SRP) を採用しており、標準のレンダリングフローには特定の描画処理を追加できない
- URPではScriptableRendererFeatureとScriptableRenderPassを活用してカスタムパスの作成が必要

すりガラス表現を実現する手法
すりガラス表現では背景をぼかして透過させるため、以下のような特定の描画順序とブラー処理が必要となります。
描画順序
URPのデフォルト設定では、UIの背後の背景を取得してブラー処理を適用することが難しいため、特定のレイヤー分けと描画順序の制御が必要となります。
- UIBlurBelowレイヤーの描画(すりガラスUIの背後にある要素)
- 画面キャプチャ&ブラー適用
- ここでカメラのカラーバッファをキャプチャし、ブラーシェーダを適用
- Defaultレイヤーの描画(通常のUIを描画)
- UIBlurAboveレイヤーの描画(すりガラス効果を適用しない前景UI)
カスタムレンダーパスの作成
カスタムレンダーパスを作成し、特定のタイミングで画面キャプチャとブラー処理を挿入します
ScriptableRendererFeatureの実装
- Createメソッドでカスタムパスを初期化
- AddRenderPassesでURPの描画キューにカスタムパスを追加
ScriptableRenderPassの実装
- 画面の一部をキャプチャし、ブラーシェーダを適用して再描画
カスタムレンダラーアセットを作成
- UnityのCreateメニューから Rendering > Universal Render Pipeline > Forward Rendererを作成
作成したカスタムレンダラーアセットにScriptableRendererFeatureを追加
- これにより、URPのレンダリングフローにカスタムパスを組み込むことが可能になる

ScriptableRendererFeatureについて
ScriptableRendererFeatureは、カスタムなレンダーパスをURPのレンダリングプロセスに組み込むための仕組みです。
目的
独自のレンダリング処理(例:後処理エフェクト、特定の描画順序の制御、ブラー処理など)をパイプラインに追加するために使用します
主な機能
初期化と設定
Create() メソッド内で、カスタムパス(ScriptableRenderPass)の初期化や設定を行います
レンダーパスの追加
AddRenderPasses() メソッドをオーバーライドし、レンダリングのキューに対してカスタムレンダーパスを登録します
エディタとの連携
Unityエディタ上で設定可能なプロパティを持たせ、必要に応じてオンオフやパラメータ調整ができるようにします

ScriptableRenderPassについて
ScriptableRenderPassは実際に描画処理を記述するためのクラスです。
ScriptableRendererFeatureによって管理され、実際の描画処理の中身を担当するため、エフェクトや特殊な処理を自由にカスタマイズする際の中核部分となります。
目的
カスタムな描画ロジックを定義し、特定のレンダリングタイミングで実行される処理を実装します
主な機能
Execute()メソッド
このメソッド内で、カメラのレンダリング結果に対して直接描画命令(例えば、画面キャプチャ、ブラー処理、特殊なマテリアルでの描画など)を発行します
レンダリングイベントの指定
パスが実行されるタイミングを制御するために、どのレンダリングイベント(例:Opaqueレンダリング後、Transparentレンダリング前など)に組み込むかを設定できます
コマンドバッファの利用
描画処理はCommandBufferを介して実行され、これにより低レベルの描画命令や後処理エフェクトを実現します

連携
設定と初期化
- ScriptableRendererFeatureがカスタムレンダーパス(ScriptableRenderPass)のインスタンスを生成し、必要なパラメータや設定を渡します
レンダリングパイプラインへの組み込み
- ScriptableRendererFeatureのAddRenderPasses()で、生成したScriptableRenderPassをURPのレンダリングキューに追加し、適切なタイミングで処理が行われるようにします
描画処理の実行
- URPのレンダリングフロー内で、登録されたScriptableRenderPassがExecute()を通じて実際の描画やエフェクト処理を行い、最終的なレンダリング結果に反映されます

記事中のコードの解析
UIRendererFeature.cs
RendererFeatureとして専用のRenderPassを生成、レンダラーに登録
- シリアライズされたフィールドでInspector上から各パラメータ(ブラーの強度や適用するレイヤー)を設定できる
- Create() メソッドでシェーダからマテリアルを生成し、RenderPassのインスタンスを生成
- AddRenderPasses() でレンダラーにRenderPassを追加し、実際の描画フローに組み込み
UIRenderPass.cs
実際の描画処理(RenderPass)を記述
- 各UIレイヤー(UIBlurBelow、Default、UIBlurAbove)の描画処理をFilteringSettingsを使って実装
- 一時的なレンダーターゲット(RTHandle)を確保、ブラー用のキャプチャや一時バッファを管理
- 描画フローに沿って、ブラーの適用処理を実行
SimpleBlur.shader
ブラー処理のコア部分
- 頂点シェーダでスクリーンスペースの座標を計算し、フラグメントシェーダへ渡す
- フラグメントシェーダで定義済みのサンプル(BLUR_KERNEL)を使って周囲のピクセルをサンプリング、ブラーを実現
BlurBackground.cs
UIのImageコンポーネントに付与して、専用シェーダ(BlurBackgroundUI)を自動で適用し、ブラーのブレンド率を変更
BlurBackgroundUI.shader
UI/Defaultシェーダをベースに、ブラー済みキャプチャテクスチャ(_BlurCaptureTex)を使用して、グラスモーフィズム表現を実現

UIRenderPassのコード
紹介されている実装が初見だと難しかったので、コメント追加したり処理を共通化したりしてみました。(といってもExecutePass やFlushCommandBufferといったヘルパーメソッドに切り出しただけですが)
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");
// シェーダー用定数(プロパティIDやRT名)
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;
// 各UI層に対応するフィルタリング設定の生成
_belowFilteringSettings = new FilteringSettings(RenderQueueRange.transparent, layerMask);
short belowSortingLayer = (short)SortingLayer.GetLayerValueFromName("UIBlurBelow");
_belowFilteringSettings.sortingLayerRange = new SortingLayerRange(belowSortingLayer, belowSortingLayer);
_defaultFilteringSettings = new FilteringSettings(RenderQueueRange.transparent, layerMask);
short defaultSortingLayer = (short)SortingLayer.GetLayerValueFromName("Default");
_defaultFilteringSettings.sortingLayerRange = new SortingLayerRange(defaultSortingLayer, defaultSortingLayer);
_aboveFilteringSettings = new FilteringSettings(RenderQueueRange.transparent, layerMask);
short aboveSortingLayer = (short)SortingLayer.GetLayerValueFromName("UIBlurAbove");
_aboveFilteringSettings.sortingLayerRange = new SortingLayerRange(aboveSortingLayer, aboveSortingLayer);
_stateBlock = new RenderStateBlock();
_shaderTagIds = new List<ShaderTagId>()
{
new ShaderTagId("UniversalForward"),
new ShaderTagId("SRPDefaultUnlit"),
};
}
/// <summary>
/// ブラーおよびグラスモーフィズム効果の実行フラグとパラメータの設定
/// </summary>
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);
// ブラー用一時RTの確保
RenderTextureDescriptor 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)
{
RTHandle cameraColorRT = renderingData.cameraData.renderer.cameraColorTargetHandle;
if (cameraColorRT == null || cameraColorRT.rt == null)
{
return;
}
// UI描画用のDrawingSettings作成(共通設定)
const SortingCriteria sortFlags = SortingCriteria.CommonTransparent;
DrawingSettings drawingSettings = CreateDrawingSettings(_shaderTagIds, ref renderingData, sortFlags);
CommandBuffer cmd = CommandBufferPool.Get();
// ■ UIBlurBelowパス: ブラー下に描画するUI
ExecutePass(context, cmd, _uiBlurBelowSampler, () =>
{
context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref _belowFilteringSettings, ref _stateBlock);
});
// ■ グラスモーフィズム(すりガラス)パス:
// カメラカラーバッファをキャプチャしてブラー処理を行う
if (_isExecGlassMorphism)
{
ExecutePass(context, cmd, _captureAndBlurSampler, () =>
{
ApplyBlur(cmd, ref renderingData, cameraColorRT, _blurCaptureRT, _blurRenderScale, _blurSize, _blurBlendRate);
});
// ブラー結果をグローバルテクスチャとして設定
CoreUtils.SetRenderTarget(cmd, cameraColorRT);
cmd.SetGlobalTexture(ShaderID.BlurCaptureTex, _blurCaptureRT);
FlushCommandBuffer(cmd, context);
}
// ■ Default UIパス: メインUIの描画
ExecutePass(context, cmd, _uiDefaultSampler, () =>
{
context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref _defaultFilteringSettings, ref _stateBlock);
});
// ■ 全体ブラー(後処理)パス: ブラー処理が有効な場合、カメラカラーバッファ全体にブラーを適用
if (_isExecBlur)
{
ExecutePass(context, cmd, _blurSampler, () =>
{
ApplyBlur(cmd, ref renderingData, cameraColorRT, cameraColorRT, _blurRenderScale, _blurSize, _blurBlendRate);
});
}
// ■ UIBlurAboveパス: ブラー上に描画するUI
ExecutePass(context, cmd, _uiBlurAboveSampler, () =>
{
context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref _aboveFilteringSettings, ref _stateBlock);
});
CommandBufferPool.Release(cmd);
}
/// <summary>
/// 共通の処理として、指定のプロファイリングスコープ内でアクションを実行し、
/// コマンドバッファの実行とクリアを行います。
/// </summary>
private void ExecutePass(ScriptableRenderContext context, CommandBuffer cmd, ProfilingSampler sampler, System.Action passAction)
{
using (new ProfilingScope(cmd, sampler))
{
FlushCommandBuffer(cmd, context);
passAction();
}
FlushCommandBuffer(cmd, context);
}
/// <summary>
/// コマンドバッファを実行してクリアするユーティリティメソッド
/// </summary>
private void FlushCommandBuffer(CommandBuffer cmd, ScriptableRenderContext context)
{
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
}
/// <summary>
/// ブラー効果を適用するための処理。
/// 指定されたRTに対して、2段階のブラー処理を行います。
/// </summary>
private void ApplyBlur(
CommandBuffer cmd,
ref RenderingData renderingData,
RTHandle source,
RTHandle destination,
float renderScale,
float blurSize,
float blendRate)
{
if (_blurMaterial == null)
{
return;
}
// ブラーのパラメータ設定(x:ブラーサイズ、y:ブレンド率)
Vector4 blurParams = new Vector4(blurSize, blendRate, 0f, 0f);
const int blurPass = 0;
const int blurFinalPass = 1;
// 第1段階: ソースから一時RT1へ、ブラー適用(水平・垂直などのパスを切り替え)
_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);
}
// 第2段階: 一時RT2から最終出力先へ反映
Blitter.BlitCameraTexture(cmd, _blurTemporaryRT2, destination, _blurMaterial, blurFinalPass);
}
public void Dispose()
{
ReleaseTemporaryRT();
}
/// <summary>
/// 確保した一時レンダーターゲットの解放
/// </summary>
private void ReleaseTemporaryRT()
{
_blurCaptureRT?.Release();
_blurTemporaryRT1?.Release();
_blurTemporaryRT2?.Release();
_blurTemporaryRT3?.Release();
}
}

UIRenderPassの処理の流れ(詳細版)
1. 初期設定(コンストラクタ内)
- 各UI描画用のフィルタリング設定(FilteringSettings)を、対象レイヤー("UIBlurBelow"、"Default"、"UIBlurAbove")に合わせて初期化する。
- シェーダータグ(ShaderTagId)のリストを作成し、後で描画対象の判別に使用する。
- ブラー処理や各種UI描画用のプロファイリングサンプラーを定義する。
2. カメラセットアップ(OnCameraSetupメソッド)
- カメラのターゲットディスクリプタ(cameraTargetDescriptor)からレンダーターゲットの設定を取得し、深度バッファを使用しないようにする。
- ブラー効果用の一時レンダーターゲット(RTHandle)を以下のように確保または再確保する:
- _blurTemporaryRT3: 元の解像度で確保(主に描画元と描画先が同一の場合の一時退避用)。
- _blurTemporaryRT1, _blurTemporaryRT2, _blurCaptureRT: ブラー処理用に、指定のブラー用スケール(_blurRenderScale)を適用した解像度で確保。
3. メイン処理(Executeメソッド)
-
カメラカラーターゲットの取得
- RenderingDataからカメラのカラーターゲット(cameraColorRT)を取得し、存在しない場合は処理を中断する。
-
共通の描画設定とコマンドバッファの生成
- 共通のDrawingSettings(ソート基準は透明オブジェクト用)を作成。
- CommandBufferをCommandBufferPoolから取得する。
各レンダーパスの実行(ExecutePassを利用)
-
UIBlurBelowパス
- プロファイリングスコープ(_uiBlurBelowSampler)内で、
- コマンドバッファの実行&クリア後、
- カリング結果に基づき、「UIBlurBelow」レイヤー用のUIを描画する。
- プロファイリングスコープ(_uiBlurBelowSampler)内で、
-
グラスモーフィズムパス(条件付き)
- _isExecGlassMorphism が有効の場合、プロファイリングスコープ(_captureAndBlurSampler)内で、
- ApplyBlurメソッドを呼び出し、カメラカラーターゲットから一時レンダーターゲット(_blurCaptureRT)へブラー処理を適用する。
- その後、
- カメラカラーターゲットをレンダーターゲットとして再設定し、
- グローバルテクスチャとしてブラー処理結果(_blurCaptureRT)をセットする。
- _isExecGlassMorphism が有効の場合、プロファイリングスコープ(_captureAndBlurSampler)内で、
-
Default UIパス
- プロファイリングスコープ(_uiDefaultSampler)内で、
- コマンドバッファを実行&クリアした後、
- 「Default」レイヤー用のUIを描画する。
- プロファイリングスコープ(_uiDefaultSampler)内で、
-
全体ブラー(後処理)パス(条件付き)
- _isExecBlur が有効の場合、プロファイリングスコープ(_blurSampler)内で、
- ApplyBlurメソッドを呼び出し、カメラカラーターゲットに対してブラー処理を適用する(ソースと描画先が同一の場合の処理も含む)。
- _isExecBlur が有効の場合、プロファイリングスコープ(_blurSampler)内で、
-
UIBlurAboveパス
- プロファイリングスコープ(_uiBlurAboveSampler)内で、
- コマンドバッファを実行&クリアした後、
- 「UIBlurAbove」レイヤー用のUIを描画する。
- プロファイリングスコープ(_uiBlurAboveSampler)内で、
- 最後に、使用したCommandBufferを解放する。
4. ブラー処理(ApplyBlurメソッド)
- _blurMaterial が存在するか確認し、なければ処理を中断する。
- ブラーのパラメータ(ブラーサイズとブレンド率)をシェーダーに設定する。
-
第1段階
-
Blitter.BlitCameraTexture()
を用いて、ソースレンダーターゲットから一時RT1へブラー処理(blurPass)を適用する。 - 続いて、一時RT1から一時RT2へ、最終ブラー処理(blurFinalPass)を適用する。
-
-
描画元と描画先の分岐
- もしソースと描画先が同一の場合は、一時RT3にソースをコピーし、グローバルテクスチャとして設定する(競合を避けるため)。
- 異なる場合は、そのままソースをグローバルテクスチャとして設定する。
-
第2段階
- 一時RT2から、最終的な描画先に対してブラー結果を反映する。
5. リソースの解放(Disposeメソッド)
- 使用済みの一時レンダーターゲット(_blurCaptureRT、_blurTemporaryRT1、_blurTemporaryRT2、_blurTemporaryRT3)を解放する。

ScriptableRenderPassの共通フローっぽいもの
-
コンストラクタ
- レンダーパスの設定、マテリアル・フィルタリングの準備
-
OnCameraSetup
- レンダーターゲットの確保・リサイズ
-
Execute
- コマンドバッファの作成と描画処理の実行
-
FrameCleanup
- 一時バッファの解放(必要な場合)
-
Dispose
- リソースの解放(
RTHandle.Release()
など)
- リソースの解放(

Unity6以降
RenderGraph以前のシステムは動かなくなるらしい。。。