Imaginantia

思ったことを書きます

記録された自分をVRChatのワールドに置く

「私」がワールド解説をしてくれるワールドを作りました。ので、そのためにやったことを以下解説します。

こんな感じ。

これによってこういうワールドが増えてくれると私はとても嬉しいです。活用方法は色々あると思います。

何が行われているのか

  • まず私は自分のワールドで、色々喋ったり動いたりしました。
  • その私の動きを40分間記録しました。
  • その記録を用いて、私の動向を再現して閲覧できるワールドを作りました。
    • タイムライン制御は自作の TimelineWand を使っています。これの話は今回はしません。

少し詳細を書くと:

  • lox9973 さんの ShaderMotion という、アバターの動きを動画に記録する仕組みがあります。
    • これは実は Lip Sync などの Shape Key も記録できます。
  • 自分のアバターに ShaderMotion の録画機構を組み込んだ状態で、40分間、フレンド(3人)に対して (録画することを前提に) 解説を行いました。
    • OBSで普通に撮りました。
  • 撮り終わった動画ファイルを ShaderMotion の MotionPlayer を用いて AnimationClip に変換しました。
  • また、動画ファイルから (今回は ffmpeg を用いて) 音声ファイルを抽出しました。
  • この AnimationClip と音声ファイルを Timeline に乗せると、「動く私」ができます。
  • あとは気合で字幕を作りました。

以上で枠組みとしては終わり (簡単!) なのですが、それぞれに細々とした話題があるので、以下詳しく書いておきます。

モーション記録

まずアバターを用意します。昔のプロジェクトから幽狐さんを久々にひっぱり出してきました。

ShaderMotion の使い方は wiki に書いてあるのでこれを参照します。また、アバター用セットアップは ここ に書いてありました。書いてある通り、"SetupAvatar" から "Setup Motion Recorder" と "Setup Animator" を実行しました。

もうこの時点でアバターをアップロードすると記録ができるようになっています (設定用の Radial Menu もついてくる!)。便利ですね。

原理を勝手に推測すると、アバターのボーン情報を記録するための Skinned Mesh を作り、それを衣装を着せるのに似たような感じでアバターに追従させているのだと思います*1アバターが動くということはボーンが動いていて、それに従って記録用メッシュも変形する。それが不思議な力によって画面上に表示されるマーカーとなって、動画を撮るだけで情報が記録できるようになるというわけですね。

モーション記録アセットとしては ShaderMotion の他にも HUMR というのもあって、実際 CUE でも使わせて頂いたのですが、仕組みが違うのでできることに差があります。

  • HUMR はワールド側ギミック、Udon
    • アバターに前処理をする必要はない
    • VRChat のログを使って動きを保存する
    • ワールドで取得できる情報しか取れないので、Lip Sync や表情は記録できない
    • いわゆる「モーション」の記録には十分かつ便利です
  • ShaderMotion はアバター側ギミック、シェーダ式
    • 対応アバターを使うか、自分でアバターセットアップをする必要がある
    • VRChat のウィンドウ画面を録画することで動きを保存する
    • シェーダで取得できるものは原理上記録できる
    • 「全て」を記録するにはこちら、になりそう

まぁ ShaderMotion は多少玄人向け感が否めませんが、大変なことをやろうとすると相応に大変だということだと思います。

今回は LipSync・表情・音声も記録する必要があったので、必然的に ShaderMotion を選択しました。というより、ShaderMotion でギリギリ実現可能だということがわかったので作り始めました。いつもありがとう lox さん。

モーション記録の原点

セットアップされたアバターにはモーション原点をリセットする機能がついているのですが、今回はワールドスケールで記録するのでワールド原点がモーション原点になってくれれば十分です。

というわけでアバターに「原点固定の GameObject」を追加し、Recorder の Anchor に Parent Constraint を追加して、その原点オブジェクトに紐づくようにしました。

なんだか Recorder の Root Bone を抜くだけでも近いことになりそうという話は聞いたんですが、私が何故それをやらなかったのかはわかりません。

Lip Sync を記録する

まず VRChat の Lip Sync は Avatar Descriptor に指定した Face Mesh の Shape Key が変化することによって実現されています。端的に言うと ShaderMotion で Lip Sync を記録する際には先ほど生成した Recorder 用のメッシュを Face Mesh として割り当てる必要があるみたいです。逆に言えば、そうすると Recorder 側に Lip Sync のモーフ情報が行くので記録ができるということですね。

だから記録用のアバターの LipSync はそのままでは動かなくなります。ちょっと寂しい。まぁ、とは言え現代では Animation Parameter に Lip Sync の情報が入っているはずなので、これは違う方法で実現できるのかもしれませんね。

ちなみに、それでセットアップしてみたところなんだか Lip Sync が動いてくれませんでした。原因を調べてみたら、まず Viseme: sil に私は何も Shape Key を設定していなかったのですが (Recorder メッシュに用意されてなかったので)、どうやら実はこれは必要みたいです (おそらくどこかで VRChat の仕様変更があったのでしょう…)。

