土屋つかさの技術ブログは今か無しか

土屋つかさが主にプログラミングについて語るブログです。

Unityフォワードレンダリングにおけるライト割り当てルールまとめ

 下はUTS2を適用したモデルの周りを複数のポイントライトが周回する動画です。ライトの位置によって、モデルの色がパキッと割れるのが観察できる良いサンプルだと思います。このフリップは、Unityがプリミティブごとに適用するライトの種類と個数を自動的に設定していることによって起きています。

(元動画が削除されていたので省略)

 これを見てて「そういえばポイントライトの割り当てルールについてちゃんと調べてなかったな」と思いまして、これを機会に実機検証を合わせてまとめてみました。

導入

 フォワードレンダリングでは、あるプリミティブを照らしているライト1個ごとにPassが実行されます。これはつまり、ライトの個数が直接描画負荷に影響するという事です。なので、プリミティブについて処理されるライトの個数はできるだけ減らしたい(とはいえ、描画精度は下げたくない)という事情があります。

 Unityでは、このライトの個数について、この記事で紹介するようなポリシーに基づいて決定しています。簡単に言えば「近くにあるライトは正しく演算し、遠ざかるほど近似値で対応する」という形になります。しかし、どのライトを近似処理にするかのルールは(恐らく歴史的な経緯から)非常に複雑で、直感的に把握しにくい所があります。

 土屋のシェーダー本1(https://booth.pm/ja/items/660001)のP14〜P16でも、このルールについて解説しているのですが、ライティング関連の詳細を次巻以降に送った(3巻で解説予定です)のと、執筆当時は検証が浅かったのもあり、正しく把握するには記述が不足している点がいなめません。そこで、改めて、以下に詳細にまとめておきたいと思います。

 下記ドキュメントと並行して読むとわかりやすいかもしれません。

フォワードレンダリングパスの詳細
https://docs.unity3d.com/ja/current/Manual/RenderTech-ForwardRendering.html

フォワードレンダリングにおけるポイントライト割り当てのルール

※以下「ライトの明るさ」という言葉が頻出しますが、これはあるライトに設定された強度(Intensity)と、そのライトとプリミティブとの距離(もしかしたら範囲(Range)も)から算出される物のようですが、実際の計算式は不明です(もしかしたら減衰量(attenuation)順とかなのかも)。←Unityではライトの減衰量(attenuation)は、強度(Intensity)とオブジェクトまでの距離によってのみ計算され、範囲(range)は使用されないようです。

1・一番明るいディレクショナルライトは自動的にForwardBase Pass扱いになる

 ForwardBase Passは常に1回しか実行されず、実行される対象は常にディレクショナルライトになります。
 確認した限りでは、ディレクショナルライトがオフになっている、あるいはシーンにディレクショナルライトが無い場合も、ForwardBase Passは実行されているようです(これについては後述します)。

2・ポイント/スポットライトと2番目以降のディレクショナルライトは、ピクセル単位ライトの候補となる。

 一番明るいディレクショナルライト以外のライトは、ピクセル単位ライトの候補になります。

 ピクセル単位ライトというのは、おおざっぱに言うとForwardAdd Passで処理されるライトのことです。ただし、実際にそれらのライトがForwardAdd Passで実行されるかどうかは、3・4・5のルールに従います。
 なお、2つ目以降のディレクショナルライトは、ポイント/スポットライトと同等に扱われます。

3・RenderModeがImportantに設定されているライトは、必ずピクセル単位ライトとなり、ForwardAdd Passを実行する

 LightインスペクタのRender Mode(デフォルトはAuto)をImportantに設定すると、そのライトはForwardAdd Passを実行します。

 以下は、1枚のプリミティブ上(Quadの半分の3角ポリゴン)に10個のライトを
配置した物です。デフォルトで有効になるライトは3つだけです。これは、ディレクショナルライトの有無と関係ありません。

 10個のライトをすべてImportant設定に変更しました。すると、10個のライトすべてが1枚のプリミティブ上に描画されます。

 Unityドキュメントでは、車のヘッドライトなど、常に点灯している必要があるライトをImportant設定するように推奨しています。

4・RenderModeがNot Importantに設定されているライトは、必ず頂点単位ライトとなり、ForwardAdd Passでは実行されない

 逆にRenderModeをNot Importantに設定したライトは強制的に頂点単位ライトとして扱われ、ForwardAdd Passでは実行されません。頂点単位ライトについては後述します。
 Not Important設定の使いどころがイマイチわからないのですが、動作環境に応じて処理負荷を軽減させたい場合に、優先順位の低いライトを実行時にNot Important設定にするのかなと思います。

5・ピクセル単位ライトの数がPoint Light Countより少ない場合、RenderModeにAutoが設定されているライトがピクセル単位ライトになる。

 Important指定されたピクセル単位ライトの個数がEdit>Project Settings>Quality>Pixel Light Count(デフォルトは3)よりも少ない場合、指定値に達するまで、Autoに指定されたライトの中で、プリミティブに対して明るい順にピクセル単位ライトに変わります。

 以下は、10個のライトをAutoにしたのち、Point Light Countを10に設定した場合です。

 Unityをデフォルト設定で使用した場合、LightコンポーネントのRender ModeはAuto、Point Light Countは3になっています。この状態でライトを配置すると、個々のプリミティブにたいしてForwardBase Passでディレクショナルライトが1個、ForwardAdd Passでポイントライトが最大で3個処理されることになります。

 余談ですが、Pixel Light Countについて、ドキュメントの記述をそのまま受け取ると、上記のような扱いになりません(ForwardBase Passに割り当てられたディレクショナルライトも数に含めているように読める)。ですが、実機で確認した限りでは、上記のような挙動になっています(まあ"Pixel Light Count"って書いてあるしな……)。

6・頂点単位ライトのうち4つが、ForwardBase Pass実行時に補間情報として処理される。

 頂点単位ライトに割り当てられたライト群は、文字通り頂点単位でのライト色に近似されます。ピクセル単位ライトと比べると再現度ががくっと落ちますが、遠い(あるいは暗い)ライトは近似しても影響がそこまで出ないとしています。

 頂点単位ライトのうち、プリミティブから明るい順に4個(←これは固定です)のライトの情報が、ForwardBase Passに渡されます。この処理があるため、ディレクショナルライトが無くてもForwardBase Passが実行されるのかと思われます。

 頂点単位ライトを反映させるには、ForwardBase Passで組み込み関数(unity_4LightPosX()/unity_4LightPosY()/unity_4LightPosZ()/unity_4LightAtten()/unity_LightColor)から4つライトの値を取得して、処理する必要があります(※土屋は実際には試してません。いずれやります。まあ、処理したとしても頂点単位ライトなので、そこまで強い効果は出なさそうですが)。

 以下は、4つのライトをNot Important設定にした物です。3角ポリゴンの各頂点が、その傍にあるライト色を反映しているのが分かるかと思います。Intencityを10倍にしているので色がついていますが、通常はここまで反映されないと思います(未確認)。

 この4つのライトは頂点単位ライトなので、以下のようにピクセル単位ライトも重ねて描画できます。

 ちなみに、実際には、ライトをプリミティブから明るい順に並べた場合、ピクセル単位ライトのうち一番暗いライトは、頂点単位ライトの一番明るいライトと重複して扱われます。これは急な色の変化を避けるために、ライトをなじませる処理のようです(実機では未確認)。

7・ピクセル単位にも頂点単位にもならなかったライトが、SHライトとして、ForwardBasePassで処理される。

 4個の頂点単位ライトよりも暗いライト群は集計され、SH(球面調和関数)という式の係数として1個にまとめられます。頂点単位ライトより更に精度が落ちますが、アンビエント光として使われることが多いようです。こちらもForwardBase Pass上でShadeSH9()関数を介して値を取得し、処理します(こちらも土屋が実際に確かめたわけではないです。いずれやります)。

 なお、6と同様に、頂点単位ライトの中で一番暗い物が、SHライトの一番明るいライトと重複して扱われます。

シェーダーからライトを制御する限界

 以上がポイントライト割当ルールになります。今回の記事で注記しておきたいのは、これらのポイントライトの扱いは、シェーダー側から制御が困難(というか実質不可能)という点です。

 Point Light Countはプロジェクト単位(まじか!)で設定される値で、シェーダーコードからは参照できません。スクリプト(QualitySettings.pixelLightCount)で変更はできます。

QualitySettings.pixelLightCount
https://docs.unity3d.com/ScriptReference/QualitySettings-pixelLightCount.html

 各ライトのRender ModeはLight側で設定されます。これもシェーダーコードからは参照できません。スクリプト(Light.renderMode)で設定できます。

Light.renderMode
https://docs.unity3d.com/ScriptReference/Light-renderMode.html

 ちなみにrenderModeプロパティに代入するのはLightRenderMode列挙体で、Auto/ForcePixel/ForceVertexから選択します(Important/Not Importantよりこっちの方がわかりやすいでしょうか?)

 これまで、シェーダーを記述する際、ライティングの設定はあらかじめ決まっていることが前提とされていました(というか、この先も基本的にはそうです)。しかし、VRChatのような「シェーダーがどのようなライト環境下で実行されるか事前に想定できない」場面が生まれ、状況は混沌としています。

 UTJ2は不断の努力でフレキシブルに動作するように改良を進められています。個人的には、VRChat(あるいは新規サービス)側で、シーンに配置するライティングについての基本ルールが整備されて欲しい所です。