お花畑

ワールドを作りました。技術寄りなお話を書きます。

きっかけ

元々は「オブジェクトを大量に描画するのに最も効率のよい方法は何か」の実験をしていました。目的はパーティクルを大量に出すためです。ぱっと思いつくのはGeometryShaderですが、Shader内ですべてをどうにかするのもそれはそれで大変なので、必要な3Dモデルを先に作っておいてGPUInstancingで大量描画する方法はどうだろうかと考え、なんやかんやと実験していました。

手を動かしながらふと、前に考えたことを思い出しました。こういうちょっとしたことから何かワールド作りのネタにならないかなあと。Boothを眺めているとこれを見つけました。

ネモフィラ。のらきゃっと好きには特別な花のひとつ。あの花畑を想起させるワールドや、いわゆる草原系のワールドは既に多く存在しますが、少し思うところもありました。板ポリにテクスチャを貼り付けてビルボードするもの。セルルックな世界観のわりにフォトリアルで微妙に浮いているもの。それはCGの長い歴史から(主に遠景向けとして)それが最適だと考えられているからで、あるいはAsset Storeにそういう素材ばかり売られているから、という理由だと思います(Physically Basedであることは一般的でしょうし)。納得がいきます。でもVRなので(?)、遠景想定であろうと近づいてよく観察できてしまうし、グルグル動くビルボードはちょっと気持ち悪いのです。
では、そういう事情を仕方なく受け入れるのではなく、ちゃんと抗ってみたいと思いました。それが同時に誰かへの贈り物になれば佳いなと願いながら。


実装

とはいえ、ワールド内に花の3Dモデルを敷き詰めるのは負荷的に現実的でないので、どうすれば現実的なラインに落とし込めるのかを考えます。
実際に花の3Dモデルが展開されるのはプレイヤーの近くだけで十分だろうと考えました。遠景となる部分は慣習通り板ポリになるようにすればよさそうです。UnityのLODっぽい挙動にも見えそう。
そういえば、以前 phi16 さんが配布していた雪パーティクルでは、カメラの位置を中心にパーティクルを展開する方法をとっていました。これと同じことをすれば、プレイヤーの周りだけ花が配置されるようになりそうです。ので、やりました。実質パーティクル芸の一種だと思っています。
問題は乱数生成のためのSeedですが、今回は頂点カラーを使いました。 Houdiniを使ってGridの各pointにランダムな頂点カラーを入れておき、copyノードで複製します。そうするとそれぞれの花がすべて異なる頂点カラーを持つことになります。

画像5

これだけだと謎物体ですね。ここに花1,400本分のモデルが収まっています。

今回はHoudiniを使っていますが、今ならblenderにもGeometry Nodeがあるので、似たようなことはできるのではないでしょうか。使ったことないので分かりませんが。

Seedにはこれに加えTransformが持っているモデルの向きベクトルも入れています。これで複製してもGPUInstancingが効きます。以下が参考記事です。

結果的には8個のfbxを置いたので、計11,200本の花が在る状態です。...たった10m四方を満たすのにこれだけの数が必要なんですね。それでもなお隙間はあるし、正直もっと増やしたいくらい。ポリゴン数にして1,400万越えです。聞いたことないですね。はい。これでも元の3Dモデルのポリ数をかなり削減(2,642tris→1,367tris)しています。

これでプレイヤーの周り(XZ平面)に花を展開できるようになったので、あとは地面に沿って配置できるようにします。地形のモデルをOrthographicカメラが表示した深度情報をテクスチャに保存して、そこからオブジェクトが配置されるべき高さを復元します。

画像5

画像4

center は花毎の中心座標、129.5とかは地面の大きさです。最後のはちょっとだけ埋めたかったから。これできれいに地面に沿って花が配置されます。

