Webブラウザ上のMMDの世界に飛び込んで一緒に踊る

## 実装したもの
下記2つの技術を同時利用して、MMDと一緒に自分の動きを反映したアバターを躍らせる。
①Webブラウザ上でMMDを再生
②スマホアプリ「[TDPT](https://digital-standard.com/tdpt_lp/)」で取得したリアルタイムモーションデータをWebブラウザにP2P送信し、Webブラウザ内のVRMを動かす（スマホアプリのカメラで撮影している被写体と同様にVRMが動く）

下記がそのデモ（音無し）
![Videotogif (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504562/2eb02f4b-6d1d-d880-af89-e1da93c5748b.gif)



初音ミクのアバター、音楽ファイル（今回は無し）、カメラが①のMMDによるもので、金髪JKが②の方法で動くVRMのアバターとなっている。

MMDの世界に自分の動きを投影したVRMアバターを投入している形で、ちょっと楽しいのではないかと思い作ってみた。

本記事ではこの実装について解説する。
上記の①②をそれぞれ説明する形で進める。但し、②は長いので「WebブラウザでVRMの読み込み」「モーションデータの送信」「VRMへのモーションデータの適用」に分けて記載する。

## MMDをWebブラウザで再生する
知っている人は知っているが、Three.jsを使うことによって、Webブラウザ上でもMMDを再生することが出来る。下記は、MozillaのTakahiroさんによるサンプルサイトだが、普通にいい感じ↓
https://takahirox.github.io/MMDLoader-app/index.html#app_music_live

これを実装するのはさほど難しくなく、下記の様にThree.jsで既に用意されているMMDLoaderというものを使えば、あとはMMDの各種ファイルをそれで読み込めばサクッと出来てしまう。

``` loadMmdCode.js
const modelFile = './resource/models/mikuv4/Miku_V4.pmx';
        const vmdFiles = ['./resource/vmds/phony.vmd'];
        const cameraFiles = ['./resource/vmds/phony_camera.vmd'];
        const audioFile = './resource/audios/phony_fixed.mp3';
        const audioParams = { delayTime: 0};

        helper = new MMDAnimationHelper();

        const mloader = new MMDLoader();
        var mesh = undefined;

        mloader.loadWithAnimation(modelFile, vmdFiles, function (mmd) {
            mloader.loadAnimation(cameraFiles, camera, function (cameraAnimation) {
                new THREE.AudioLoader().load(audioFile, function (buffer) {
                    const audio = new THREE.Audio(listener).setBuffer(buffer);
                    helper.add(audio, audioParams);

                    window.setTimeout( () => { 
                        mesh = mmd.mesh;
                        mesh.castShadow = false;
                        helper.add(mesh, {
                            animation: mmd.animation,
                            physics: true
                        });
                        scene.add(mesh);

                        helper.add(camera, {
                            animation: cameraAnimation
                        });
                    }, vmdLoadDelayMSec);
                    
                    ready = true;
                }, onProgress, null);
            }, onProgress, null);
        }, onProgress, null);

```

こちらの実装の詳細については、下記のThree.jsの公式サイトや@akamecoさんのQiita記事など、多数取り上げられているので割愛したい。
[公式サイト](https://threejs.org/docs/#examples/en/loaders/MMDLoader)
[three.jsでMMDを使う](https://qiita.com/shoichi1023/items/6cbaefe078c33f600bfe)

## WebブラウザでVRMの読み込み
ここではpixiv社のライブラリ「[three-vrm](https://github.com/pixiv/three-vrm)」を利用する。
CDNからでもnpmでもインポート出来る。
これによって拡張？されたThree.jsのGLTFLoaderでVRMファイルを下記の様に読み込む

```loadVrmSample.js
 // VRMの読み込み
        const loader = new GLTFLoader()
        if(dancyType != DANCE_ONLY_MMD)
        {
            loader.load('./vrm/jk.vrm',
                (gltf) => {
                    gltf.scene.traverse( function( node ) {
                        if ( node.isMesh ) { node.castShadow = true; }
                    } );
                    
                    VRM.from(gltf).then((vrm) => {
                        // シーンへの追加
                        leftVrm = vrm;
                        scene.add(vrm.scene)
                    })
                }
            )
        }

```

詳しい使い方は、three-vrm公式サイトの[Example](https://pixiv.github.io/three-vrm/examples/)を見ると、大体やりたいことのサンプルがあるのでそちらを参照頂くのが早い。

## モーションデータの送信
モーションキャプチャに今回はiOSアプリ「TDPT」を利用しているが、Webブラウザへの送信時にはVMCプロトコルベースに勝手に新規に策定（？）したVRMプロトコル on WebRTCフォーマットにパースして送信している。

そのデータ例が下記だが、2行目以降はVMCプロトコル同様に各関節の座標と角度を渡していて、異なるのは1行目に誰のデータかを示すIDが用意されている部分のみ。

```
/VMC/Ext/PeerID,testid
/VMC/Ext/Root/Pos, root, -0.037, -0.090, 0.596, 0.000, 0.000, 0.000, 1.000
/VMC/Ext/Bone/Pos, Hips, 0.0003016568, 0.8938107, 0.006238983, 0.005892843, 0.998634, -0.02814334, 0.04362769
/VMC/Ext/Bone/Pos, LeftUpperLeg, -0.07712238, -0.039702, -0.004908806, 0.12836, 0.007014029, 0.004473836, -0.9916928
/VMC/Ext/Bone/Pos, RightUpperLeg, 0.07712237, -0.03970218, -0.004908811, 0.1009582, -0.007165656, 0.007948338, -0.9948332
/VMC/Ext/Bone/Pos, LeftLowerLeg, 0.01963498, -0.3424794, -0.006634509, -0.4286922, -0.3398216, 0.1506255, -0.8234417
/VMC/Ext/Bone/Pos, RightLowerLeg, -0.01963532, -0.342479, -0.006634448, 0.1789453, -0.003881423, -0.0269276, 0.9834828
/VMC/Ext/Bone/Pos, LeftFoot, 0.01001082, -0.4015794, -0.02094309, -0.1859397, 0.0943882, -0.01822841, -0.9778472
/VMC/Ext/Bone/Pos, RightFoot, -0.01001169, -0.4015797, -0.02094342, -0.0004667639, -0.00860797, -0.000444672, 0.9999627
/VMC/Ext/Bone/Pos, Spine, -6.781192E-09, 0.05212796, 0.009726938, -0.02695227, -0.001907047, 0.008789263, 0.9995963
/VMC/Ext/Bone/Pos, Chest, -1.449371E-08, 0.1108859, 0.0145103, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, Neck, 1.056469E-08, 0.1146152, -0.03339567, -0.001919191, 8.114346E-05, -0.0004588788, -0.9999981
/VMC/Ext/Bone/Pos, Head, -2.249726E-08, 0.07376957, 0.009335324, -0.02476265, 0.1980658, 0.07959835, -0.9766375
/VMC/Ext/Bone/Pos, LeftShoulder, -0.02238563, 0.08726394, -0.02744284, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, RightShoulder, 0.02238562, 0.0872668, -0.0274428, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, LeftUpperArm, -0.0862947, -0.01486909, 0.005363747, 0.01783156, 0.1377123, -0.5569516, -0.8188543
/VMC/Ext/Bone/Pos, RightUpperArm, 0.08629446, -0.01486981, 0.005363718, 0.005612874, -0.02489537, 0.3981973, -0.9169447
/VMC/Ext/Bone/Pos, LeftLowerArm, -0.2101652, -0.009785891, 0.00176318, -0.5166137, 0.4948301, -0.1473635, 0.6830355
/VMC/Ext/Bone/Pos, RightLowerArm, 0.2101669, -0.009788394, 0.001763381, 0.1535443, -0.60361, -0.3241428, 0.7120467
/VMC/Ext/Bone/Pos, LeftHand, -0.2047527, -0.0004464388, 0.01690157, 0.3855804, 0.07508802, -0.09436044, 0.9147599
/VMC/Ext/Bone/Pos, RightHand, 0.2047517, -0.0004454851, 0.0169019, 0.40417, -0.01523766, 0.004531763, 0.9145458
/VMC/Ext/Bone/Pos, LeftToes, -0.001442652, -0.0644471, 0.1105376, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, RightToes, 0.001442045, -0.06444693, 0.1105376, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, LeftEye, -0.02003608, 0.05925202, 0.02838261, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, RightEye, 0.02003603, 0.05925214, 0.02838264, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, LeftThumbProximal, -0.002031922, -0.006911874, 0.01773388, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, LeftThumbIntermediate, -0.03228712, -0.002700686, 0.03340113, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, LeftThumbDistal, -0.02132374, -0.001511931, 0.01903463, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, LeftIndexProximal, -0.06390566, 0.005562067, 0.02144239, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, LeftIndexIntermediate, -0.03282076, -0.0001342297, 0.00514891, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, LeftIndexDistal, -0.02031362, -0.0006734133, 0.002301291, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, LeftMiddleProximal, -0.0661118, 0.008372664, 0.004779827, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, LeftMiddleIntermediate, -0.03690064, -0.001349092, 0.002437942, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, LeftMiddleDistal, -0.02268302, -0.002405524, 0.0009373538, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, LeftRingProximal, -0.06668097, 0.008306146, -0.01096119, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, LeftRingIntermediate, -0.03432286, -0.0005381107, 0.000172466, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, LeftRingDistal, -0.01979637, 0.0007004738, 0.0002486929, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, LeftLittleProximal, -0.06326133, 0.003125191, -0.02679357, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, LeftLittleIntermediate, -0.03139824, -0.0002733469, 2.214313E-05, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, LeftLittleDistal, -0.01805919, 0.0006788969, -0.0009434298, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, RightThumbProximal, 0.002031922, -0.006911874, 0.01773389, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, RightThumbIntermediate, 0.03227693, -0.002678037, 0.03341267, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, RightThumbDistal, 0.02131957, -0.001498699, 0.01904053, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, RightIndexProximal, 0.06390578, 0.005562067, 0.02144244, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, RightIndexIntermediate, 0.03282058, -0.0001343489, 0.005149204, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, RightIndexDistal, 0.02031338, -0.0006735325, 0.002301518, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, RightMiddleProximal, 0.0661118, 0.008372545, 0.00477986, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, RightMiddleIntermediate, 0.03690064, -0.001348972, 0.002438031, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, RightMiddleDistal, 0.02268302, -0.002405405, 0.0009373799, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, RightRingProximal, 0.06668091, 0.008306026, -0.01096114, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, RightRingIntermediate, 0.03432292, -0.0005381107, 0.0001727045, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, RightRingDistal, 0.01979649, 0.0007003546, 0.000248827, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, RightLittleProximal, 0.06326145, 0.003124952, -0.02679349, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, RightLittleIntermediate, 0.0313983, -0.0002729893, 2.286583E-05, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, RightLittleDistal, 0.01805925, 0.0006790161, -0.0009429902, 0, 0, 0, 1
/VMC/Ext/Bone/Pos, UpperChest, 1.251465E-08, 0.1242849, -0.01331276, 0, 0, 0, 1
```

※今回は[TDPT](https://digital-standard.com/tdpt_lp/)を利用したが、このプロトコルに従って送信すればどのようなモーションキャプチャツールでも応用可能。

このデータをSkyway社のサービスを利用して、ターゲットのブラウザにP2P接続してからWebRTCでモーションデータを毎フレームどんどん送り付ける。
構成としては下記イメージの通り。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504562/3625a73b-208d-366d-ea1a-1e7e90a602d8.png)

ちなみに何故、WebRTCにしたかというとWebSocketなどのTCP通信ではネットワーク遅延が発生してしまい20-30fpsでモーションデータを送り続けることが実質出来なかったから。
WebRTCはUDPなのでその悩みを解消することが出来た。

またTDPTはUnityで出来ているので通常、WebRTCをするのは困難だが、[SkywayWebRTC GateWay](https://webrtc.ecl.ntt.com/documents/webrtc-gateway.html)ではそれをさくっと解決してくれているのでこれを利用させて頂いた。
UnityからブラウザへのWebRTC接続、及び、テキスト送信についてはGitHubにサンプルコードを上げているので良ければ参考にしてください⇒　https://github.com/h-nakae/SkyWay-WebRTC-GW-Unity-Sample


## VRMへのモーションデータの適用
ブラウザ側では受け取ったモーションデータでVRMを動かすのだが、その為にはモーションデータを[VRMPoseオブジェクト](https://pixiv.github.io/three-vrm/docs/interfaces/vrmpose.html)にパースしてやる必要がある（three-vrmの仕様）。

ここのパースのコードは勿論、自作しましたがまだきったないので控えさせて頂きます。

当初はこの「Webブラウザ上でVRMを動かす」方法が無いのかな、と思っていたが、けしごむさんの記事[#01 VRMお手軽ポーズ/ポーズ共有機能](https://scrapbox.io/ke456memo/%2301_VRM%E3%81%8A%E6%89%8B%E8%BB%BD%E3%83%9D%E3%83%BC%E3%82%BA%2F%E3%83%9D%E3%83%BC%E3%82%BA%E5%85%B1%E6%9C%89%E6%A9%9F%E8%83%BD)を拝見して、このやり方に気づいた。感謝です！

## 最後に注意
上記の方法をまとめて実施すれば、冒頭のデモが実現されますが、最後に注意すべきはthree-vrm、skyway、Three.jsなどはすべて<b>CDNではなくnpmでインポート</b>した。
そうしないと、MMDLoaderとthree-vrmの共存が出来ないので、そこだけご注意下さい。