ノートの端の書き残し

UnityやらC#やら。設計が得意かもしれない。

【URP】RenderGraph入門 概念編

前置き

現時点での私の持っている知識は、URP12くらいまでの、

  1. ScriptableRendererFeatureからScriptableRendererPassを生成して
  2. Passの中でCommandBufferに色んな命令を入れて実行

という実装知識です。
レンダリングパイプラインのカスタムによる負荷軽減(縮小バッファ、などと呼ばれるもの)や簡単なポストエフェクトなどは実装経験がありますが、RenderGraphについては全く知りませんでした。 Unity6000.0.0bを数字のデカさに爆笑しながらインストールしていつも通りURPで開いてみたら何やら見慣れないコードが見えたため、一転泣きながら入門しようという状態です。

そんな姿勢で書いていくので、従来のURPのカスタマイズの知識はあると良いです。
RenderGraphは既存の知識に追加で知るべきものなので、従来のURPの勉強は全く無駄にならないと思います)

RenderGraphとは

Unity特有の概念ではありません。以下の記事はUEの公式マニュアルですが、URPのRenderGraphと同じようなことを言っています。

docs.unrealengine.com

従来のレンダリングパイプラインのカスタマイズにおいては、各カスタムパスで自由に処理を記述することができました。
自由さそれ自体に問題は無いのですが、複数のパスで計算途中にバッファとして使用するテクスチャが存在しているなどの場合、バッファを使いまわすためにはプログラマが気を付けて設計する他ありませんでした。

描画とは無関係に、私のフレームワークやライブラリの実装経験から至った考えなのですが、パフォーマンスの観点において、「中央集権的な管理コアモジュールを用意して、利用者は知らないうちにそれを必ず利用している。そして管理コアモジュールが全体を見て最適化を行う」というのは非常に実装しやすい割に効果的です。簡単な例を挙げると、オブジェクトプールなどは中央集権的な管理コアモジュールが生成破棄のコストを削減していますね。
抽象的に見るとRenderGraphの最適化はそれと同じと思ってよいでしょう。

RenderGraphは何がしたいのか

先ほどのオブジェクトプールの例のように、「パイプライン全体で使用されるリソースを管理して、良い感じに使いまわしたり、無駄を無くしたい!」というのが目的です。RenderGraphの中身を読む前に、一般的にこのような要件を求められたらどう実装したいかを考えてみましょう。

  1. 描画処理の前に、このフレームでどんなリソースを使うのかを知りたい
  2. カスタムパスで使用する型には制限をかけたくない
  3. カスタムパスの処理内容には制限をかけたくない

この辺が要件定義時に真っ先に出てくることと思います。(どんなフレームワークを作っていても割と似たような要件が出てくると思う)

そして、これらを満たすための最低条件として、ユーザには「処理の実行」ではなく「処理の登録」というモチベーションでコードを書いてもらいたいですね。

「処理の実行」ではなく「処理の登録」というモチベーション

RenderGraphによるPassの処理では以下のような記述が登場します。

public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
    using var builder = renderGraph.AddUnsafePass<PassData>(_passName, out var passData);
        
    builder.SetRenderFunc((PassData data, UnsafeGraphContext context) =>
    {
        using (new ProfilingScope(context.cmd, profilingSampler))
        {
            // 描画処理
        }
    });
}

従来のExecuteメソッドの代わりがこのRecordRenderGraphメソッドです。名前からして「処理の実行」ではなく「処理の登録」ですね。

このフレームでどんなリソースを使うのか知りたい

先ほどのコードにPassDataという型が出てきましたが、これは利用者である我々が勝手に宣言していいものです。そのためジェネリックの型引数に入れることでURPフレームワークに教えてあげる必要があります。 勝手に宣言するものなので名前も宣言場所もなんでもいいのですが、要するにパスで使用するデータ、なので内部クラスとしてPassDataと名付けることが一般的なようです。

public class CustomPass : ScriptableRenderPass
{
    private class PassData
    {
        public Material PostEffectMaterial;
    }

