無を作りました よろしくおねがいします
— phi16 (@phi16_) 2021年5月26日
制作した「無」について解説します。
きっと殆どの人は気づいていない。何より気づかなくて良い。
ただ其処に在るだけの概念です。
光
結論から言います。
あのワールドでは Unity についてる Realtime GI 機能は使われておらず、動的光源による光量の計算を全て自前で行っています。
たまに見かける「ライトマップの寄与をシェーダ側で制御することでオンオフする」やり方 (ref 1, 2) を拡張したものにあたると思います。この辺やってたのは伏線ですね。
加えて、アバターのライティングの為に視界ジャックによる光量の直接的な調整を行っており、また Directional Light の色も動的に制御しています。
理由は幾らかあって。
- Realtime GI だと高速な点滅に対応できない
- 処理負荷が高いと稀に「光りっぱなしになる」現象が起きる
- アバターに影響する光量はシェーダによってまちまちなので統一させたい
- 本物の「黒」を作りたい
- 何よりも光るアバターを潰したい
- 光るな
加えて、光量計算の際に光源の情報を取り入れることで、単純なライトマップだけでは出せない「反射感」を出していたりもします。実はこれの話は伏線です。
概要
まずは単純な方から。
あの部屋にある16個のサイドランプは、見た目通り動きません。よってその寄与は「各々単体で焼いたLightmapに個別の明るさを乗算したものを全部足し合わせる」ことで計算が出来ると言えます。
つまりこうです。
対して正面にあるアレは「動く」のでどうしようもありません。そこで逆に諦めて、一枚の平面と捉えることにしました。
まず可動域を囲える3.4m×3.1mの矩形を次のように8分割して、それぞれに対するLightmapを生成します。
そして光る部分を前面からCameraで256x256のRenderTextureにキャプチャして、Level 6 の LoD (4x4) で各々の中央の点を (bilinearで) サンプリングするとこれは各領域の平均色になります。
というわけでこの色を係数として線形和を取れば、好きにアレを動かしたとしても概ねそれっぽく振る舞うようにできます。内部実装を知らなければ気づくことは無いと思います。
この分割は「天井に近い方は (壁に激しく映るので) 解像度が高い必要がある」「逆に壁から遠ければ曖昧で良い」という理由で選択しました。
また、加えてあの部屋には「VJ室」「キッチン」「通路」それぞれに光源があります (VJ室はローカルなので基本見えないです)。今回は「VJ室」と「通路」の光源に対するLightmapをこちらで処理していて、キッチンの光は「普通に焼いたもの」を利用する形になっています (一番ディティールが出るべき光源なので)。
ちなみに通路はドアが閉じると光が届かなくなるので、光が届かなくなります。自然すぎて気づかれないと思うんですけど。まず暗いんですけど。
ビル側にも同じことをやる必要があるわけですが、bake工程を独立にしたかったので「ビル側の床だけをこっちに持ってきてbake、それを本物のビルの上からAdditiveで合成」ということをやっています。
つまりあっちにも一応赤い光はちゃんと乗ってるんですが……めっっっちゃよく見ないとわかりません。私の作った中では一番気づかれないディティールだと思います。
反射
これでベースとなる「光の広がり」ができたわけですが、加えて壁に反射が欲しい気持ちになります。あのワールドは通常のbakeに Bakery の SH モード を使っていて、それがあのとてつもないディティール感に結構寄与しています。つまり反射は偉いのです。
でもこの自作のLightmapアトラスにそこまでの情報を含めるのは面倒だし、何よりサンプリング回数が少ないほうが嬉しいのです。
「きれいな反射が出る」ことと「光源の位置がわかっている」ことはほぼ同義です。今回は正面のモニタ群の座標やサイドランプの座標が明らかなので、それぞれ「いい感じに」やる余地がありました。
面光源をいい感じに計算する手法として Real-Time Polygonal-Light Shading with Linearly Transformed Cosines というものを聞いたことがあったので、やってみることにしました。
𝑹𝑻𝑿 𝑶𝑵 とか言ってはしゃいでました。ちなみにこれはroughnessの扱いがすごく適当で、BRDFの再現とかも一切やってないんですが、それっぽいのでOKということにしました。
キッチンにめちゃくちゃ綺麗にモニタ群が映るのはこれのせいです。本来はもうちょっと遮蔽されるはずですね。
さて、これを各サイドランプに適用してみたところとても重かったので良くないなとなりました。代わりに、サイドランプの光源はおおよそ「線分」であることを利用して、これを直線と解釈して計算できる擬似的な方向成分を使って指向性を出しています。いい感じです。
あ、ちなみに Realtime Reflection Probe (No Time Slicing) も置いてあります。デッキの反射はこっちです。
オーバーレイ
これまでの話は全てワールド側のマテリアルの話です。アバターには掛かりません。
アバターのライティングを行うために、「画面全体を一定の色で乗算するオーバーレイ」を作りました。基本的に光量は線形なので、乗算によって適切に色を乗せることができるはずです。
乗るべき色としては前面のモニタの平均色に加えて各サイドランプの明るさがあって、それらの重み付き平均でどうにかしています。重みは手で調節しました。
また、bake時にはこの場所はほぼ白色光源で囲まれた部屋になっているんですが、一応モニタ側が明るくなるように調節はしてあります。
あとアバターのみに掛かる Directional Light も同じ感じで計算しています。こちらはサイドランプ抑えめ、モニタ群強めになっていて、この2色でアバターのライティングを誤魔化しています。
意外と綺麗に出ます。
さて、オーバーレイは見境なく「全て」に掛けるのでこのままでは壁もさらに暗くなってしまいます。というわけで、ワールド側のシェーダには「最終計算された明るさを、オーバーレイの色で除算する」処理が入っています (Directional Lightの色にも入ってます)。これで相殺されて狙った色で出るというわけ。普通のEmissionも綺麗に出ます。
アバターのEmissionは全て潰れます。
そして全てのライトを消せば、全てが消えます。「無」です。これがやりたかった。
ただこれだと写真撮影時に困るということで一応の回避策も用意してあります。ご要望あればやり方伝えますのでこっそり聞いてください。
実動作
というわけで、クラブ部屋のライティングの工程は次のように行われます。
- サイドランプの光量がUdonによって決定、Texture2Dに記録
- モニタ群を前面から撮影して RenderTextureに保存
- これらを元にしてオーバーレイ色とDirectional Lightの色をシェーダで算出、カメラで撮影 (2x1)
- ReadPixelsでUdon側で読み取ってDirectional Lightに反映 (ref)
- SetPixelsでTexture2Dに記録
- Realtime Reflection Probeが6面分をキャプチャ (ここにオーバーレイ自体は含まれていない、除算は行う)
- ワールドとアバターを普通にレンダリング
- オーバーレイによって色が乗算
だいぶGPUに無理をさせているように見えますが、45fpsは出てるのでまぁいいんじゃないかな…。これまでの Realtime GI 分の負荷を考えればそう悪くはないはずです。
終わり
めちゃくちゃ大変です。bake工程も意味わからないくらい複雑になっちゃったし。まだ一部のbake結果おかしいし。データ受け渡しもしんどいし。
開発自体に時間がだいぶ掛かっているわけですが、実は途中成果として、ある時のコンクリでコレの一部分を使ったりもしました。気づいた方もいらっしゃいましたが、その完成形が今回の機構ということになります。
大変です。でもまぁ。その甲斐はあったのだとは思います。
あの良い空間の「良さ」を引き上げる成分になれていたら幸いです。
これからの 𝑮𝒉𝒐𝒔𝒕 𝑰𝒍𝒍𝒖𝒎𝒊𝒏𝒂𝒕𝒊𝒐𝒏 をよろしくお願いします。
ProjectCiAN 制作記に書かれていた FakeGI の「オリジナル」がコレということになるんでしょうけど、実物を見たとき、なんだかわたちゃんの純粋性を汚してしまった感じがあって複雑な気持ちでありました。というかそれでりふさんを恨んでいるという表現になっていたんですけど。
でもまぁ。そんなのは気にすることではなかったのかもしれない。我を持ったみなさん最高です。本当にありがとうございました。