見出し画像

draw();#5 VJ解説・振り返り

2024/6/30、draw();#5というVRChat内のクラブイベントでVJをしました。
VJをするのはVR・リアル含め初めてでした。
久しぶりの大きめの自主制作・個人制作だったため、多少なり情報を公開しておきたいと思い、記録を取りながら制作を進めました。

概要

自分では会場の様子を記録できなかったため、感想ツイートを引用します。

https://x.com/minagi_at/status/1807436238322106867

https://x.com/takamin_/status/1807413577491640649

Unityで自作したVJツールを用いて映像をOBSに送るという形をとりました。
静止画ではうまく伝わらないかもしれませんが、

  • アバターOSCでVJツール側のカメラの位置をVRChat内の自分のアバターに渡す

  • 4つのカメラでアバター系レイヤーとそれ以外のレイヤーそれぞれのColorと深度を取り、画面を4分割して表示

  • VJツール側で画面キャプチャを行いそれらの情報を取得、カメラ位置に適当なディレイをかけて位置を合わせ、深度やワールド座標・法線方向を復元。VJツール内のエフェクトと正しい前後関係で合成したり、追加のライティングを行ったり、DOFをかけたり、GeometryShaderでVoxelやパーティクル等の好きなルックに加工したり

といったことを行っています。
VRChatのワールド内で見た目を変えているのではなく、あくまでVJとしてこの画面を出力しているというものです。VRChat内でAR的な表現をしているといえるかもしれません。色だけでなく、深度から各ピクセルのワールド座標と法線方向を復元しているので高い自由度で合成と加工が行えるのが特徴です。
ワールド側への仕込みやアバターへのエフェクトの仕込みを行わず、VRChatにおける一般的なVJの枠組み内ギリギリで新しいことをやるという意識でした。

参加の経緯

2月末に主催のSainaさんからオファーを頂きました。
VJ自体したことないのに本当に有難い話です。draw();は以前から観客側として楽しく参加していたイベントです。
6月末とのことだったので仕事の兼ね合い自体は何とかなる可能性が高いと考えました。しかしこの時点では仕事が思うように進んでおらず、6月になったら一度休みたいと思っていたため後ろ向きでした。

しかし数日後にtanittaさんにお悩み相談をしていた際、「なんかカルチャーにどっぷり漬かって、意図的に嗜好に偏りを持たせるとか良いんじゃないですかね~最近だとほら、draw();の人たちとか良いカルチャーですよね」というお言葉を頂き、偶然にもオファーが来ているイベントの話が出るのは天啓か何かだろうと参加することにしました。
仕事での制作においてもアーティスト部分の能力の不足を感じていたので、VJのような高い自由度で好みのルックを自分の力で作らざるを得ない機会が得られるのは有難いことでした。

アイデアの検討

・アバターOSCを使ったVJをしたいというのは1年以上前から考えてはいた
・アバターのパーティクルとかを操作するのはVJの枠からは外れてしまうのでNG、アウトプットはVJ画面だけにするのが重要
→ならインプットで利用すればいいのでは?
・2023年5月のCHANNELでKeijiro Takahashiさんがフロアを撮影して低遅延のStable Diffusionか何かに入力してVJしていたのを思い出す
→アバターOSCからカメラを操作し、VJツールで画面キャプチャ、深度と色を加工してシーンを演出するという今回の方法を思いつく
・すべてローカルで動作すればOKなのでデバッグも十分行える
・深度を使って合成すればVJツール側は何でも出せて幅が広い
→採用

リアル(AR)でやってるのはあるだろうな、と思ってはいたのですが事前にはあまり調べていませんでした。この記事を書くにあたって調べたところ、やはりKeijiro Takahashiさんがやっていました。エフェクトもだいぶ似通っているものがありました。

また、VRChat画面のVJへの取り込みについても以下の動画が見つかりました。VRC Cameraを利用して撮影しているようです。探せば他の例も出てくるかもしれません。

実装(アバター)

まずOSCでカメラ制御をするアバターを作りました。
先行事例として2年前にじぇしかさんがUnityからVRChat内のカメラを制御する仕組みを作っています。

この時点で実装方法まではわかりませんでしたが、少なくとも頑張れば実装可能である保証があるだけでも有難かったです。

今更ですが、アバターOSCはアバターのパラメータ(Animator内のアレ、表情を制御したりアクセサリを出し入れするのに使われる)をOSCという通信規格で外部から操作できるというものです。つまり、最終的に制御できるのはアバターに仕込んだAnimationです。

アバターのパラメータの精度は8bit程度だったと記憶していたので、それについても調べたところ、有難い先行調査がありました。