    public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
    {
        using var builder = renderGraph.AddUnsafePass<PassData>(_passName, out var passData);
        
        // builderがジェネリックなので、我々が定義したPassDataが渡ってくる
        builder.SetRenderFunc((PassData data, UnsafeGraphContext context) =>
        {
            using (new ProfilingScope(context.cmd, profilingSampler))
            {
                // 描画処理
            }
        });
    }
}

描画処理には制限をかけたくない

リソースの宣言には様々なルールを設けていますが、処理の実行はメソッドを普通に記述してデリゲートで渡すので、ほとんど制限はありません。

public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
    using var builder = renderGraph.AddUnsafePass<PassData>(_passName, out var passData);
        
    builder.SetRenderFunc((PassData data, UnsafeGraphContext context) =>
    {
        using (new ProfilingScope(context.cmd, profilingSampler))
        {
            // 描画処理。従来のExecuteに書いていた、例えばBlitなどはここに書くことになる。
        }
    });
}

終わり

パッと見これまでと全然違うような気がするRenderGraphですが、RenderGraphがやりたいことを考えれば結構素直な設計をしていますね。まぁ今回書かなかった部分で、フレームワークとしてかなり許せないポイントがあるのですが……それは番外編としてまた書きます。

参考

zenn.dev

ゲーム開発においてDIコンテナライブラリは別に正解でもなんでもないという話

DIコンテナの持つ2要素

DIコンテナと呼ばれるライブラリの多くは、「スコープ管理」と「依存性注入」の2つの役割を持っています。これらは同時に利用されることも多いですが、概念としては異なります。

スコープ管理

Unityでの開発であれば、あるシーン内でのみ使用し、そのシーンから抜けたら消えてほしいインスタンスは、そのシーンが「スコープ」となり、プロジェクトがコンテナの活用を基本とした設計になっているならば、シーンにコンテナが1つ用意されており、そのコンテナにインスタンスを登録することになります。コンテナには子コンテナがツリー状に存在できます。

DIコンテナライブラリを利用していなくても、このスコープ管理は非常に重要なので、何らかの手段で必ず実装しておくべきものです。Disposableを活用しましょう。

依存性注入

要するにインスタンスの生成を実際にはDIコンテナが行うことで、依存関係の解決を行いやすくしようぜくらいの話です。これはあると便利なケースは僅かにありますが、別に必須ではありません。単体テストが実質不可能なところが多いゲーム開発では尚更重要度は低いです。

依存性注入が解決できるほとんど唯一の問題

以下のような依存関係を持つクラスA,B,C,Dがあったとします。A,B,C,Dで一つのモジュールを形成していると思ってください。

graph TD;
A-->B
A-->C
B-->D

さらに、DのインスタンスはこのA,B,Cから成るシステムの外部から渡されるとすると、その受け口はAになるでしょう。
ということは、Aは「BにDを渡すために」Dに依存する必要が出てきます。

graph TD;

subgraph module
A-->B
A-->C
B-->D
A-->|Bに渡すために依存|D
end

E[D]-->|渡される|module

これは嫌ですね。Aは要件だけを見ればDを知らなくて済むはずなのに、Bに渡すという実装上の都合のせいでDへの依存が増えました。
こういう依存を無くしてくれるのがDIです。Bに直接Dを渡す神がいればAはDを知らずに済み、例えばBを、Dなんて必要としないB'に置き換えることも容易です。

もちろんDIコンテナを使わなくても実装できますし、やっていることは何も難しくないです。

DIを使えば綺麗になるということはありません。普通に依存関係をシンプルにしたうえで、深いモジュール構造における依存の受け渡しが邪魔になってしまうときに初めてDIを検討する、くらいでよいし、さらにはそのためにDIコンテナライブラリを導入する必要もありません。

参考

www.ulsystems.co.jp