一般的にパーティクル芸をするにあたって注意するべきは、元のメッシュがカメラの視界内に収まっていないと視錐台カリングが働いてパーティクルが描画されなくなるということです。手っ取り早いのはSkinnedMeshRendererでBoundsを大きくすることですが、そうするとGPUInstancingが効かなくなります。次点はエディタ拡張でBoundsを大きくしたメッシュデータを生成できるんですが、今回はそうするとただでさえデータ容量が大きい3Dモデルが編集後には100MBを超えてしまったので却下となりました。ということで、Bounds稼ぎ用のポリゴンを6個入れてあります。

画像6

これはイメージで、実際はワールド全体を覆う程度のBoundsを確保しています。

あとは、見える花の総数を稼ぎたかったので、花が展開される中心を視線方向に伸ばしました。先の記事にあったカメラの視線方向ベクトルですね。VRで視線ベクトルを使う上で問題になるのは視差(Pimaxが顕著ですね)なんですが、これは両目の視線ベクトルの平均をとることで対応しました。最終的にはこう。

画像9

意外となんとかなるものです。こういうことをちゃんと指摘してくれる方がいるというのはありがたいことですね。以下Reference。

そうすると視線を動かしたときに繰り返しの境界付近にいる花がパチパチと表示非表示が切り替わってしまうのが気になったので、境界付近はFadeするようにしました。少しは馴染んだでしょうか。


遠景の板ポリの花はShrubberyShaderです。3Dモデルの花がプレイヤーの周りに展開されるので、こちらは逆にプレイヤーの周りのみ非表示にする処理を追加しています。テクスチャは元の3Dモデルをカメラで撮ってテクスチャに保存し、状況に応じてUVを動かして見た目の調整をしています。

画像12

また、東屋のある場所や木の生えている場所には花は配置されません。これもマスク画像を生成してそこには花を配置しない(ポリゴンを生成しない)処理を書いているだけです。そういえば地面もHoudini製ではありますが、単にマスク画像が作りやすかったというだけの理由で使ってます。

花の主な実装はだいたいそんな感じです。


いろいろなこと

この場所のモデルはご存じ国営ひたち海浜公園です。みはらしの丘のネモフィラを一度は生で観てみたいものです。ぽつんと生えている木と影が生み出すコントラストのおかげでなんだかいい感じです。

また、東屋もとあるゲームが元ネタで。過去に作ったワールドの没案を拾う形になりました。以前はいろいろあってできなかったので。
東屋等はmarupopさんにモデリングをお願いしました。

購入アセットを並べるだけだとモノの質感が統一できなくて悩んでいるという話を制作途中にしたところ、なんとモデリングを引き受けていただきました。自分のふわっともまとまっていないイメージをきちんと形にしていただきました。本当にありがとうございます。

景観の良さはSkyBoxの力が強いなーと思っています。いいAssetを教えてくださった落雷さん、ありがとうございます。
このワールドには昼、夕方、夜の3つの時間帯がありますが、ワールド内に最初の人がJoinしたタイミングで時間帯を決定し、以降固定されています。高々VRChatのワールドですし、インタラクトできるCubeか何かをわかりやすい場所に置いておいた方が実用面でも喜ばれると思います。でも、「世界に干渉するってそう簡単にできるものじゃなくない?」と、ふと考えてしまって。そうしたいなら説得力のある何かがなければならなかった。いろいろ考えた結果、無いとなったのでこうなりました。

一応、デバック用と称して隠しています。直接お会いした方々にはバラしていますが。まあ、緩やかに世界と向き合うことを推奨します。

ティーセットのギミックは、元はSDK2で組まれていたのを雰囲気で再現していますが、いくらか機能がありません。というか絶賛バグっております。Labに上げて10分後くらいには気づきましたが、原因が全然わからなくて未だに直せていません。だいぶつらい。いつか直します。はい。

今回は自前でテクスチャ等を生成することがいくらかあったので、必要な作業をほぼ自動化できるようにスクリプトを作ったことが個人的な努力点かなと思います。まあ見えるものではないのでアレなんですが。