自分で確認してみても同様の結果でした。プレイヤー間の通信量を抑えるためにそうなっているのでしょう。
今回はローカルで動作させるギミックなので32bitの精度が使えることになります。一方でカメラ位置を他のプレイヤーに見せるのは難しいようです。

基準点(VJツール側の原点に一致)のPos・Rot(Yのみ)とカメラ位置のPos・Rotで合計10個のFloat値があれば制御できそうです。
(あとからFOVやギミックONOFFのパラメータを追加しました)
最悪みたいなHierarchyを組んでアニメーターで制御しました。

UnityのRotationの適用順はZXYなのでその逆順にRotが並ぶ

アニメーター側ではMotionTimeを使います。
基準点は+-100m、カメラは+-10mの範囲内で動かせるようにしました。

こんな感じ。AnimationのカーブをLinearにするのを忘れずに。

OSCから値を送って操作してみて、ローカルでは十分な精度と反応速度であることがわかりました。

更に自身を視界ジャックして色と深度を表示する仕組みが必要です。
簡単だと思っていたのですがいくつか注意すべき点がありました。

  • Depth生成用の影付きDirectional LightもローカルでActive化されるようにする

    • ワールド側にあれば不要ですが、レイヤー指定等が正しければ大した負荷増にはならないので入れておくのが無難です

  • Depthを取得するカメラは別途必要なので、4台のカメラが必要

    • アバター系とワールド系のレイヤーでそれぞれColorとDepthを撮影

    • Cameraを分けずにDepthを取得するにはポスプロ的なシェーダーが必要ですが、アバターでそれを必要なカメラだけにかけることがおそらくできません(レイヤーがすべてPlayerLocalになるため/今思いつきましたがカメラの座標等で分けることができそうです)

  • ワールドにかかっているPPSのColorGradingをどうにかして無視しないとDepthの値が壊れてしまいます

    • アバターにTargetTexture指定の無いカメラを入れたら視界が上書きされた記憶(※)があったので、試してみたところPPSを無効化できました

      • このカメラに4つの情報を映します

      • したがってアバターに含まれるカメラは合計5台、全てIsLocalでActive化するようにしています

  • sRGBを戻す処理がVJツール側で必要

※3年半前、初めて作ったアバターにBlenderからエクスポートされてしまったCameraとDirectional Lightが入っていてPublicのJapan Shrineを凄まじい光で照らしながら壊れた視界(VR・5mくらい下がった三人称視点)でメニューを開くこともできず彷徨ったことがあります

  • Rチャンネルのみで送った場合、復元側でDepthの精度が足りなくなるため、RGBの3チャンネルに分けて送るようにしました

当初送っていた映像
VJツール側でワールド座標を復元してグリッドを引いてみると、
精度が足りず崩れていることがわかります
RGB各チャンネルを使ってVJツール側で復元することで改善しました
まだ少し崩れていますが、これはRenderTextureのFilterMode=Bilinearだったのが原因で、
DepthのRチャンネルの境界部分が補完されて崩れています
あとからFilterMode=Pointにして改善しています

VJツール制作

環境は学習コスト削減のためその時点のVRChat最新と揃えました。
・Unity2022.3.6f1 Built-in RP
URP選択の余地がありましたが私はBRPのシェーダーの方が圧倒的に書き慣れています。URPではVFX Graphを使える利点もありますが、新しい要素を取り込むと表現側がそれに引っ張られてしまうので、今回の目的を鑑みて見送りました。
VRChat外なのでCompute Shaderを使えるはずですが、今回は演出の内容的に使う機会がありませんでした。これはBoids等をやるのであれば使うつもりでした。

実装(VJツール)

