RenderTargetを管理するクラスは色々あります。最終的にエンジンのコアAPIが受け取るのは、RenderTexture
及び、RenderTargetIdentifer
、NameId
(int) で、その他はURPが提供するラッパークラスです。
NameId (int)
Shader.PropertyId(string)
で取得される整数のIDです。RenderPipline内では基本的に、このIDによる参照が中心になります。
要するにグローバルな名前参照です。当然名前が衝突したらアウトです。
int texID = Shader.PropertyId("_TmpColorTarget");
cmd.GetTemporaryRT(texId, 1920, 1080);
cmd.ReleaseRT(texId);
似たようなID…というか、同じ Shader.PropertyId
で取得するIDに、SRVとしてBindされるものがあります。以下のように使用するものです。
// hlsl
Texture2D _LightMap : t0;
// cs
int nameInShader = Shader.PropertyId("_LightMap");
Texture2D tex = Resources.Load<Texture2D>(...)
Shader.SetGlobalTexture(nameInShader, tex);
これらの二種類のIDは別のIDとして管理されています。ただ、ややこしいのは、前者(リソースのID)で確保したとき、後者(ShaderへのBindのID)としてもBindされるので、以下のようになります。
基本的には、「それぞれ別物として考えるべき、ただし重複はしないように」というのがお作法のようです。
// hlsl
Texture2D _TexA : t0; // _TexA(赤)がBindされる
Texture2D _TexB : t1; // _TexA(赤)がBindされる
Texture2D _TexC : t2; // _TexA(赤)がBindされる
Texture2D _TexD : t3; // Bindされない(GraphicPiplineの場合、fallbackで灰色が刺さされる)
// cs
cmd.GetTemporaryRT("_TexA", 100, 100); // リソースIDの _TexA と、ShaderID の _TexA にBind
cmd.SetRenderTarget("_TexA");
cmd.ClearRenderTarget(RTClearFlags.Color, Color.red, 0, 0); // 赤
cmd.GetTemporaryRT("_TexB", 100, 100); // リソースIDの _TexB と、ShaderID の _TexB にBind
cmd.SetRenderTarget("_TexB");
cmd.ClearRenderTarget(RTClearFlags.Color, Color.blue, 0, 0); // 青
cmd.SetGlobalTexture("_TexB", "_TexA"); // ShaderID の _TexB に リソースID _TexA をBind(上書き)
cmd.SetGlobalTexture("_TexC", "_TexA"); // ShaderID の _TexC に リソースID _TexA をBind
cmd.SetGloablTexture("_TexD", "_TexC"); // error "render texture _TexC not found"
RenderTargetIdentifer
NameIDを型付けしたものです。厳密には、NativeポインタやInstanceID(RenderTexture/Textureを使う際)としてもつことも出来ます。
この型はUnityのコアエンジンに含まれており、APIも基本的にはこの型かNameIDを受け取るオーバーロードになっています。
struct RenderTargetIdentifier : IEquatable<RenderTargetIdentifier>
{
BuiltinRenderTextureType m_Type; // Swapchain などを指す場合などで意味を持つ
int m_NameID; // 基本ここだけ使う
int m_InstanceID; // TextureAsset からキャストした場合などで意味を持つ
...
}
RenderTargetHandler
NameIDとRenderTargetIdentiferのセットです。URPによるラッパーとしては最も原始的な第初代のリソース(のIDの)管理クラスです。
以下のような使い方がされます。リソースの確保・解放はやってくれるわけではないので外でやる必要があるます。
RenderTargetHandler m_Handler;
public void Init() => m_Handler.Init("_Texture_No_Onamae");
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
...
cmd.GetTemporaryRT(m_Handler.id, descriptor, FilterMode.Bilinear); // NameID互換のAPIで使うとき
cmd.Blit(..., m_Handler.Identifer()); // RenderTargetIdnentifer互換のAPIで使うとき
cmd.ReleaseRT(m_Handler.id); // NameID互換のAPIで使うとき
...
}
RenderTargetIdentiferは、NameIDに型付しただけで同じデータですので、NameID を生int で持ちたなくて済むメリットがあります。
何となく、Nativeリソースの確保・解放がスコープ内でされている雰囲気を感じとることができるぐらいです。インターフェース風に書くとこんな感じです。
interface IRenderable {}
interface IAllocFreeAndRenderable : IRenderable {}
struct RenderTargetIdentifer : IRenderable {}
struct RenderTargetHandler : IAllocFreeAndRenderable {}
RenderTexture
UnityEngine.Object
を継承したクラスで、アセットとして扱われます。特徴としては内部のNativeリソースの寿命が Object
の寿命に連動するということです。ただし、Nativeリソースは遅延生成となっていて、必要になった時に確保されます。
使い回しもされず、Managedリソースも使用するので、毎フレームnew/Destroyするようなものではありません。
Inspectorでポチポチ設定したいとき以外、基本的に使いません。また、Materialに刺したいときなどはObjectでないと刺せないので、これにしておく必要があります。
RenderTargetBufferSystem
URPの第二世代リソース管理ラッパークラスです。
RenderTargetHandlerからの進化点としては、
- 確保・解放コードを内包している
- 裏表2枚のリソースを持つ(使うメモリ量も二倍です、遅延生成とかはないです)
UnivesalRendererでは、もろもろのオブジェクトの描画先となるColorバッファに用いられています。
RenderTargetHandler m_BufferSys;
void Setup() => m_BufferSys = new RenderTargetBufferSystem("_CameraColorAttachment");
void Execute()
{
...
m_BufferSys.SetCameraSettings(cmd, descriptor, FilterMode.Bilinear); // 途中でサイズを変えてもOK
cmd.Blit(..., m_BufferSys.GetBackBuffer(cmd));
m_BufferSys.Clear();
...
}
Swapをすると、BackBufferが切り替わります。ダブルバッファなのでSwapchain的なものを連想しますが、そういうフレームをまたぐためのものではなさそうです。
var src = m_BufferSys.GetBackBuffer(cmd);
m_BufferSys.Swap();
cmd.Blit(src, m_BufferSys.GetBackBuffer(), negaMaterial);
例えば、ネガみたいなポストエフェクトをかけるときに、概念としては入出力が同じリソースになりますが、そういうことはできないので一旦TemporaryなRTを用意してコピーしてから入力にするという方法が一般的です。このクラスを使うと、BackBufferを入力、FrontBufferを出力として、レンダリング後にSwapするだけで良くなります。
// これは無理
cmd.Blit("_ColorTarget", "_ColorTarget");
// 一般的なやりかた
cmd.GetTemporaryRT("_TmpColorTarget", 1920, 1080);
cmd.Blit("_ColorTarget", "_TmpColorTarget"); // Copy
cmd.Blit("_TmpColorTarget", "_ColorTarget");
cmd.ReleaseRT(_TmpColorTarget");
// RenderTargetBufferSystem
cmd.Blit(m_BufferSys.GetBackBuffer(), m_BufferSys.GetFrontBuffer());
m_BufferSys.Swap();
逆に、GetBackBuffer()
の結果をキャッシュしてしまうと問題になるので注意が必要です。特に、RendererやFeature、PassのExecuteはSetupに遅延するので、Setup時にBackBafferを取ってしまうと問題になりかねません。
(個人的には、どっちが自然な挙動かと言われればどっちも微妙な気がしますが…)
m_Pass.Setup(m_BufferSys.GetBackBuffer()); // Buffer_A
EnqueuePass(m_Pass); // Executeは遅延しているのでSwapの後
m_BufferSys.Swap(); // BackBufferが変わる -> Buffer_B
class Pass
{
RenderTargetIdentifer m_Dst; // Buffer_A
void Setup(RenderTargetIdentifer rt) => m_Dst = rt; // Buffer_A
void Execute()
{
// ここでは、Swapされる前の Buffer_A が使われる
}
}
RTHandleSystem
URPの提供する最新式のラッパークラスです。
RenderTargetBufferSystemとの違いとしては、
- リサイズをGlobalなScaleベースで自動実行してくれる
- ダウンサイズ時に再Allocが生じない(Viewportが小さくなる)
- ダブルバッファの廃止(
BufferedRTHandleSystem
を使う)
これまでの、GetTemporaryRT
はその名の通り、カメラ内で確保し解放するような使われ方をしています。しかし、これはそうではなく、カメラ…フレームををまたぐ用途っぽいです。(まだ、使用例が少ないので、いまいち意図を掴み切れません)内部的にも、GetTemporaryRT
ではなくAssetとして new RenderTexture
しています。
イメージコードはこのようになります。
RTHandle screenColorBuffer = RTHandles.Alloc(cameraPixelSize => cameraPixelSize);
RTHandle screenNormalBuffer = RTHandles.Alloc(cameraPixelSize => cameraPixelSize);
for(var frame = 0; ; frame++)
{
foreach(var camera in cameras) // カメラ解像度: [(512, 512), (1280, 720), (1920, 1080)]
{
RTHandles.SetReferenceSize(camera.width, camera.height);
...
cmd.SetRenderTarget(new [] { screenColorBuffer, screenNormalBuffer }
...
}
await NextFrame();
}
screenColorBuffer.Release();
screenNormalBuffer.Release();
GetTemporaryRT
で毎Camera確保・解放する場合、カメラごとにバッファサイズが異なるので、6枚のバッファの合計値が固定でかかります。一方で、RTHandleSystem を使えば、最大解像度の場合の2枚だけで済みます。更にカメラ内でも同時利用しないバッファ同士で使いまわすことも出来そうです。その場合、カメラスコープに連動しない SetReferenceSize
をすることになるので、AllocatorはGlobalのではなくインスタンスバージョンを使った方が良いかもしれません。
// GetTemporaryRT
Camera#0 Color (512, 512)
Camera#0 Normal (512, 512)
Camera#1 Color (1280, 720)
Camera#1 Normal (1280, 720)
Camera#2 Color (1920, 1080)
Camera#2 Normal (1920, 1080)
// RTHandleSystem
Camera#0~2 Color (1920, 1080)
Camera#0~2 Normal (1920, 1080)
原理的には、最大サイズの分の広さのバッファを確保しておいて、それ以下の場合はViewPort(書き込み)とUV(読み込み)を調整してバッファの一部だけを利用することになります。
RTHandle buff = ...;
// write
cmd.SetRenderTarget(buff);
cmd.SetViewport(buff.rtHandleProperties.currentViewportSize);
// read
cmd.SetGloablTexture(buff);
cmd.SetGlobalVector("_UVScale", buff.rtHandleProperties.rtHandleProperties.rtHandleScale);
BufferedRTHandleSystem
RTHandleSystem
にバッファリング機能を持たせたものです。bufferCount
を2にすることで RenderTargetBufferSystem
と同じように使えます。(3以上の使い道は、若干用途の意味合いが異なりますが、TAAとか履歴として使ってほしい感じ)
まとめ
RenderTargetは、ポストプロセスを盛ったりリフレクションや影を多用していると、あっという間に膨れ上がってしまいがちです。最近は、スマホHWでもメモリたっぷりだったりしますが、余ればキャッシュに回したりもできますしね。
そんなリソースの問題もURPでいろいろな試行錯誤がなされているので、参考にして最適化していきましょう!