4
Help us understand the problem. What are the problem?

posted at

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

実装したもの

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

下記がそのデモ(音無し)
Videotogif (1).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記事など、多数取り上げられているので割愛したい。
公式サイト
three.jsでMMDを使う

WebブラウザでVRMの読み込み

ここでは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を見ると、大体やりたいことのサンプルがあるのでそちらを参照頂くのが早い。

モーションデータの送信

モーションキャプチャに今回は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を利用したが、このプロトコルに従って送信すればどのようなモーションキャプチャツールでも応用可能。

このデータをSkyway社のサービスを利用して、ターゲットのブラウザにP2P接続してからWebRTCでモーションデータを毎フレームどんどん送り付ける。
構成としては下記イメージの通り。
キャプチャ.PNG

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

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

VRMへのモーションデータの適用

ブラウザ側では受け取ったモーションデータでVRMを動かすのだが、その為にはモーションデータをVRMPoseオブジェクトにパースしてやる必要がある(three-vrmの仕様)。

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

当初はこの「Webブラウザ上でVRMを動かす」方法が無いのかな、と思っていたが、けしごむさんの記事#01 VRMお手軽ポーズ/ポーズ共有機能を拝見して、このやり方に気づいた。感謝です!

最後に注意

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
4
Help us understand the problem. What are the problem?