ライブラリが使えるものは有難くどんどん使っていきます。
凹みさんとKeijiro Takahashiさんには常々感謝しています。
特有の部分以外はUnityでVJツールを自作したい方全般に参考になると思います。

  • カメラ制御用OSC送信

    • uOSCv2を使いました

    • 可動範囲が決まっているPosition・Rotationを0-1の値にしてOSCで送るだけです

    • OSCはライブラリを使って操作する分にはとても単純な仕組みなので、気軽に触ってみて良いと思います

  • 画面キャプチャ

  • VJツール内のカメラ制御

    • 普通にCinemachineです

      • Unity公式が提供しているカメラ制御のpackage

    • 割と使い慣れていること、Sainaさんに何を使っていますかと確認してCinemachineだったため

    • OSC送信->画面キャプチャの往復の遅延によりVRChat内カメラ位置とVJツール内カメラ位置がズレます。これはVJツール内のカメラ位置を数フレーム遅延させることで対応しました

      • CinemachineBrainをアタッチしたオブジェクトのCameraコンポーネントは非アクティブにしておきます。画面を撮影しなくなるだけで、CinemachineBrain自体は問題なく動作します

      • MainCameraが上記カメラに指定のフレーム数遅延して追従するようにします

        • 遅延フレームはPCにかかる負荷によって6-9フレーム前後になるようでしたが、多少ズレていてもそれほど気にならないので目視でキャリブレーションしていました。演出内容によってはよりシビアさが求められる可能性もあります

    • フェーダーやツマミで多少マニュアル制御が効くようにしましたが、この辺りはもっと自由度を上げたいところでした。今後3Dマウスを使ったりしてみようかと考えています。

  • OBSへの映像送出

  • MIDI操作

    • 各種テスト用に購入した手元のnanoKontrol2が文鎮になっていたのでちゃんとMIDIコン対応しようと思いました

    • MidiJackは使ったことがありましたがMinisは使ったことがありませんでした

      • それぞれUnityの旧新InputSystemに対応したMidiInput用のライブラリです

      • この機に覚えておくためにMinisを使いました

      • Minisというより新InputSystemがとても難しく感じました

        • Player InputコンポーネントのInvokeUnityEventsだけは便利だったので、全てここから操作しています

          • 適当に呼び出したい関数を書くことができます

          • if(ctx.performed)をつけないと何度も呼ばれてしまうのでつけます

	   int Mod(int a, int n) => (a % n + n) % n;
       public void SwitchNextCamera(InputAction.CallbackContext ctx)
       {
           if(ctx.performed)
           {
               _CameraControl.Index = Mod((_CameraControl.Index + 1), _CameraControl.VCams.Length);
               Debug.Log("SwitchNextCamera");
           }
       }
  • 各種復元用のシェーダー

    • これの制作が一番大変でした

    • 前述の通り、VRChat画面に表示した色とDepthの情報から以下のものを復元・作成しています

      • ワールド座標

        • 深度はカメラからの距離なので、カメラ関連の行列があれば座標を復元できます

ノイズやエフェクト等をこのワールド座標を使って作ることで、
平面的ではない効果をかけることができます
多分初めてWorldNormalを復元したときの画像
ノーマルがあるのでリアルタイムにライティングができますが、
うまく演出に取り入れられなかったので使いませんでした
  • アバターのマスク

    • 単純なマスクと、ワールドに隠れている部分を除くマスク

  • エッジ検出

    • DepthとNormalがありますが、解像度が半分なのであまりきれいには出ません

  • ShadowCasterで深度書き込み

    • DOFをかけるため

    • 通常のVertexShaderで書き込む形ではなくFragmentShaderで書き込む必要があります

pout frag(v2f i)
{
    pout o;
    o.color = 0;
    o.depth = GetDepthCRT(i.uv);
    return o;
}
  • 演出用のシェーダー

復元用のシェーダーを用意している段階でアイデアが出てきたり、DJのよもちさんと打ち合わせている中で見えてきたものがあったりしたので、構えていたよりスムーズに進みました。
40分あることから普段制作しているライブ演出のような高密度の内容はそもそも無理で、飽きさせないというよりはゆっくりした変化でも面白く感じるのを目指していました。

最初はノイズ(Normal色のほう)をScreenUVでかけたりして、あえて平面的に見せるところから入ろうみたいな
同じノイズでもScreenUVとWorldPositionで見え方が結構違う

Ame(B)の演出については事前に決まっていました。
「自分のVRChatクラブイベントの原体験はGHOSTCLUBのCONCRETEで最後にLook at the skyが流れて青空が映し出された回なので、歌ものが流れて空とが見えると気持ちよくなる」という話をよもちさんにしたところ、終盤でAme(B)をかけてもらうことになりました。
手元に良い感じのオーロラもあったので、こんな感じになりました。

ワールド座標があるので、指定した範囲のメッシュをディゾルブさせ
天井に穴をあけることができる

Voxel化や粒子化はジオメトリシェーダーです。
分割したQuadを用意してCRTからワールド座標を復元、その位置にVoxelや粒子を生やしています。そのままだとVoxelはZFightで重くなったりしたので適宜間引いたりしています。
これもShadowCasterを書いてあげたことでDOFがかかるのが気に入っています。

粒子化したあと、ノイズで崩すと良い感じのビジュアルになりました

感想

書ききれないなと思ってきたのでこの辺で一旦終わりにします。
知りたいことがあったら書き足すのでTwitterとかで言ってください。

VJはとても楽しかったのでぜひまたやりたいです。流石にツールを毎回自作するのは厳しいので、今回のをもう少し汎用的に使えるように修正したものを用意し、何度も使わせてもらうのが良いですかね。
秋とか、それ以降になると思いますが良かったらお声がけお願いします。


いいなと思ったら応援しよう!