今回はクレジットもワールド内に置いてあります。感謝と敬意を込めて。


ほしいですよね。私は在るべきだと思いました。

ひとまず影ありDirectionalLightを置きます。するとエディターが固まります。そうですね。
つまり花をRealtimeLightで落ち影まで描画するためには最低でも①通常の描画、②カメラから見た深度生成、③(落ち影の描画のための)ライトから見た深度生成、という計3回のPassが実行される(実際はたぶんもっと増える?)わけですが、それを計数千万ポリゴンのオブジェクトに対して実行すればどうなるか想像に難くないと思います。しかしながら写真撮影用にカメラ深度までは生成しなければならないということもあり、落ち影をどうにかする方法を考えます。

具体的に大変なのは花に対してあらゆる影を落とすことです。これは遠景の板ポリ花も含みます。
とはいえワールドに最初からあるものはどうにかしようがあります。ということで、まずは東屋と木。地面に落ちた影をテクスチャに保存して、それを真上から花に乗算します。そうすれば花にも影が落ちているように見えそうです。落ち影だけを受けるShaderを地面に適用して、真上からカメラで映してテクスチャに保存します。

画像10

東屋や木のMeshRendererでCast ShadowをShadowsOnlyにすれば落ち影だけを描画することができます。
地面に落ちてる花の影も同じやり方です。花は動くので、4枚撮って遷移させてる感じです。実はきれいに合っていません。

問題はアバターですが……えっと。つまりこれも影のみを撮影し、真上から乗算すればいいわけです。
まず、面の向きが反転した地面を置きます。そうするとアバターの落ち影が地面に映ります。これを真下からカメラで撮ります。

画像11

このカメラをアバターに追従させつつ落ち影を真下から映し、これを座標や向きを補正しながら花に乗算しています。そうしてアバターの影を再現しています。
問題は、この地面よりアバターが下にあるとき、カメラがアバター本体を映してしまうことです。上の画像でも靴がちょっと見えてますね。これがカメラに映ってしまうと、花より低い位置にあるアバターが影としてカメラに映ってしまいます。ということで、この反転した地面のShaderに2Pass目を用意し、ZTest Greaterで地面より下にあるオブジェクトを白く塗りつぶします。

画像12

画像11

たぶんこれ伝わらないと思うんですが。とりあえず願ったとおりに影の再現ができました。
とはいえ、カメラがベースなので自分の周りだけしか影が見えないのは少し残念ではありますが、自分が思いつく方法としては最適だと思いました。

また、普通の描画用と深度生成用に花のモデルとShaderを分けることができるので、深度生成用はさらにポリ数を削減してます。おしべの部分は花弁に囲まれているので厳密な深度は必要ないと判断し、この部分を削っています。ここまでやっても花だけで計2,500万ポリゴン以上。うん。

自前実装した影はこのくらいです。あとは普通にレイヤー分けをして影ありRealtimeLightで影を落としています。普通にLightmapベイクしてもいいのですが、アバターの影がちゃんとしていないと意味がないのでベイクなしです。花の負荷に比べればこの辺りは無視できる程度のものだと思います。


結果だけを書くと大したことはしていないのでさらっとしていますが、影の再現はけっこう大変でした。ここに至るまで多くの紆余曲折があり、特に制作期間の半分以上は影の再現に苦心していました。机の前にいてもアイデアは振ってこない。今回の学びです。


終わりに

元は息抜きの延長くらいの意気込みでしたが、結果的になかなか大変な作業になってしまいました。

それに、軽量化をがんばったとはいえそれでもなお重いです。願いの代償とはよく言ったものです。
こういう表現をするための手段として私はこうしましたが、もっといい手法があるのかもしれませんし、そうであればいいなあと思います。他の誰かがより良い解を出してくれたなら、私はとても嬉しいと思うでしょう(同時に悔しいとも思うでしょうが、それはどうでもいい話です)。

ということで。この静かなお花畑でのんびりとすごしていただければ幸いです。

画像11