とりあえず私は MeshRecorderGen.cs の最後の方に sil (silent) 用の無の Shape Key を追加して対応しました。よく考えたらこれは早く作者にフィードバックすべきだったかもしれない。

表情を記録する

ShaderMotion には emotion_0 から emotion_7 までの8つの Shape Key が用意されています。流石に表情アニメーションを全部記録するわけにはいかないので、手による Gesture (8通り) の値をコレにそのまま割り当てて、再生時に表情を復元するということを行います。

無みたいなアニメーションを8通り作りました。

Gesture の Animation Controller にコレ用のレイヤーを追加して、えいえいとアニメーションを組みます。まぁ参考にすべき状態遷移図はすぐそこにあるので大丈夫。これで表情を表す値が記録できるようになりました。

撮影

やります。

ちなみに記録テストはかなりやりました。怖いし。あと1人リハーサルも原稿づくりもやりました。最終的には好きに思いついたことを喋っている感じですが。書いたこと読むの苦手なのでこれでよかったと思います。

モーション復元

モーションは、一度動画から復元した後、Timeline で再生するために Animation Clip に書き出す必要があります。ShaderMotion の MotionPlayer は指定したテクスチャからリアルタイムでモーションを復元してくれて、さらに AnimationRecorder は指定した Animator の動きを Animation Clip にしてくれます。つまり道具は揃っています。

(ちなみに今回は使いませんでしたが、ShaderMotion は VRChat 上で動画からリアルタイムにモーション復元を行う仕組みも持っているみたいです。ただそれはシェーダ式なので、揺れ物とかは多分動かないんじゃないかな?)

まずアバターをワールドに置きます。ちなみにこのアバター、DynamicBone 時代のものなので素直にワールドに置けました。

そして録画した動画を Assets に入れ、ワールドに VideoPlayer を置きます。Target Texture を Material/Motion.renderTexture にします。この辺は (アバターも含めて) 記録用のギミックなので、最終的には (VRChat 上では) 不要です (EditorOnly にしています)。

アバターに Motion Player を設定して (多分 Example を見ながら設定しました)、Motion Buffer として Material/MotionDec.asset を参照します。これでとりあえず動いた気がする (まぁデータはちゃんとつながっている)。

Morph Settings に Lip Sync の正しい Shape Key 名を入れると Lip Sync も動きます。

ちなみに、Lip Sync として記録されているのは自分から見た Lip Sync の値です。実はこれは実際の発声のタイミングから少し遅れていて、このままだと…少し遅れます。はい。全体的に Lip Sync だけちょっと時間を早めてあげるみたいな実装を入れたほうがいいのかもしれませんが、今回はやっていません (考えてなかった)。

表情を復元する

Morph Settings では一応表情用の Shape Key を指定できますが、本来はもっといろいろ (耳ボーンとか) 動いてるので、色々やる必要がありました。ので改造しました。

どこを弄ったのか正確には覚えていませんが:

  • アバター Root の 親 GameObject に Animator を追加し、そこでジェスチャを動かすことにした
  • MotionPlayer からその親 Animator を参照し、「8つの表情 ShapeKey のうち最大の値を取っているもの」を SetInteger で教えてあげるようにした
    • decoder.shapes に追加させる為に MorphSettings の方もちょっと弄ったみたいです
  • 親 Animator には元々アバターで使っていた表情用アニメーションとその遷移を入れる
    • ただアニメーションの path が変わるのでズラす必要がある (スクリプトを組んだのかな?どうやったか忘れました)

これで耳も含め動くようになりました。

AnimationClip を生成する

まず AnimationRecorder は「現在の動き」を記録していく機能みたいです。なので、つまり、40分の動画を使うなら40分エディタ上で再生させておく必要があります。

ちょっと勿体ないかなとは思ったんですが、まぁ丁寧に作るのも面倒なのでそのまま使いました。記録中にラグが発生しないように基本的にPCは触らないようにしていました、別にそんなに不安定なわけでもないですが。

内部的には Unity の GameObjectRecorder を使っていて*2、いくつかのコンポーネントを Bind した後毎フレーム TakeSnapshot を行う実装です。元々の状態だと耳ボーンが書き込まれてなかったので (それはそう) 追加したりしました。

また、TakeSnapshot は delta time を受け取るのですが、そのまま Time.deltaTime を用いると VideoPlayer の時間とズレるような気がしました (というか、ズレたのを治すための試行錯誤の1つとして考えたこと)。

なので VideoPlayer の time を参照して安定して時間差分を計算するように変更しました。どれくらい効果があったかはわかりません。

時間がずれる

というのも、色々調節してみても音声とモーションがズレるという現象が発生していました。このズレは全て Lip Sync を基準にチェックしていたのですが、あるところでタイミングぴったりになるようにしても別のところでぴったりにならなかったのです。

