リアルタイムライトをたくさん使いたい
VRCLVはVRC固有のものですが、そういったライトを代替する仕組みを用いて対処するという考え方自体はUnityBRP一般にもいえるので、両方のタグをつけておきます。
なおUnity URPやHDRP、UEではBRPに比べ多くのリアルタイムライトを扱ったりリアルタイムGIを扱ったりするための仕組みが提供されているので、ここで紹介しているものとは考え方が大きく異なります。
新しい仕組みが使えない一方で、自前でスクリプトやシェーダーを書いてライティングシステムを作り負荷軽減しつつルックを担保するというのはフォワードレンダリングそのものへの理解が深まって良いという側面もあると思います。あるといいな。
基本的には重要度の低い光源ではライトを代替する仕組み(以下代替ライトとします)を用い、演者周辺など重要度の高い光源ではUnity標準のリアルタイムライトを用いる形が多いかなと思います。
ひたすら代替ライトに置き換えるだけでは見た目の品質が落ちてしまうため、重要度が高いところでリアルタイムライトを使うための余力を確保するようなイメージでその他の最適化を行うことが大切…と私は考えています。
リアルタイムライト自体を軽くする方法として、描画負荷を軽減するもあわせて参照してください。 代替ライトについては現状以下が主な選択肢と考えています。代替ライト以外のアプローチも最後に紹介します。
VRCLVのPointLightVolumeを使う
DepthによるWorld座標復元を利用したフェイクライトを作る
ShaderGlobal変数を利用して自作の代替ライトを作る
基本的にはバッチ数を減らすためForwardAddパスを呼ばずに追加のライトを処理するための方法を考えていくことになります。
当然バッチ数が少なければそれで良いという話ではなく、ForwardBaseでの処理もやりすぎるとGPU側の負荷が高くなるため落としどころは探る必要があります。
ForwardBaseやForwardAddとは何か…というのはシェーダー一般の話なのでここでは省略します。
それぞれの選択肢について説明します。
VRCLVのPointLightVolume(PLV)を使う 2025年に突然登場した最も楽な方法です。
lilToonおよび主要な環境シェーダーが対応しているので、その見た目で十分であればシェーダーを書かなくて良いのが利点です。
その見た目で十分でないことはまあまああると思います。PLVの光をどう評価して見た目に反映するかという部分は各シェーダーで異なり、それをコントロールしたい場合はシェーダーの改造が必要になります。例えばlilToonはPLVによるリムライトの調整幅があまり大きくないので、改造する箇所の候補になると思います。
PointLightVolumeも例に漏れずUdonであるためキーフレームが打てず、タイムラインから操作するためには迂回策を講じる必要があります。つまり、タイムラインで扱う場合は言うほど楽ではないです。
PointLightVolumeはそもそもそんなに軽くないので、使う場合はそれなりに個数に注意する必要があります(上限の128個はかなり多い)
Max Additive Overdrawの数までしか同じピクセルに影響できません。これは明るい順にソートされたりするわけではないので、たくさんのスポットライトを1点に集めるような動きをさせた場合は不自然な見た目になることがあります。Max Additive Overdrawを増やせば解消しますがその分シェーダー内でのループ回数が増え、GPU側の負荷が高くなります。この辺りはシーン全体の負荷を見ながら調整や妥協をする必要があります。
リアルタイムライトとの差異として、影を使うことはできません。
表現側としては影なしリアルタイムライトがたくさん置けるようになる、という理解でほぼ良いと思います。
DepthによるWorld座標復元を利用したフェイクライトを作る
表現は割と縛られますが、実装難度、負荷、得られる見た目への寄与のバランスが良いです。
DepthTextureからワールド座標を復元し、シェーダーのグローバル変数やマテリアルプロパティで与えらえた光源位置との距離から明るさを計算します。
以下の記事で詳細を解説しています。
ワールド座標を復元しただけの場合、法線の情報が無いので光の反対側の面も明るく、サブサーフェススキャッタリングっぽくなります。
それでも良いケースは多いです(特にトゥーン寄りなキャラクター)
GrabPassと組み合わせることで元の色にライトが当たっているような計算にすることができますが、元の色とは通常の光源によるライティング後の色である点に注意が必要です。
Albedo × LightColor ではなくFinalColor × LightColorであるということです。
より具体的には元々真っ黒な部分を照らせない、とかです。
単にライトの色を加算する計算にすればその点は回避できますが、物体の色が入らないのでライトらしさは減ります。
更にワールド法線を復元する方法もありますが、だいぶ高負荷になる割に綺麗に出すことが難しいため、そこまでするなら他のアプローチの方が良いかもしれません。
単に近傍ピクセルから傾きを取るだけではほとんど使い物にならない精度になってしまいます。
それでもできるだけ綺麗に法線を復元したい場合はこの辺りが参考になります。単純なライティング以外の用途でも使えることはあると思います。
VRCを用いたVRC外のレンダリング(?)ですが、試したことはあります。
このときはオンラインVJなので圧縮され画質が悪くなることがわかっていたため、逆に採用できる可能性がありました。
ShaderGlobal変数を利用して自作の代替ライトを作る
VRCLV以前から行われていますが、通常のライト的な挙動であれば現在はVRCLVに乗る方が楽だと思います。
その仕組みに依らないシェーディングを行える利点は何かしらあるかもしれません。
より難易度は高いですが、代替ライトを用意するというより、リアルタイムGI(大域照明)の仕組みを用意するようなアプローチがあります。
VRCLVを改造してSHを操作する
VRCLVはPLVを除くと本来動的なライトを扱う仕組みではありません。しかし、VRCLVが扱う静的な3Dテクスチャを動的な3DCRTに置き換えることでリアルタイムにSHの影響を変更することができます。
この方法は表現の幅が相当ありますが、一方で3DCRTの計算負荷が大きく、通常のLightVolumeに比べて範囲や解像度(Voxel Per Unit)が大幅に制限されるというデメリットがあります。
演出の空間が狭いほど使うチャンスといえそうです。
ベースの部分のサンプルをphi16さんが公開しています。
huwaさんがパーティクルを光源として扱った際のnoteが公開されています。
lilToonのlilさんがこの仕組みを使ってワールドを作った際の記事がFANBOXで公開されています。
疑似光源というより特殊なライティングですが、VRCLV改造の本質はむしろこちらだと思います。
ライティングを計算するより、SHに直接任意の値を書き込む方が軽く、また面白くなり得るという意味です。
光源があって光るのではなく、光ったところが光源となる形で良いというか。
私もコードの一部を公開したり、色々作ったりしました。
これはGPUパーティクルを光源として扱っているものです。
ワールドのリンクを貼っておきます。
https://scrapbox.io/files/69510c4296ae654e06a36f34.png
コードは公開しませんが、デカい平行投影カメラにSetReplacementShaderをつけてSH書き込み用のシェーダーに置換するという方法を取っています。SH書き込み用のシェーダーは3DCRTを2Dに開いた場合の書き込み先にジオメトリシェーダーで面を作ります。コンピュートシェーダーのRandomWriteをVRC環境で無理やりやっている感じです。重そうですが、3DCRTの全ピクセルで全光源分のループを回すよりずっと計算量が少ないため、より多くの光源を扱えます。
シェーダーのRenderTypeタグを変なのにしてCVSからCenterとColorとSizeを送ります。
規格を合わせればアバターのパーティクルやモデルからも書き込めます。
完全に自前でリアルタイムGIの仕組みを作る
VRCLVに乗らないパターンです。
基本的には、演出内容や地形等から光源に何らかの制約を課すことができるときに成立しやすい(リアルタイムで、許容できる負荷で、十分な見た目になる)と思います。
この領域はphi16さんが昔から取り組んでいて解説もたくさん公開してくれているので、参考になります。
多分もっとあります。