こんにちは。
Cocone Engineering(以下、CE)でグラフィックエンジニア兼テクニカルアーティストのTechLeadとして、データパイプラインやレンダーパイプラインの開発、アセット制作環境の整備等に携わっている高野正也と申します。
CEでは常にユーザー体験の最大化を目指し、Unity 6 や URP を活用したアプリケーション開発を進めています。
本記事では、その中で培われた実践的な知見の一部として、UnityのRenderFeatureを用いたマルチパスレンダリングについて、その仕組みや実装方法を説明します。
はじめに
Coconeでは主にUnity 6とUniversal Render Pipeline (URP)を用いてアプリ開発を行っています。以前使用されていたBuiltInレンダラーではシェーダ内に定義された複数パスを使ったマルチパスレンダリングを容易に行うことができました。
また、URPでも初期のバージョンではマルチパスレンダリングを容易に行えましたが、仕様変更により、現在ではRenderFeatureを用いた実装が必要となっています。
本記事について
今回紹介するマルチパスレンダリングについて非常にシンプルな処理ですが、URPのバージョン更新等の影響もありサンプル例が見つからず、はじめてURPでマルチパスレンダリングを行いたいという方は困っている方も多いのではないでしょうか。
本記事はすぐにマルチパスレンダリングを実装できるように極力シンプルな内容にしています。サンプルが皆様のお役に立てましたら幸いです。
・Unity 6000.2.2f1、URP 17.2.0、Forward Rendering Pathで動作確認していますが、Unity 6000系(Unity 6)であれば問題なく動作します。
RenderFeature とは何か
RenderFeatureはレンダーパイプラインに柔軟な描画処理を追加するための仕組みです。以下のような利点があります。
・拡張性: 将来、さらなる描画ステージ(例:カスタムライト、ポストFX、シャドウパスなど)を手軽に追加できます。
・柔軟性: 描画フローの途中でコードを差し込むことが可能なため、表現力が豊かになります。
サンプル説明
3Dモデルに割り当てたCustomMultipassシェーダの2つのパスを使い、2パスレンダリングを行うサンプルです。1パス目でステンシルバッファに1を描画し、2パス目でステンシルバッファに1が書かれている部分のみ描画を行います。
実装内容はとてもシンプルですが、応用により同一座標でのモデルの優先描画や、部分的なマスク描画、アウトラインなど様々な用途に活用できます。
実装手順
① CustomRenderFeature.cs、CustomRenderPass.cs、CustomMultipass.shaderの3つのファイルをUnityプロジェクト内に作り、中身をコピー&ペーストして保存してください。
using System;
using UnityEngine;
using UnityEngine.Rendering.Universal;
namespace Example.Graphics
{
public class CustomRenderFeature : ScriptableRendererFeature
{
[Serializable]
public class Settings
{
public string passTag = "CustomPass";
public string drawTagName = "WriteStencil";
public RenderPassEvent renderPassEvent = RenderPassEvent.BeforeRenderingOpaques;
public LayerMask layerMask = ~0;
}
public Settings settings = new();
private CustomRenderPass renderPass;
public override void Create()
{
renderPass = new CustomRenderPass(
settings.passTag,
settings.drawTagName,
settings.renderPassEvent,
settings.layerMask
);
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
// プレビュー用カメラではスキップ
if (renderingData.cameraData.cameraType == CameraType.Preview)
{
return;
}
renderer.EnqueuePass(renderPass);
}
}
}
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RendererUtils;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.Universal;
namespace Example.Graphics
{
public class CustomRenderPass : ScriptableRenderPass
{
private readonly string _profilerTag;
private readonly ProfilingSampler _profilingSampler;
private readonly Material _overrideMaterial;
private readonly int _overrideMaterialPassIndex;
private readonly LayerMask _layerMask;
private readonly RenderStateBlock _renderStateBlock;
private readonly List<ShaderTagId> _shaderTagIds = new();
public CustomRenderPass(
string tag,
string shaderTagName,
RenderPassEvent passEvent,
LayerMask layerMask
)
{
_profilerTag = tag;
_profilingSampler = new ProfilingSampler(tag);
renderPassEvent = passEvent;
_layerMask = layerMask;
_renderStateBlock = new RenderStateBlock(RenderStateMask.Nothing);
if (!string.IsNullOrEmpty(shaderTagName))
{
_shaderTagIds.Add(new ShaderTagId(shaderTagName));
}
}
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
var cameraData = frameData.Get<UniversalCameraData>();
var renderingData = frameData.Get<UniversalRenderingData>();
var resourceData = frameData.Get<UniversalResourceData>();
foreach (var shaderTag in _shaderTagIds)
{
using var builder = renderGraph.AddRasterRenderPass<PassData>(
$"{_profilerTag}_{shaderTag.name}",
out var passData,
_profilingSampler
);
passData.ColorTarget = resourceData.activeColorTexture;
builder.SetRenderAttachment(passData.ColorTarget, 0);
builder.SetRenderAttachmentDepth(resourceData.activeDepthTexture);
var sorting = SortingCriteria.CommonOpaque;
var rendererListDesc = new RendererListDesc(
shaderTag,
renderingData.cullResults,
cameraData.camera
)
{
sortingCriteria = sorting,
renderQueueRange = RenderQueueRange.opaque,
layerMask = _layerMask,
overrideMaterial = _overrideMaterial,
overrideMaterialPassIndex = _overrideMaterialPassIndex,
stateBlock = _renderStateBlock,
rendererConfiguration = PerObjectData.None,
};
passData.RendererList = renderGraph.CreateRendererList(rendererListDesc);
builder.UseRendererList(passData.RendererList);
builder.AllowPassCulling(false);
builder.SetRenderFunc((PassData data, RasterGraphContext ctx) => { ctx.cmd.DrawRendererList(data.RendererList); });
}
}
private class PassData
{
public TextureHandle ColorTarget;
public RendererListHandle RendererList;
}
}
}
Shader "Example/CustomMultipass"
{
Properties
{
_MainTex("Main Texture", 2D) = "white" {}
_Color("Color Tint", Color) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
// === パス 1: ステンシルを書き込む ===
Pass
{
Name "WriteStencil"
Tags { "LightMode" = "WriteStencil" }
Stencil
{
Ref 1
Comp always
Pass replace
}
ZWrite Off
ColorMask 0
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct Attributes
{
float4 positionOS : POSITION;
};
struct Varyings
{
float4 positionHCS : SV_POSITION;
};
Varyings vert(Attributes IN)
{
Varyings OUT;
OUT.positionHCS = TransformObjectToHClip(IN.positionOS);
return OUT;
}
half4 frag(Varyings IN) : SV_Target
{
return 0;
}
ENDHLSL
}
// === パス 2: ステンシルにマスクされた範囲だけ描画 ===
Pass
{
Name "RenderMasked"
Tags { "LightMode" = "UniversalForward" }
Stencil
{
Ref 1
Comp equal
Pass keep
}
ZWrite On
ColorMask RGBA
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
float4 _Color;
CBUFFER_END
struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
};
struct Varyings
{
float4 positionHCS : SV_POSITION;
float2 uv : TEXCOORD0;
};
Varyings vert(Attributes IN)
{
Varyings OUT;
OUT.positionHCS = TransformObjectToHClip(IN.positionOS);
OUT.uv = TRANSFORM_TEX(IN.uv, _MainTex);
return OUT;
}
half4 frag(Varyings IN) : SV_Target
{
half4 texColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, IN.uv);
return texColor * _Color;
}
ENDHLSL
}
}
FallBack "Hidden/Shader Graph/FallbackError"
}
② 描画モデルのマテリアルにCustomMultipassシェーダを割り当てます。またモデルにこの処理を適用する専用のレイヤー名CustomRenderFeatureLayerを追加し割り当てます。
③ 使用しているUniversalRenderDataの下部にあるAdd Render Featureボタンを押しCustomRenderFeatureを追加します。(モバイルのデフォルト設定だとAssets/URP-Balanced-Rendererを使用)Layer Maskに追加したCustomRenderFeatureLayerのみを設定してください。
以上で、実装は完了です。
白い球体が描画されただけで通常と変わらないように見えますが、シェーダに入っている2つのパスを使ったマルチパスレンダリングで描画が行われています。
次回、実装内容について解説編で説明させて頂きます。