利用コストを理解した上で、DIを利用するようにしましょう。DIが必要とされるのは、通常疎結合が必要とされるところです。そして、アプリケーション内で疎結合が必要とされる箇所はごく限られています。そのため、DIコンテナに登録されるべきクラスはアプリケーション内のごく一部のクラスのはずです。アプリケーションの規模に比べてDIコンテナに登録されているクラス数が多い場合は、本来登録されるべきではないクラスが登録されてしまっている可能性があります。そのようなときは、本当に必要なところでのみDIが利用されているかどうか、見直してください。

Tiny Character ComponentでUnityもUEくらいハードル下がったのでは!?感

github.com

Unity公式で、なんか色々入ったパッケージのリポジトリが公開されました。

恐らく今のところ、プロジェクト単位でリポジトリを落としてくるしか入手方法が無い?ようです。

サンプルシーンや機能のチュートリアルがとても丁寧なので、1から順番に触ってみたんですが、

これUnreal EngineのCharacterMovementComponentだ!!

というのが最初の感想でした。
それだけでなく、 リポジトリの名前はCharacterControlでCCなのですが、キャラ操作以外にも便利なサンプルが大量に入っており、そのまま使えそうだし、勉強にもなります。

空間内のキャラに話しかけてイベント発生、みたいなサンプルも

とりあえずこんなページ読んでないで早くリポジトリクローンして手元で動かしましょう。

Unity or Unreal Engine

世間ではUnreal Engineの方が難易度が高いと思われがちな空気が何故かあると思ってて、恐らく理由はデフォルトのグラフィック設定が高度だからだと思うんですけど。そんな世間の印象とは裏腹に、個人的には間違いなくUnityの方が難易度が高いと思っており……

最大の理由は、Unityってただエンジンをインストールしただけでは中身がスカスカで、すぐに何かを動かすってことができないんですよね。グラフィックの設定もシンプルだし。デフォルトアセットなんて気の利いたものは無い。

それに対してUEのテンプレートを(空っぽ以外)どれでもいいから開いてもらうとわかるんですが、ちょっとしたアクションゲームに既になっています。

そんなデフォルトアセットの貧弱さが大きな弱点だったUnityが、キャラ操作なんていう一番ゲーム作ってる感ある部分を高いクオリティで用意してくれるだけでとても大きな意味があります。設計も上手くできており、また(見た感じ)オープンソースなので、もし物足りなければ自分で改造もできそうです。オープンソースなのはいいですね、Unityさん。C++部分の公開まだですか。

感想

マジで損しないので、是非触ってみましょう!
僕も苦手な分野とかいっぱいあるので(VisualScriptingにそもそも慣れていない)、中身読んで勉強します。

MVPアーキテクチャ誤解あるある

特にUnity界隈を見ていると一生擦られているMVPパターン。
確かに決して悪いものではないのですが、別に最強の設計パターンじゃないし、なにより、適切に適用できずもっっっっっのすごく面倒なことになっているケースをよく見ます。

シーンレベルでPresenterを1つ用意すればいいという誤解

ぶっちぎりで多いのがこれだと思います。これをしてしまうとPresenterやルートとなるModel、Viewの肥大化は避けられません。

MVPパターンに限らずなのですが、アーキテクチャは別にプロジェクト全体で同じものを使う必要は無く、閉じたモジュール内で使えばよいのです。
そして、モジュール同士の関係はシンプルなツリー状になっていればそれでよい。

ナントカパターンみたいなのはカッコよさげで真似したくなるのはわかるし、大人数での開発において、多少具体的な設計思想が存在するのは意思疎通の面で確かにメリットはあります。

でも、一番大事なのは単一責任原則と、依存関係の方向を一方向にすること、依存を単純にすること、というシンプルな原則なので、それを忘れないようにしましょう。切実に。

効率的な開発に欠かせないゲームデバッグ機能の重要性

ゲームに限らず、プログラミングによって作成されるアプリケーションの開発において、デバッグ機能を丁寧に開発しておくことは非常に重要です。

(例外として、1人で1ヶ月以内とか短期間開発し、完成したら二度とメンテナンスしないようなものは気にしなくていいです)

デバッグ機能のすゝめ

