曲から踊りのモーションを自動で作る|拍の推定と、自然に見せるための工夫
このサイトでは、曲をドロップするだけで、その曲に合わせて踊るVMDを作れます。中身は「曲から拍を見つける」と「拍に合わせて姿勢を動かす」の2段で、機械学習は使っていません。作りながら測ったこと、外れたこと、直したことをまとめます。
試すだけなら曲に合わせて踊るモーションを作るへ曲をドロップしてください。 できたVMDはダウンロードでき、そのまま手元のMMDを再生で踊らせることもできます。どちらもアップロード・ログイン不要です。
全体の流れ
- 曲を解読する。 ブラウザの
decodeAudioData()で波形にし、左右を1本にまとめます。 - 拍を見つける。 音の立ち上がりの強さを並べ、その繰り返しの間隔からBPMを、低音の位置から1拍目を決めます。
- 拍に合わせて姿勢を作る。 曲の盛り上がりに応じて動きの型を選び、時刻の関数として姿勢を出します。
- VMDに書き出す。 3フレームおきにキーを置きます。3分24秒の曲で、キー2,041個・3.2MB・生成0.4秒でした。
拍(BPM)の見つけ方
まず、短い区間ごとに周波数ごとの音の強さを出し、前の区間より強くなった分を足し合わせます(対数スペクトルの増え方)。太鼓やベースが鳴った瞬間に大きくなる値の列です。この列が何フレームおきに似た形を繰り返すかを自己相関で調べ、その間隔をBPMにします。
作りながら、合成音(キック + 裏拍のハイハット + 雑音)のテストで3つの穴を踏みました。
| 起きたこと | 原因 | 直し方 |
|---|---|---|
| BPMが0.6ずれる | 自己相関の間隔は1フレーム(約11.6ms)刻みでしか取れない | 曲全体で拍の位置が揃う間隔を、1フレームより細かく探して詰める |
| 裏拍に合わせて踊る | 全部の周波数で1拍目を決めると、帯域の広いハイハットの合計が大きくなる | 200Hz以下(キック・ベース)を重く見る |
| 150 BPMを75と取る | 間隔が34.45フレームのように半端だと、整数のずれで相関が下がる | 自己相関の前に2フレームぶんぼかす |
直したあと、合成音の4つのテンポ(96・128・150・174)で、BPMの誤差は0.5未満、拍の位置のずれは30ms未満になりました。
実物の曲でどこまで合ったか
作者がBPMを公開している実物の12曲(incompetech の CC BY の曲)で確かめました。3分の曲の解析にかかる時間は1〜1.6秒(Chromium)です。
| 結果 | 曲数 | 内訳 |
|---|---|---|
| 作者のBPMと一致 | 8 | |
| 倍・半分・2/3で取った | 3 | 155→103(2/3)、200→100、204→95 |
| 外れ | 1 | 70→140 |
倍・半分に取ると、踊りは速すぎるか遅すぎるように見えますが、拍の位置自体は合っているので、BPMを×2・½するだけで直ります。道具の画面には、BPMと1拍目の入力欄、×2・½・×1.5・÷1.5のボタン、手で叩いて合わせるボタン、拍の音を曲に重ねて20秒聴くボタンを置いています。
拍の位置も確かめました。正解の拍の時刻は無いので、推定した拍の上の立ち上がりの強さを、ずらした位置と比べています。BPMが合った8曲で、半拍ずらした位置の1.09〜5.05倍、1/4・3/4ずらした位置の1.6〜6.4倍でした。裏拍や16分の位置に合ってしまった曲はありませんでした。
推定には「確からしさ」(0〜1)も出しています。BPMを取り違えた4曲は0.21〜0.44で、合っていた8曲は1曲(0.23)を除いて0.60以上でした。そこで0.5未満のときだけ「×2・½で直せます」と注意書きを出しています。低いのに合っている曲もあるので、外れの目安でしかありません。
踊りの組み立て方
最初の版は、「拍ちょうど」と「半拍後」の2つの姿勢を直線でつなぐだけでした。これだと拍のたびにカクッと折り返し、途中で腕が真横に開いた形(Tポーズに近い形)を何度も通ります。作り直した版では、次のようにしています。
- 姿勢を時刻の連続した関数として作り、3フレームおきにキーを置く。 上下の動きは拍で沈む cos の波なので、動き出しと止まり際がゆっくりになります。キーのあいだはVMDの補間に任せます。
- 体重移動を入れる。 センターを左右へ動かし、腰を体重の側へ傾け、上半身は逆へ少し傾けます。首・腕・手首は体より0.12拍遅らせます。
- 腕は真横を通らない。 下げた腕を前後に振るか、前を回して上げます。テストで「腕が水平に近いときは前を向いている」ことを全部の型について見ています。
- 型は6つ。 落ち着き・ノリ・盛り上がりを2つずつ用意し、8拍ごとに曲の立ち上がりの強さで選びます。
- ステップの型だけ足を浮かせる。 体重が乗っていない側の足IKを少し上げます。ほかの型では足は床から動きません。
- まばたきを2〜5秒おきに入れる。 頭もゆっくり揺らします。
型の切り替えでも穴を踏みました。8拍の区間の境目を1拍で混ぜると、腕を下ろした型から両手を上げる型へ移るときに1キーで28°回りました。いまは前後1拍ずつ、計2拍かけて混ぜています。また、区間ごとに左右の向きを入れ替えると、境目で体重が逆の側へ一気に移り、センターが1キーで0.4動きました。左右の向きは曲を通して固定しています。片手ずつ突き上げる型も、2拍で1往復だと腕が1拍で140°回って速すぎたので、4拍で1往復にしました。
自然に見えるかを数字で確かめる
「自然に見えるか」は目で判断しにくいので、髪の揺れ方を実在のダンスモーションと比べました。動きが不自然に速ければ髪は跳ね、少なすぎれば髪はほとんど揺れません。動画の書き出しと同じ1/30秒の固定刻みで物理を進め、ツインテールの先が頭に対して動く速さを、人物Aで15秒ぶん測っています。
| モーション | 中央値 | 上位5% |
|---|---|---|
| 作り直す前 | 2.2 | 5.4〜5.9 |
| 作り直した後 | 5.6〜6.3 | 9.5〜11 |
| ad.vmd(このモデル向けの実物のダンス) | 6.3〜6.8 | 14〜20 |
| kimino.vmd(激しいダンス) | 16〜21 | 46〜50 |
| gal.vmd・kabe.vmd(ゆっくりした動き) | 0.9〜4.8 | 5.5〜9.7 |
作り直した後は、実物のダンスと同じくらいの揺れになりました。前の版の揺れが小さかったのは、動きそのものが少なかったためです。
物理の見た目は、再生しながら撮った画面で判断してはいけません。 一度、描画が毎秒1.3フレームしか出ていない環境で見て「髪が真横に跳ねる」と判断し、誤りでした。物理は描画1回ぶんの時間をまとめて進めるので、遅い環境では実際とは別の揺れ方になります。固定の刻みで進めて数字で比べてください。
ほかに、足首の高さは1.39〜1.99(ステップで浮かせた分を含む)、前後左右は±1.09に収まり、手首と体の軸の距離は最小2.63で、手が体にめり込まないことも確かめました。
曲とモーションは周回ごとにずれる
できた踊りを曲と一緒にループ再生して、音の再生位置とモーションの再生位置の差を5秒おきに測ったところ、2周目から2.3秒ずれ続けていました。
<audio loop>は曲の長さ(204.07秒)で頭へ戻る。- three.js の MMDAnimationHelper は、モーションを「最後のキー + 余韻(afterglow)2秒」(206.2秒)で頭へ戻す。
- 1周目も、音とモーションの読み込みが別々に終わるので0.33秒遅れていた。140 BPMの拍の間隔(0.43秒)の3/4で、見て分かるずれ。
そこで、音を正にしてモーションを合わせるようにしました。250msおきに差を見て、0.12秒を超えるずれが2回続いたらモーションをシークします。1回で直すと、描画が一瞬止まったときに「遅れて見える → シーク → 追いついて進みすぎ → もう一度シーク」の往復が起きました。直した後は差が±0.05秒以内で、シークは始まりの1回と周回ごとの1〜2回だけです。自動の踊りに限らず、BGMとモーションを一緒に再生するときはどれも同じなので、このサイトの再生画面全部に入れています。
できないこと
- 振付らしさは無い。 型を拍に合わせて動かすだけで、曲の歌詞や構成を表す動きにはなりません。
- 途中でテンポが変わる曲は追えない。 曲全体を1つのテンポとして扱います。
- 口・表情は動かさない。 入れているのはまばたきだけです。
- モデルに標準ボーンが必要。 センター・上半身・腕・ひじ・足IKなどの名前で動かすので、名前の違うモデルでは動かない部分が出ます。
再生画面(シーン・手元のMMDを再生)のモーション欄でも「自動: BGMに合わせて踊る」を選べます。選んでいるBGMからその場で踊りを作り、シーンとして保存すれば、開き直したときも同じ選択に戻ります。