なんかやってました。自分用のメモ程度に書いておきます。
スイッチ
スイッチです。なにかの。
— phi16 (@phi16_) 2025年8月10日
モデリング
モデルを作り始めたときは時間の余裕があったのでバカなことをしています。
KineFX 使ったほうが絶対良かった。次やるならそうします。
モデルには特に何か変なものは仕込んでません。ボタンの UV は (n+0,0)-(n+1,1) 範囲で管理しているのでシェーダ側で開いています。
テクスチャは適当なタイリングテクスチャを Substance Painter で作ったんですが、もっとちゃんとやればよかったなぁみたいな気持ちになっています。ちなみにストローとか UV が全部縮退しています ((0,0) にあります)。あと思ったより彩度が高すぎたな。
UI
操作系は (私にしては) 珍しく uGUI です。ちっちゃいので Interact だと暴発しそう&見づらいだろうというのと、最近? uGUI ベースの UI が (諦め半分でしょうけど) 多いので自分もやってみた感じです。誰でも操作できそうだし。
が、表示に関しては uGUI は何もやってなくて (Image の Color を完全透明にして Cull Transparent Mesh 入れてます)、Button/Slider のイベントを全て Udon で取得して自前で見た目に反映させています (なので頑張れば 1 Material にできる気がするけど面倒だったのでボタンと箱で 2 Materials です)。
やってることとしては :
- Button の状態遷移
- Button の Transition を Animation に設定
- 各 Trigger に個別のトリガー名を割り当て
- AnimationController で各トリガー名で各 Animation を発火させるように設定
- Animation には Animation Event を 1 つだけ割当、そこで
SendCustomEvent(好きな名前)
を発火させる - つまり :
- ボタンにレーザーを hover すると
- Animation Controller に Trigger が発行され
- それによって Animation が再生され
- Animation Event が発火し
- 自分に対して
SendCustomEvent
が実行される - という流れで、Udon 側でボタンのイベントを拾うことができます。
- Button のクリック
- 素直に OnClick に
SendCustomEvent
を登録するだけ
- 素直に OnClick に
こんな感じ。Udon は各ボタンに 1 個ずつ付いていますが、中身は上位の Udon に委譲するだけです。
見た目
ボタンの状態情報は Udon で (VectorArray に) 書き込んでいます。アニメーションの為に毎フレーム更新するのは勿体ないので、前と同じく「ベース時間との差分」だけ書き込んで、毎フレームの処理では現在時刻だけを書き込む形にしてアニメーションさせています。
ベース時間っていうのはアレです、Networking.GetServerTimeInSeconds
のことです。これは同期する必要がないので同期していません (自分がワールドに join した時刻が 0 になります)。
また、時間 (秒) を直接いれてしまうと 7 時間持続するようなインスタンスでは心許ないので、秒の整数部分/小数部分を分けてシェーダに渡しています。
スライダーは頂点シェーダで動いています。なので Pickup 形状には元の形が残っているんですが面倒だったのでそのままになっています。
そういえば LV 対応したかったので orels Unity Shader で (.orlshader を) 書いてます。普通にめちゃ書き心地良くていいです。おすすめです。
花火
形状
SVG で描いた Path を Houdini で読み込んで Resample で点を打っています。ae_SVG を使ってるんですが最初動かなくて、なんでかな~と思って調べたら exactly な記事があってウケました。大変助かりました。
ちなみに花火の細やかさを調整するために Path の幅 (pscale に入ってた) を Resample の Length に突っ込んでいます。
「曲線」ではなく「点」を打ちたいことが稀にあったので (極小の曲線を仕込んだ上で) Fuse を通したりもしてます。細かい調整はいろいろやってますが割愛。
とりあえず形状制御は全部 SVG でできるようにしていたので、操作が比較的しやすく良かったです。
後はその点情報をテクスチャにして、exr に吐き出して、シェーダで読んでいます。
ついでにスイッチのサムネイルを作る為に Copernicus でレンダリングして TOP 内で Wedge して ImageMagick 通してアトラス作るのもやりました。参考にしました。
パーティクル系
パーティクル表示用のメッシュはこんな感じです。点の数が高々 200 くらいだったので、256 x 16 個用意しています (つまり 16 個まで同時に出せます)。
約 7.3 万ポリあります。殆どはトレイル用 (上の繋がってるやつ) のせいなんですが、爆発した瞬間しかほぼ見えないので、基本的に描画されてるポリゴン数は大した量ではありません。
表示系はまぁいい感じにやります。最初 Bloom が足りないと思ってメッシュ範囲を広げたりしたんですが重くなってしまったので今は Post Processing の Bloom だけでなんとかしています。
花火の管理はまた自作 GPU パーティクルシステムを作って運用しています。Mipmap 作ってバッファを圧縮するやつです。なので Udon から SetVector でパラメータを突っ込むだけで花火が増えます。別に要らなかった気もします (各ピクセルに順番に突っ込めばいいだけ)。
先述の通り高々 16 個しか発火できないので 4x4 で十分なんですが、念の為 8x8 の領域で管理しています。
発火と同期
一応同期しています。正確には Owner だけ (パラメータ変更の瞬間は) 少し多いことがありますが。
なので自前で乱数を用意して、全員で同じ値を計算するようにしています。late-join を考えると任意のタイミングで任意の位置の乱数系列を O(1) でサンプリングできる必要があります。
どうしようかな~と思ってたんですが、一番簡単な Look Up Table に落ち着きました。
適当に 0~255 の permutation を生成しました (どうやったかは忘れた、多分 GHCi か JavaScript)。で、これだけだと 256 でループしちゃうので index/256 に無理数を絡めて適当にそれっぽい値を出すようにしています (frac(lut[index%256] / 256.0 + (index/256) * pi + seed)
です)。
キャラは「現在有効になっているキャラリストから uniform sample」しています。こっちは簡単。
出現位置の方は、今回 9 通り出るようにしているんですが、できるだけ被らないようにしたい気持ちがあります。ありました。計算してからリロールするのは出力が過去に依存するので今回には不向きです。
というわけで、次のような処理をしています :
- 奇数回目は奇数番目に、偶数回目は偶数番目の位置に登場するようにしています。
- これで 2 連続で同じ場所に出ることは無くなります。
- 4 で割って 0 または 1 の回数のときは、単純にランダムに決めます。
- 4 で割って 2 または 3 の回数のときは、「2個前」と「2個後」の位置に被らないようにサンプリングします。
- (2個前/2個後の位置は ↑ のルールによって既に決まっています)
- これによって、「1回出た場所には、以降3回はそこを使わない」保証ができます。
発射間隔を変えると乱数の seed が変わるのでその時だけは位置が被ることがあるんですが、そこまでがちゃがちゃスライダー動かさないだろうと思ったので、まぁ大丈夫かなと。
LV
一応、アバターに光が当たっています。VJ が光ってるともうわかんないのでわかんないですけど。すごくほんのり入ってます。
適当なシーンで Directional Light を焼いて 4x4x4 の Volume を作りました。別に私は 1x1x1 でもいいんですけど焼き加減が変だったので増やしました。
後は光量を計算するだけなんですが、花火の光量がわからないので、計算できるようにします。
まず Houdini で各花火の各星の色の重み付け和を計算して TOP Text Output で Space-Separated String を吐きます。これです。
んで、各花火の現在の明るさでさらに重み付けして和を計算したいんですが、花火情報は全部 GPU が持ってるので一旦 GPU に任せました。つまり VectorArray として渡しました。
で、Mipmap を有効にしたバッファに明るさを出力させて、VRCAsyncGPUReadback を mipIndex = 3 で発行します (8x8 なので log₂ 8 = 3)。明るさが取れました。LightVolumeInstance の Color に突っ込みます。反映されます。おわり。
光量はシェーダ側で好きにコントロールできるので適当に係数を掛けています。Mipmap は (総和ではなく) 平均値を取ってしまうので 8x8 を予め乗算することを忘れずに。
おわり
ちらっとでも空間に楽しさを寄与できていたら幸いです。