どんなデバッグ機能を作ればいいのかというと、理想は、ゲームプレイにおいて変更されうる全ての状態を監視、変更する機能です。
将来的にどんなバグが起きるかわからないので、これができればまぁ理想です。 実際のところこれを実現しようとするとC#で言えばSource Generatorを使うなどコード生成に頼る必要があるでしょうし、 小規模の開発であればそこまでの頑張りに見合うリターンがあるかと言われると、ちょっとやりすぎな感じはありますね。

そこまで徹底しない場合は、初めからデバッグ機能を用意するのではなく、後から気軽に、リリース環境に影響を与えずに追加できる状態にしておくのが良いです。

デバッグ機能は割と何をしてもいい

デバッグ機能は大して設計を気にしなくても構いません。「デバッグシンボルが有効な場合はpublic staticなアクセスが可能」とかもやっていいと思います。

↓くらい自由でもいいと思います。

public class HogeClass : IDisposable
{
    public HogeClass()
    {
# DEBUG
        Debugackdoor = this;
#endif
    }

    public void Dispose()
    {
# DEBUG
        Debugackdoor = null;
#endif
    }
# DEBUG
    public static HogeClass DebugBackdoor;
    
    // その他、デバッグ機能時にアクセス可能な色々なメソッドやプロパティ
#endif
}

見栄えが悪い感はありますが、まぁこの辺実際どう実装するかは些事でしょう。

デバッグ機能があると、気軽に作れると何故嬉しいのか

バグ修正が簡単になる

当然ですが、バグの調査と修正が格段に楽になります。最初から見通しが立っていない実装や修正の重要なところは、基本的に情報集めです。
必要な情報が無いから難しく感じるのであって、必要な情報さえ集まっていれば誰でも対応は可能です。

というわけで、気軽に色々試せる環境を作って、情報集めのコストが下がることは間違いなくバグ修正を楽にしてくれるでしょう。

クオリティアップに繋がる

創作物のクオリティアップには、試行錯誤の回数が不可欠です。
デバッグ機能が充実していると、部分部分の設定変更が容易になり、様々なものを高速に試すことができます。

設計が綺麗な状態に保たれる

デバッグ機能はpublic staticとか作って雑にしてもいいんだ、と書きましたが、実のところそれで上手くいく状態にする前段階では真っ当な設計がなされている必要があります。 どこのパラメータを変更したらどこに影響があるのか? デバッグ機能から変更することによって整合性が取れないモジュールが出ないか?(表示と内部パラメータがずれるとか)とか。

デバッグ機能が適切に追加できる状態は、単一責任原則をしっかり守ってモジュール分割するであったり、
プロパティ変更レイヤーを間に挟むであったり、あとモジュール間の依存の方向性を適切に一方向にするであったり、 そういった一般的に良いとされている設計を丁寧に実践することによって実現できます。

そのため、デバッグ機能を作りやすい状態は、そもそもバグが出にくい状態にもなっているのではないかな、と思います。

逆にデバッグ機能の危ういところ

デバッグ機能の開発は楽しいです。楽しいんですが、デバッグ機能ばっかり作っていても本質的な開発は進まないので、楽しさに引っ張られないようにしましょう。

まとめ

デバッグ機能は作り得です。作った方がむしろ開発が早く終わるまであるくらいには役立ちますし、デバッグ機能を充実させることも念頭において設計することでアプリケーションそのもののクオリティも向上します。 とはいえデバッグ機能の開発ばかりしても作業は進まないので、簡単に作成できる環境を維持し、適切に気軽に用意し活用するようにしましょう。

ちなみに、今回記事のタイトルはAIに考えてもらったんですが、意外と悪くないですね。(はてなブログにそういう機能が追加されていた)
AIの発達で開発が楽になるといいんですが、今回話題にしたような抽象的な戦略の部分はしばらくは人間の仕事なんだろうなぁと、ぼんやり思います。早く仕事を奪ってくれ。

【URP】CopyDepthPass

