バーチャルミュージアム・技術編
この記事は「バーチャルミュージアム・思想編」に続く技術編です。
制作にあたってのモデリング、シェーダー、アニメーション、負荷問題について、やったことや悩ましかったことをドバっと書いてみる。
動画だとこんな世界。VRChat用のVR空間はUnityで制作する。モデリングはRhinocerousを使ったのち、Blenderでメッシュ調整・UV展開をしている。
技術編① ワールドモデリング
思想編で紹介した3Dボロノイのランドスケープは、建築のモデリングではわりと一般化したRhinocerous+Grasshopperを使っている。なお、通称ライノはポリゴンモデリングに一切向いてない。つくったメッシュはBlenderで整えないと使い物にならない…。ただ、そもそもモデリングの仕方の思想が違うので、ライノだから簡単にできることもたくさんある(はず)。
例えば、3Dボロノイが簡単につくれる。Grasshopperというノードベースのプロシージャルモデリング用アドオンを使い、80m角×高さ10mの空間にランダムに300点を打って、3DVoronoiコンポーネントに入れる。上図右の紫色エリアのノードだけでできる。お手軽。
このあと適当な曲面で上部を削り取って丘のようなランドスケープにしてから、各セルの辺を抽出して三角柱にした。
で、これができる。面しか見えないけど、各辺は細い三角柱になっている。
適当な曲面で削り取る部分が実は手作業で、そこも全部プロシージャルにつくれば地形も自動生成できるのに…という。でもイメージが固まっている場合、かつ後々修正が発生しなさそうな見通しがある場合は、もう早いからこれって決め打ちしちゃいます。
展示ブースが入ってから急遽つくったデジタルツリーもほぼ同じ要領。ある範囲内でランダムなサイズ・位置のBoxを生成したのち、手作業で木の形に剪定した。手作業。プロシージャルを活かしつつ活かさない絶妙な塩梅!(頭使ってノード組むの面倒だったから逃げたんだけど、無限の選択肢から好きな形が出てくるまでシード値いじるより、好きな形つくっちゃったほうが早いこともこの世にはある)
ちなみに、このデジタルツリーは10秒ごとにBoxの見た目が少し変わるようになっている。モデリングの際にBoxを4グループに分けて、UV展開で各グループごとにBoxの面を下図の①〜④に割り振り。
あとはシェーダーで①→②→③→④→①の順で時間変化するようにすれば、1マテリアル、1テクスチャで色味と模様が変化する木になる。
技術編② アニメーション
バーチャルミュージアムでうまくいったのがオープニングアニメーション。真っ白な世界からぶわっと地形が拡大して色彩が現れる部分(冒頭の動画参照)。ここは、以下のようにつくった。
白い球体にはDissolveシェーダーを使って、アニメーションで不透明→透明を推移させている。また、模型が拡大するときに、まだ不透明な白い球体を突き抜けていくため、そのままだと球体の外に出た部分が描画されなくなってしまう。というわけで、模型のシェーダーにZTest Alwaysを書き加えていつでも描画されるようにしている。(RenderQueueをTransparent+1にするだけでも良さそうだけど、なんらかの理由でそうした気がする)
なんかかっこいいっぽいオープニングアニメーションになったから褒めて!
技術編③ シェーダー
建築設計をやっているだけだと、シェーダーにはまず出会わない。(というかUV展開すら1年前は知らなかったよね。)
プログラミングを全くやってこなかった身からするとハードル高いけど、VR空間でやりたい表現を実現するためにシェーダーは避けて通れない。
いろんなサイトや先人の知恵を参照しながら、今回は「アトラス化した画像を指定秒で切り替えるノイズ付きRGB液晶ディスプレイ」を自作した。
詳しい解説をする紙面が足りないけど、フラグメントシェーダー内はこんな感じで記述してあります。
fixed4 frag (v2f i) : SV_Target
{
float2 inUV = i.uv;
// 色を計算
float3 col;
float3 col2;
// 各画像の表示時間、切り替え
float t = floor(_Time.y / _Duration);
float2 t2 = float2(t, floor(t / _Atlas));
float2 uv = inUV + t2;
// 切り替えノイズ
uv.x += (rand(floor(uv.y * 500) + _Time.y) - 0.5) * _NoiseX * step(_Duration - 0.7, mod(_Time.y, _Duration));
// モザイク化
float2 pix = 1.0 / _Resolution;
float2 mozaik = float2(uv.x - (fmod(uv.x, pix.x) - (pix.x * 0.5)), uv.y - (fmod(uv.y, pix.y) - (pix.y * 0.5)));
// 色を取得
col = tex2D(_MainTex, frac(mozaik / _Atlas));
col2 = tex2D(_LEDTex, inUV * _Resolution);
// LEDパネル化
float3 col3 = clamp(float3(col.r * col2.r, col.g * col2.g, col.b * col2.b) * 5, float3(0, 0, 0), float3(1, 1, 1));
// RGBノイズ
if (rand(floor(inUV.y * 500) + _Time.y) < _RGBNoise)
{
col3.r = rand(inUV + float2(123 + _Time.y, 0));
col3.g = rand(inUV + float2(123 + _Time.y, 1));
col3.b = rand(inUV + float2(123 + _Time.y, 2));
}
return float4(col3, 1);
}
最終的に使った画像はこの2枚。
ノイズ2種に関しては完全にnotargsさんのこの記事からちょっと改造しつつ基本的にそのまま使用させてもらいました。
画像切り替えに関しては、"_Atlas" がアトラス化画像の行=列の数で、今回は2行2列なので"2"。"_Duration" が各画像の表示時間。UVスクロールと同じ原理だけど、"floor()" を使うことでパッと切り替わる。
あとは、"_Resolution" でピクセルサイズを決めて、画像をそのピクセル数でモザイク化した上で、同じ数のRGB画像と掛け合わせて、赤い部分は赤だけが反応するようにしている。暗くなっちゃうのでrgbの値を5倍している。
さらっと言ってるけど、ノートに図を書きつつ苦心して理解した。Amplify Shader Editorで先にノードベースで組んだ上で、各関数の働きをビジュアルで理解して、それからテキストベースに書き直している。初心者におすすめの勉強法だと思う!
何かを解読しようとしている人の怪文書
もうひとつ作った、ボロノイのフレーム上に虹色を移動させて「情報が伝達してる感じ」を出しているシェーダー。こちらはだいぶ単純です。
void surf (Input IN, inout SurfaceOutputStandard o) {
float dist = distance( fixed3(100,0,0), IN.worldPos);
float val = abs(sin(dist*0.5-_Time*5));
if( val > 0.7 ){
o.Albedo = fixed4(abs(cos(dist)), abs(sin(dist)), 1-abs(sin(dist*3)), 1);
} else {
o.Albedo = fixed4(1, 1, 1, 1);
}
}
ワールド座標から虹色に塗る場所を決めて時間でスクロールしている。sin波の絶対値が0.7以上になる部分だけ色を塗って、それ以外は白とすることで、色が移動していってる感じが出せた。(シェーダーで "if" を使うのはあまりよろしくないという説もあるけど)
シェーダーの話の最後に、バーチャルミュージアムに訪れた多くの人が感動した「アルテマ音楽祭」だけど、Stencilを使ってSkyboxの書き換えを行っているため、ワールド側のほぼ全てのシェーダーにStencil Buffer値を書き加えている。なぜかStandard Transparentだけうまく動作せず、ワールド側の半透明オブジェクトはシェーダーを新たに作ることになった。
視覚情報の上書きができるStencil Maskはいろんな視覚トリックに使える。
場所を変えずにブースがある世界でそのまま体験型音楽ライブが始まるというのがとてもとても良かった…
技術編④ 負荷問題
バーチャルミュージアムは高負荷が大きな問題となってしまった。特に3会場あるうちのA会場がとても重く、VRreadyなPCでもVRだと20fps近くまで下がる視点が多く、快適でなかったり、そもそも来れない人が出てきてしまった。とても申し訳なく、悔しかった。ブース配置以降に時間があれば何らかの解決ができたけど、今回は時間がなく、直前まで原因究明と対策を練ったけど時間切れだった。
ワールド自体は90fps張り付きになるよう作っていて、Batches/SetPassCallsもともに50ちょい程度と大して重くない。(ブースに求められた推奨値が20ずつくらいなので、ブース2.5個分くらい。)なのにAはどうしてこんなに重くなった…!?
結論から言うと、以下の3点が問題と判明した。(問題の大きい順)
1)一度に視界に入るブース数が多すぎる
2)とても重いブースがいくつかある
3)水シェーダー用のリアルタイムライトが悪さしてる
床や木などの半透明の多用が悪いのでは??という意見も出ていたが、半透明自体はほとんど問題ではないと結論づけた。(もちろんモバイルでは非推奨と言われるけど)
1)については、正直なところ、表現としてブースを同時にたくさん見せたいという思いがあって、不安はあったけどトライした結果。今回はブース入稿の軽量化規定があったため、わりと行けるかも…? と期待していた。でも、やっぱり40ブース(Aが一番多い)は厳しかった。とはいえ、BとCはギリギリOKなレベルだったので、ブース軽量化はけっこううまく行っていたと思う。
ただ、「たまたまAが厳しくて、B/Cはまぁまぁ」では駄目なので、今のマシンスペックでは、やはり同時に見れるブース数は絞らないといけない。
ワールド側にOcclusion Cullingが効くような壁がなかったため、途中からの解決は難しく、下のように試しにワールドに馴染む壁を追加してみたけど、Bake設定をどういじってもほとんど効き目がなかった。(開催まで24時間切っててモデリングしてたねこれ…)
中央のそびえる壁でブースを5つくらい隠しているんだけど、壁にかなり近づいたときはOccluderの役割を果たすのに、遠くからだとガバガバに視線のRayが突き抜けまくっていた…。なぜよ。OCって、ダンジョンや市街地のような巨大な壁面に囲われた世界を想定しているように思う。
大量ブース同時視が問題なのは、「モクリバザール」の屋根の上からの見下ろしで30fpsまで落ちたり、「異世界マルシェ」のカリングが効かない上空からの見下ろしでガクガクになったという報告もあったので、ワールド表現に依らず起きることだと思う。
で、これについては、ミュージアムはLOD(Level Of Detail)で解決する予定。見え方としてそこまで不自然ではなかった。
2)は、Batches/SetPassCallsに表れて来ない重たいシェーダーが原因で、そのブースを含むと(後述するリアルタイムライトを切っても)、3-4ブース見ただけで30fpsまで一気に落ちるということがわかった。B/C会場が比較的大丈夫だったのはこの差。これについては個別に解決策を見つけて、出展者の協力をいただきつつ解決するしかないです。(今回はそこの規定がなかったのでブース側に落ち度はない。)
ちなみに、Opaqueで良い表現なのにCutout等に設定されているものがけっこうあり地味に負荷を上げていったりするので、ブース側も確認してほしいという話が出ていた。
3)は盲点というか知らなかったやつ。ワールドにある水のシェーダーが、Shadow付きRealtime Directional Lightがないと描画されないため、水のレイヤのみに当てるライトを追加していたんだけど、これが当ててないレイヤのオブジェクトの負荷も上げていたんだね。Depth情報を取得するシェーダーは全てこのライトを参照するらしい…。厄介。(噴水のある「異世界マルシェ」も同様のライトを追加している。)
実は「アルテマ音楽祭」のときにこのライトがあるとStencilで消したオブジェクトが影のようにパーティクルを消してしまう現象が起きていて、当ててないのになぜ…と思っていた。なので、「アルテマ」中だけ切っている。これが「アルテマ」中はちょっと軽くなる理由です。
ただ、このライト問題は、2)までの問題ほどクリティカルではなく、他を解決すればそれほど大きな問題にならなかったので、このままで行く予定です。でも、恐るべしリアルタイムライト!
おまけ BGM
長くなった…。けど書ききった感はあるよ。
おまけで、BGMの話。バーチャルミュージアムのBGMは、私が大好きなジャズギタリスト、Kurt Rosenwinkelの『Caipi』というアルバムの「Little b」という美しい曲のラストのコード進行をそのまま使ってます。カートさんの曲は複雑なコード進行で謎な転調繰り返すのに気づいたら元に戻ってて死ぬほど気持ち良いんだけど、この曲は単純ながら無限に心地よく下降していく感じがお気に入り。◯outubeに転がっているぽいので探してみてね。
G→FM7→G→FM7→C→Bm7
E→DM7→E→DM7→A→D(→Gに戻る)
おわり。
(前回の思想編はこちら)
番匠カンナバーチャル建築設計事務所 番匠カンナ
https://www.banjo-kanna.com/