本記事は サムザップ Advent Calendar 2025 の22日目の記事です。
本記事での環境
- Unity 6.3(URP 17.x)
- URP プロジェクト(Universal Render Pipeline Asset 使用)
- RenderGraph 有効(Compatibility Mode は無効)
URP 17 からは RenderGraph が導入されていて、カスタムパスも RenderGraph API ベースで書くのが推奨になっています。既存の ScriptableRenderPass ベースの実装は「Compatibility Mode」として残っていますが、新規実装は RenderGraph を使った書き方に寄せた方が良さそうです。
参考:Unity公式ドキュメント「Upgrade to URP 17 (Unity 6.0)」
はじめに
本記事では、「グレースケール」の最小ポストエフェクトを題材にして、
- フルスクリーン用シェーダーを書く
- RenderGraph 対応の ScriptableRenderPass を作る
- ScriptableRendererFeature でカメラパイプラインに差し込む
という流れを一気に通します。
ゴール(この記事でできること)
- Unity 6(URP 17)で 独自のポストエフェクトを挿入する
- コードは、なるべく最小構成で実現する
- ポストエフェクトの処理的には「グレースケール」で実装
- cmd.Blit() や Blitter に依存しない「RenderGraph 標準の書き方」でやってみる
© Unity Technologies Japan/UCL
前提
本題へ入る前に前提となる部分を整理しておきます。
RenderGraph の前知識
RenderGraphについては公式のドキュメントをご確認ください。
参照:Unity公式ドキュメント「Introduction to the render graph system in URP」
ここでは本記事に関連する一部へ触れておきます。
RenderGraph には次のルールがあります:
RenderGraph では、1 つの Raster Pass 内で同じ TextureHandle を Read と Write の両方として宣言することは推奨されません。
(依存関係が曖昧になり、RenderGraph の最適化や動作保証で崩れる可能性があります)
つまり、次のような処理は 禁止:
activeColorTexture を読みつつ、activeColorTexture に書く
そのため、ポストエフェクトで「画面の色を上書きしたい」場合は必ず中間テクスチャが必要になります。
フローはこんな感じ:
① activeColor → tempColor (グレースケール化)
② tempColor → activeColor (書き戻し)
参考:Unity公式ドキュメント「Read or write to a texture in a render pass in URP」
今回は、最小構成の1Pass実装のため、
① activeColorTexture を src として読む
② 新規テクスチャ dst に書く
③ cameraColor を dst に差し替える
で実現しています。
ここでの “1Pass” とは、RenderGraph 上で登録される Raster Pass が1つである、という意味です。実際には cameraColor の差し替えによって、次フレーム以降の入力が更新されるため、見た目として 1Pass のポストエフェクトとして動作します。
RenderGraph を有効化
Unity 6 の新規 URP プロジェクトでは RenderGraph はデフォルト ON ですが、
旧 URP からアップグレードした Project だと 互換モード(Render Graph Disabled) が有効になっていることがあります。
その為、RenderGraphが有効になっている事が前提となっている本記事では、
Edit → Project Settings → Graphics項目「Render Graph」 内のCompatibility Mode (Render Graph Disabled) のチェックを外した状態での環境になります。
これで RenderGraph ベースのパスが動く状態になります。
フォルダ構成
本記事では、下記のようなフォルダ構成で作っています。
Assets/
Shaders/
Grayscale.shader
Scripts/
Rendering/
GrayscaleRendererFeature.cs
GrayscaleRenderPass.cs
GrayscaleVolume.cs
本題
「グレースケール」の最小ポストエフェクトを題材に書いていきます。
参照:Unity公式ドキュメント「Write a render pass using the render graph system in URP」
1. フルスクリーン用シェーダーを書く
Assets/Shaders/Grayscale.shader
Shader "Hidden/Post-processing Custom/GrayScale"
{
SubShader
{
Tags { "RenderPipeline" = "UniversalPipeline" }
Pass
{
Name "GrayScale"
ZTest Always Cull Off ZWrite Off
HLSLPROGRAM
#pragma target 3.0
#pragma vertex Vert
#pragma fragment Frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
TEXTURE2D_X(_MainTexture);
SAMPLER(sampler_BlitTexture);
float4 _MainTexture_TexelSize;
float _Weight;
struct Varyings { float4 positionHCS : SV_POSITION; float2 uv : TEXCOORD0; };
Varyings Vert(uint id : SV_VertexID)
{
Varyings o;
o.positionHCS = GetFullScreenTriangleVertexPosition(id);
o.uv = GetFullScreenTriangleTexCoord(id);
return o;
}
half3 ApplyGrayScale(half3 color)
{
half gray_color = dot(color.rgb, half3(0.299, 0.587, 0.114));
return lerp(color.rgb, gray_color.xxx, _Weight);
}
half4 Frag(Varyings i) : SV_Target
{
half4 col = SAMPLE_TEXTURE2D_X(_MainTexture, sampler_LinearClamp, i.uv);
col.rgb = ApplyGrayScale(col.rgb);
return col;
}
ENDHLSL
}
}
}
コードのポイント
-
_MainTextureを コード側から SetGlobalTexture で渡す -
ApplyGrayScale()内で dot でグレースケール化し lerp でブレンド - フルスクリーントライアングル(3点)を使った最適な描画方式
2. RenderGraph 対応の ScriptableRenderPass / ScriptableRendererFeature を作り、Volumeコンポーネントで調整出来るようにする
Assets/Scripts/Rendering/GrayscaleRendererFeature.cs
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
public class GrayScaleRendererFeature : ScriptableRendererFeature
{
[SerializeField]
private Settings settings = new Settings();
private string ShaderName => "Hidden/Post-processing Custom/GrayScale";
private GrayScaleRenderPass _pass;
private GrayScaleRenderPass CreatePass(Settings s)
{
if(s.shader == null)
s.shader = Shader.Find(ShaderName);
return new GrayScaleRenderPass(s.shader, s.injectionPoint);
}
public override void Create()
{
_pass = CreatePass(settings);
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
if (renderingData.cameraData.isSceneViewCamera) return;
// SceneView等のフィルタ/XR判定などがあればここで
var v = VolumeManager.instance.stack.GetComponent<GrayScaleVolume>();
if (v == null || !v.IsActive())
return;
renderer.EnqueuePass(_pass);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (_pass != null)
{
_pass.Dispose();
_pass = null;
}
}
#if UNITY_EDITOR
private void OnValidate()
{
// 旧pass破棄
if (_pass != null)
{
_pass.Dispose();
_pass = null;
}
// 再生成
Create();
}
#endif
[System.Serializable]
public class Settings
{
public Shader shader;
public RenderPassEvent injectionPoint = RenderPassEvent.AfterRenderingPostProcessing;
}
}
コードのポイント
- Volume が ON のときだけパスを追加する
- URP RendererFeature 標準構造でシンプル
Assets/Scripts/Rendering/GrayscaleRenderPass.cs
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.Universal;
public class GrayScaleRenderPass : ScriptableRenderPass
{
private static readonly int MainTexture = Shader.PropertyToID("_MainTexture");
private static readonly int Weight = Shader.PropertyToID("_Weight");
private string PassName => "GrayScalePass";
private Material _material;
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="shader">Shader</param>
/// <param name="evt">RenderPassEvent</param>
public GrayScaleRenderPass(Shader shader, RenderPassEvent evt)
{
renderPassEvent = evt;
if (shader != null)
_material = CoreUtils.CreateEngineMaterial(shader);
}
/// <summary>
/// RenderGraphのレコード
/// </summary>
/// <param name="renderGraph"></param>
/// <param name="frameData"></param>
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
// Volumeを取得
var stack = VolumeManager.instance.stack;
var volume = stack.GetComponent<GrayScaleVolume>();
if (volume == null || !volume.IsActive() || _material == null) return;
// 必要情報をContextContainerから取得
var resources = frameData.Get<UniversalResourceData>();
var src = resources.activeColorTexture;
var desc = renderGraph.GetTextureDesc(src);
var dst = renderGraph.CreateTexture(desc);
using (var builder = renderGraph.AddRasterRenderPass<PassData>(PassName, out var data))
{
// 3) ラスターパス
data.Material = _material;
data.Src = src;
data.Dst = dst;
data.Weight = volume.blend.value;
builder.UseTexture(src, AccessFlags.Read);
builder.SetRenderAttachment(dst, 0, AccessFlags.Write);
builder.AllowGlobalStateModification(true);
builder.SetRenderFunc((PassData d, RasterGraphContext ctx) =>
{
d.Material.SetFloat(Weight, d.Weight);
ctx.cmd.SetGlobalTexture(MainTexture, d.Src);
CoreUtils.DrawFullScreen(ctx.cmd, d.Material);
});
resources.cameraColor = dst;
}
}
public virtual void Dispose()
{
if (_material != null)
{
CoreUtils.Destroy(_material);
_material = null;
}
}
private class PassData
{
public TextureHandle Src, Dst;
public Material Material;
public float Weight;
}
}
コードのポイント
- 1Pass 実現の肝
- src は activeColorTexture(現在の画面)
- dst は新しい一時テクスチャ
- 1Pass で dst にのみ書き込み
- 最後に
cameraColor = dstと差し替える
- これにより、次以降のパスからは dst が “最新の画面色” として扱われる
- RenderGraph の制約を守りつつ、実質的に 1Pass のフルスクリーンポストエフェクトが成立する
Assets/Scripts/Rendering/GrayscaleVolume.cs
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
[VolumeComponentMenu("Post-processing Custom/GrayScale")]
[VolumeRequiresRendererFeatures(typeof(GrayScaleRendererFeature))]
public class GrayScaleVolume : VolumeComponent, IPostProcessComponent
{
/// <summary>
/// オーバーレイ計算をかけた画像を、元画像にブレンドする係数
/// </summary>
public ClampedFloatParameter blend = new ClampedFloatParameter(0f, 0f, 1f);
public bool IsActive() => active && EffectActive();
/// <summary>
/// レンダリング判定
/// </summary>
private bool EffectActive() => blend.value > 0f;
}
コードのポイント
-
blendが 0 のときは処理しない -
IPostProcessComponent.IsActive()の返り値で最終判定
4. Renderer Asset に Renderer Feature を追加する
ScriptableRenderer に追加する手順は下記になります。
- 該当のUniversal Renderer Asset を開く
- Inspector 下部の 「Renderer Features」 にAdd Renderer Feature →
GrayscaleRendererFeatureを追加 - 対象カメラがこの Renderer Asset を使っていることを確認
これだけで、Volume の blend を上げるとグレースケールが適用されます。
まとめ
- URP17 の RenderGraph では カスタムポストエフェクトも RenderGraph API で書くのが推奨
- ポストエフェクトの基本ルール:
- 同じテクスチャへの Read/Write は同パスで禁止
→ 通常は 2Pass が必要になる
- 同じテクスチャへの Read/Write は同パスで禁止
- 本記事の方法では、
- dst に描画した後 cameraColor を差し替えることで 1Pass を成立させている
- Shader / Pass / Feature の構成は最小構成なので応用しやすい
参考
- Unity公式ドキュメント「Upgrade to URP 17 (Unity 6.0)」
- Unity公式ドキュメント「Introduction to the render graph system in URP」
- Unity公式ドキュメント「Write a render pass using the render graph system in URP」
- Unity公式ドキュメント「Read or write to a texture in a render pass in URP」
- Unity公式ドキュメント「Custom rendering and post-processing in URP」
ライセンス関連
本記事内で扱っているUnityプロジェクトのサンプルは、ユニティちゃんライセンス条項に則って、ユニティ・テクノロジーズ・ジャパン株式会社が提供するユニティちゃんをプロジェクトに利用しています。(© Unity Technologies Japan/UCL)