Unityにおいて、(深度バッファを使用する設定であれば)シェーダで_CameraDepthTextureと宣言することで深度バッファを参照することができます。

これはURPにおいて、CopyDepthPassと呼ばれるパスでレンダリング中の深度バッファを_CameraDepthTexureにコピーする処理と、シェーダに対してこの深度バッファを渡すようなSetGlobalTextureが呼ばれているためです。

実際にシーン中のメッシュなどを描画する際に深度を書きこんでいる先は_CameraDepthAttachmentというテクスチャなのですが、このパスのタイミングで_CameraDepthAttachmentから_CameraDepthTextureへのコピーが実行されます。

基本的には画面の描画は

  1. 不透明オブジェクトの描画
  2. 深度バッファの_CameraDepthTextureへのコピー
  3. 透明オブジェクトの描画

の順で実行され、3のシェーダで_CameraDepthTextureを使用するのが普通なので、普段は特に意識することは無いと思います。

でも、URPでオレオレカスタマイズをする際にはちゃんと意識するようにしましょう。
オレオレレンダリングの場合は透明不透明の垣根すら壊せてしまいますからね。

ちなみに、CopyDepthPassの実行タイミングはURPの設定インスタンスでちょっと変更できます。

また、CopyDepthPassのcsコードや使用しているシェーダは公開されているので、同じようなコードを書いてやれば深度情報を好きなように利用できます。

github.com

github.com

結局はレンダリングパイプラインなんてものは

  1. カメラ空間内のメッシュ情報をテクスチャに描きこむ
  2. テクスチャ自体を操作する

の組み合わせでしかないので、この辺のAPIが公開されているならばかなり自由に改造できることがわかりますね。

とはいえ使用するテクスチャが多くなりすぎないように管理することは重要です。RenderTextureは解像度次第ですがかなりメモリを食いますので。

FrameDebuggerでURPの基礎を学ぼう

FrameDebugger

FrameDebuggerは、1フレームの描画がどのように行われているかを調べるためのプロファイラです。

プロファイラなのでパフォーマンス調査のために使用するものですが、初学者だとよくわからない描画の全体像を掴むのにもピッタリだと思うので、描画周りがよくわからない場合は、サンプルプロジェクトを使って色々いじってみましょう。

この記事ではUnity2022.3.12f1で確認しています。

URPテンプレートを見てみる

ForwardRenderingは要するに、

参照するメッシュ、使うシェーダ、描きこむRenderTexture(RT)を決めて実行する処理を重ねるものです。

描きこむRenderTextureとしては色情報を入れるRTと、Zテストをしたりシェーダから利用したいために深度情報を入れるためのRTの2種類を使用できます。

描画パスを自作する際にはCommandBuffer.SetRenderTargetというAPIで、描き込む先のRTを選択することができます。 どこに上の画像の場合、色情報は_CameraColorAttachmentAと名付けられたRTに描き込まれています。また、RT0と書かれたプルダウンから、深度情報に関しても確認でき、_CameraDepthAttachmentと名付けられたRTに描き込まれているようです。

よく見るとRTの名前には_CameraColorAttachmentA_1920x1080_B10G11R11_UFloatPack32_Tex2D_MSAA2xと、なんかRTのピクセル数とかの情報が書かれていますね。

自作のパスではCommandBuffer.GetTemporaryRTなどのAPIを用いて、自由にRTを取得できるのですが、使用メモリの観点で、できる限りRTの数は抑えたいです。 仮に同じIDでRTを取得しても、ピクセル数やColorFormatのような設定が異なる場合は実体として別物のRTが取得されてしまうので、同じRTを利用して使いまわせるよう、ここの設定をちゃんと見るようにしましょう。

また、メモリプロファイラを見た際にRTが結構なメモリを使用していることを見かけることがあると思いますが、URPの実体を理解して、本当に必要なRTだけが使われているかをチェックしましょう。

他にもFrameDebuggerにはたくさん情報があるので、サンプルプロジェクトをいじりながら一つ一つの要素を見てみると良いです。すごく勉強になると思います。(僕は勉強になりました)