シェーダー本4に向けてSRP(LWRP=UniversalRP)の勉強を始めています(ちなみに来春予定です。直近の技術書典には間に合わないですごめんなさい)。
今回はSRP(LWRP=UniversalRP)で採用されたシングルPassフォワードレンダリングについて
SRPとはなにか(レンダリングパイプラインとはなにか)
SRP(Scriptable Render Pipeline)はUnity2018.1から提供が始まったPackageで、今後Unityの基幹となるPackageの一つです。
SRPはレンダリングパイプラインを構築するための機能を持っています。レンダリングパイプラインというのは、描画に必要なリソースや命令(シェーダーコードも含みます)を処理してGPUに送信する、CPU側の(つまりC#で書かれた)処理実装の事です。
これまでのUnityでは、レンダリングパイプラインはエンジンに組み込みの状態で提供されていました(公式では「Unity Built-in render pipeline.(Unity組み込みレンダーパイプライン)」と言います)。Unity組み込みレンダーパイプラインはとても優れた設計でしたが、プラットフォームの多様化と性能向上に追従する為に内部実装が複雑化し、最適化が難しくなっていました(←これは土屋の想像で、実際にそういう発表があったわけではありません)。
レンダリングパイプラインは一度実装すればそれで終わりというわけではありません。例えば、モバイルのように性能が異なる様々な種類のGPUをカバーしなければならないプラットフォームと、コンシューマ機のように単一の超高性能GPU上での動作を前提にできるプラットフォームとでは、それぞれ最適な実装が異なります。これは「CPUとGPUを如何に協調させて描画を行うか」という、かなり低レイヤでの実装の問題である為、シェーダーコードを差し替えるだけでは根本的は解決になりません。
また、リアルタイムレンダリング技術は現在も発展途上にあり、GPUの速度向上や機能追加に応じて、既存のレンダリングパイプラインの改良案や、全く新しい設計が日々提案されています。この時、場合によってはレンダリングパイプラインを一から実装し直す必要があります。しかし、これまでのUnityではユーザーがレンダリングパイプラインを構築し直す方法が用意されていませんでした。
そこで、Unityでは、レンダリングパイプラインを構築する仕組みを本体から切り離し、SRP(Scriptable Render Pipeline. 「プログラム可能なレンダーパイプライン」くらいの意味)としてPackage化しました。ユーザーはSRPを使って自身のプロジェクトに最適化したレンダリングパイプラインを構築できるようになったのです。
予想ですが、今後はUnity組み込みレンダーパイプラインは徐々にクローズしてゆき、最終的にはSRPに一本化されるだろうと思います。
LWRP(UniversalRP)とはなにか。
SRPはあくまでレンダリングパイプラインを構築するためのPackageであり、単体では機能しません。また、レンダリングパイプラインの実装は膨大で、スクラッチから実装するのは現実的ではありません。そこでUnityでは、SRPで構築した「LWRP」と「HDRP」という2種類のレンダリングパイプラインをPackageで提供しています。
「LWRP(Light weight render pipline)」は「軽量レンダーパイプライン」という意味で、効率が求められるプラットフォームでの使用を想定しています。とはいえ、LWRPは、スペックで言えば、Unity組み込みレンダーパイプラインとほぼ同等の機能を持っています。ほぼStandardシェーダーを再現できる能力があり、Unity組み込みレンダーパイプラインで提供されていた各種組み込みシェーダーに相当するシェーダーコードが提供されています(完全に互換性があるわけではありません)。
LWRPはUnity2019.3から改名して"UniversalRP(Universal Render Pipeline)"という名称になりました。想像ですが"Light Weight(軽量)"という名称が「スペックの低いプラットフォーム向け」と誤解されるのを避ける為なのかなと思います。あるいは、Unity2019.3から使用出来るライトの個数が増えたので、「軽量」の看板を下ろす事にしたのかもしれません。
いずれにせよ、今後Unity組み込みレンダーパイプラインからSRPに移行する場合、UniversalRPを採用するのが基本スタンスになると思います。ちなみに、「HDRP(High Definition Render Pipeline)」は、コンピュートシェーダーをサポートしているプラットフォームを対象とした、かなりリッチな描画を目的としたレンダリングパイプラインです。AAAタイトルの開発では、こちらが採用されるでしょう。また、UniversalRP/HDRPはコードが全て公開されているので、ユーザーはこれらをカスタマイズして独自のレンダリングパイプラインを構築することができます。
シングルPassフォワードレンダリングとはなにか
さてようやく本題(長かった……)。UniversalRP(旧LWRP。面倒なので今後はUniversalRPで統一します)では、レンダリングパイプラインの仕組みとして「シングルPassフォワードレンダリング」が採用されています。Unity組み込みレンダーパイプラインでは、仕組みとして「フォワードレンダリング」が採用されていました。この"シングルPass"とはなんなのでしょうか?
以下、「シングルPassフォワードレンダリング」の解説です。「フォワードレンダリング」についての知識を前提としているので、そちらを先に知りたい方は拙著「Unityシェーダプログラミングの教科書」シリーズをどうぞ。
booth.pm
フォワードレンダリング
まず、フォワードレンダリングではどのようにPassが実行されていたのかを確認します。
Standardシェーダーをはじめとした一般的なシェーダーには以下のPassが実装されていました。
- ForwaredBase
- ForwaredAdd
- ShadowCaster
- META
ShadowCasterはシャドウを描画する為に実行するPass、METAはGIのベイク時に実行されるPassです。この2つのPassは今回関係ありません。
ForwaredBaseはオブジェクトを照らすメインライト(通常はディレクショナルライト)のレンダリング時に実行されます。ForwaredAddはオブジェクトを照らすメインライト以外のサブライトのレンダリング時に実行されます(適用するライトの個数は設定で制御します)。
ForwaredBaseおよびForwaredAddでは、オブジェクトの頂点単位で頂点シェーダーメソッドが、フラグメント単位でフラグメントシェーダーメソッドが実行されます。頂点の個数は常に一定ですが、フラグメントの個数は、そのオブジェクトとカメラとの距離によって大きく増減します。
ForwaredAddのフラグメントシェーダーメソッドは、サブライト単位で実行されるため、サブライトが増えると、(乱暴に言えば)処理コストが等倍で増えてしまいます。この負荷は非常に高く、通常フォワードレンダリングでは、オブジェクトごとに照らすサブライトは4個前後に制限されます。
シングルPassフォワードレンダリング
次に、シングルPassフォワードレンダリングで実行されるPassを見ていきます。
- UniversalForward
- ShadowCaster
- META
ShadowCaster/META Passについては以前と同じです。
シングルPassフォワードレンダリングでは、UniversalForward Passのみが用意され、メインライト/サブライトで実行されるPassが分かれていません。また、ライトの個数に関係なく、Passのフラグメントシェーダーメソッドの実行回数はフラグメントの個数分になります。正しい例えではありませんが、フォワードレンダリングでのForwardBaseだけが呼ばれる、と言えます。"シングルPass"という呼称はここから来ているのだと思われます(ちなみに、以前のフォワードレンダリングは対応して「マルチPass」と呼ばれます)。
では、何故そんなことが可能なのかというと、これは簡単な話で、フラグメントシェーダーメソッドの中で、処理するライトの数だけForループを回しているからです(正確には、メインライトを処理した後、サブライトをForループで回している)。各ループで演算されるカラー値を合算し、フレームバッファに書き込むことで、複数のライトの処理を1回のフラグメントシェーダーメソッド呼び出しで解決します。このループ処理が必要なため、シングルPassはマルチPassと比べてシェーダーコードが若干複雑になっています。
マルチPassと比較して、シングルPassを採用するメリットは、もちろんフレームバッファへの書き込みが1回で済むという点が大きいと思われますが、他にもドローコールの削減が期待できるというのがあります。
ドローコールというのは、CPUからGPUに対して実行する描画命令(と、それに付随するデータ)の事を指します。一般にドローコールの回数が増えると処理負荷に直結するため、1フレーム中に発生するドローコールの回数を如何に少なくするかがチューニング要素とされます。
さて、通常は、ドローコールはPass単位で発生します(Unity側で複数のドローコールを機械的にまとめる機能もある)。そのため、フォワードレンダリングでは、その構造上、サブライトの数に応じてドローコールが増えてしまっていました。
もうお分かりかと思いますが、シングルPassフォワードレンダリングでは、ライトの個数に関わらず、1個のPassのフラグメントシェーダーメソッドの中でForループを回して処理するため、ドローコールは1回で済むわけです(本当に1回なのかは知らないけど、フォワードレンダリングよりも少ない回数になることは期待できる)。これは地味に効いてくる効率化ではないかと感じます。
余談:遅延レンダリングについて
フォワードレンダリング(シングルPass含む)では、オブジェクトのフラグメント単位でフラグメントシェーダーメソッドが実行されますが、スクリーン上でよりカメラ側にオブジェクトがあった場合、そのオブジェクトにスクリーン上のピクセルが上書きされ、処理が無駄になってしまいます(早期深度テストで弾けるのは一部です)。
この問題に対処した遅延レンダリング(Deferred Rendering/Deferred Shadingとも)という手法があります。遅延レンダリングはG-Bufferと呼ばれる中間バッファに必要な情報を格納することで、ライトの計算処理を最低限に抑える事ができます。
遅延レンダリングは、ビルトインレンダーパイプラインでは使用できます(ただし、特殊な方法で実装されているためか、土屋は使いこなしているケースを見た事がありません)。
SRPではどうかと言いますと、これを書いている時点ではUniversalRPでは使用できません。Unity2019.3での実装を目指しているとの事ですが、間に合うかな……? という印象(2019/9/27追記。遅延レンダリングとシャドウマスク対応は2020.1にに延期されました)。ちなみに、HDRPでは遅延レンダリングが選択可能です。
終わりに
……「シングルPassフォワードレンダリング」について説明するために必要な文章量がこんだけあるってヤバくね? 今後もこんな感じで書いていきますので応援よろしくです。