誤差が線形ならなんとかなると思って色々弄ったんですが結論から言うと非線形でした。「途中からだんだん増えて、最後の方になるとだんだん0に戻っていく」ような誤差でした。謎です。

前述の Lip Sync のズレ問題はまぁ手動で合わせるつもりだったので (逆にモーションが先行するんですけど!) その辺は大丈夫だろうと思っていたんですが、こうなったら話は違います。困りました。

困ったので、clip を切り刻んで手動で合わせました。時間が足りない部分はちょっと長めにして blend、短い部分は重ねるようにして blend しました。

ちなみにモーションのズレは最大4秒くらいでした、ちょっとそのまま使うには厳しい感じだった。こんなに細かく区切らなくても大まかに合わせることはできるとは思いますが。

あとそういえば音声は ffmpeg で出しました(あとノイズ除去とかやった)。こういう状況、どこに問題があるのかわからないのがしんどいところですね…(ffmpeg は大丈夫だと思ってるんですが)。

 

というわけでなんやかんやあって「私」が動くようになりました。やったね~

余談

ClothとDB

スカートが Cloth で動いていて、よく吹っ飛びます。特に早回しとかするともちろん吹っ飛びます。

あとDBがHeadに入っていて(!)、Timelineを止めると変な挙動になったりしました。どんなことになってたのかは忘れました。

ので、Timeline Wand の機能として、停止/再生の切り替わりのときの callback を追加して、ClothのリセットとDBのオンオフを入れるようにしました。

Clothは自動的に治る仕組みとかは入ってないので多分よく吹っ飛んでると思いますが、まぁVRChatらしいのでOKということにします。

入りと抜け、鏡だったらかっこいいな~って思ったのでやりました。というかなんとかなるだろうと思ってそのように撮りました。

面倒でした。知ってた。

これは Geometry Shader です*3。「鏡の中のメッシュ」を鏡の平面で反転させて表示しています。詳細は書くのも面倒なので割愛。

なのでよく見ると境界線が見えます (鏡の手前と奥は違うメッシュ)。シェーディングを合わせる為に法線を反転させたりもする。

このアバター基本 UTS なんですが、他にも自作のアクセサリー用のシェーダとか色々入っていて全部に対応する必要がありました。面倒でした。

 

また、同時に問題となったのが Bounds で。「鏡としては本来はレンダリングする必要はないが、今回の目的ではちゃんと映らなきゃいけないメッシュ」があります。

つまりオレンジのメッシュを映したいのだけど、鏡としては当然 (鏡の奥側にあるものだから) レンダリングする必要はないことになって、アバターのメッシュが消えます。そうだね。

困ったので Bounds を 10m くらいにしました。とりあえず大体は大丈夫になりました。

しかしさらに問題だったのが前述の Cloth で、こいつは毎フレーム御丁寧に Bounds を再計算します。止めることも後から書き換えることもできそうにありませんでした。やめてほしい。

困ったので、「アバターが鏡の中に入っているときは Cloth を無効化する」ようにしました。まぁそんな長い時間でもないですし、コンテンツとしての影響はないでしょう。

というわけで、鏡で in/out するのはかっこいいと思うんですが、おすすめしません。

字幕

無いと困るな~と思ったので日本語字幕・英語字幕をつけました。シークするときにわかりやすいし。

つくりかた:

  • まず Whisper に音声を投げます。途中までしか帰ってきませんでした。
  • なので音声を10分割して、それぞれ Whisper に投げました。たまに失敗するので結果をチェックして異常だったら投げ直します。
    • 入力が同じだと投げ直す意味がなさそうだったので音声の開始タイミングを少しずらしたりしました。
  • 句読点のスタイル・平均の字幕の長さ・カタカナ/英単語が混じった微妙な文章が得られました。(1000行)
  • ほぼ全部書き直しました。
    • 書き直す用のツールも作りました (生データを編集するのは効率が悪すぎた)。
      • 音声の開始/終了タイミングも結構ズレてたので Timeline を凝視しながら全部直しました。
    • でも0から書くよりは取り組みやすかったと思います。

これで日本語字幕ができました。次に英語字幕を作ります。

適当にコンテキストつけて ChatGPT に投げました。よく止まるのでまぁ何度か頑張ります。

全部できても概ねニュアンスが意図したものになってないので、全部チェックしました。

こんな感じでやってます。便利です。

文の用意ができたらあとはいい感じに表示系を作って完成~

字幕作るのに一番時間掛かりました。

おわり

存在していてほしいものが欲しかったので作りました。なのでメインコンテンツは「私」および「可能性の提示」で、パイプラインの解説それ自体はおまけみたいなもんです。

「記録された存在」、是非いろんな人にやってほしいなと思っています。大変かもしれませんが。

きっと可能性は伝わったと思うので。

*1:まぁ実際には同じ Transform を参照しているだけでしょう

*2:実は Unity Recorder みたいなもの、簡単につくれるみたい

*3:Vertex Shader でひっくり返すと面の裏表も反転するのでダメ