#three.js でMMDを使う
###○ この記事で書くこと
- MMDLoaderを使ったMMDモデルの読み込み
- アニメーションの付け方
- アニメーションの発火・切り替え
three.jsの基礎はこちら:Hello three.js World
###準備
この記事で使うmmdモデルとvmdのモーションファイルをダウンロードします:GoogleDrive
モデルはLat様のものを拝借しており、無改変で使用させて頂いております。
モデルの使用、再配布、モーションの利用、再配布についてもそれぞれファイル内に同包されているRedeMeに従ってください。
###〇 MMDLoaderを使ったMMDモデルの読み込み
細かい説明は基礎の記事を読んでください。
ここでは読み込み部分のみを中心に書きます。
###ファイル構成
- index.html
- js
- main.js
- three.min.js
- TGALoader.js
- mmdparser.min.js
- MMDLoader.js
- css
- main.css
- mmd
- miku
mmdparser.min.jsはthree.js/examples/js/libsの中に、それ以外のLoader関係はthree.js/examples/js/loadersの中にあります。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<meta http-equiv="Content-Script-Type" content="text/javascript" />
<title>sitename</title>
<link rel="shortcut icon" href="./img/favicon.png" /> <!-- favicon -->
<link rel="stylesheet" href="./css/main.css" /><!-- css -->
<!-- script -->
<script src="js/three.min.js"></script>
<script src="js/TGALoader.js"></script>
<script src="js/mmdparser.min.js"></script>
<script src="js/MMDLoader.js"></script>
<script src="js/main.js"></script>
</head>
<body>
</body>
</html>
@charset"utf-8";
*{
margin:0;
padding:0;
}
body{
width:100%;
height:100%;
overflow:hidden;
}
@media screen and (max-width:600px){
}
@media screen and (max-width:400px){
}
var mesh, camera, scene, renderer;
var windowWidth = window.innerWidth;
var windowHeight = window.innerHeight;
window.onload = function() {
init();
render();
};
function init() {
// シーンの作成
scene = new THREE.Scene();
// 光の作成
var ambient = new THREE.AmbientLight(0xeeeeee);
scene.add(ambient);
// 画面表示の設定
renderer = new THREE.WebGLRenderer({ alpha: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xcccccc, 0);
document.body.appendChild(renderer.domElement);
// カメラの作成
camera = new THREE.PerspectiveCamera(40, windowWidth / windowHeight, 1, 1000);
camera.position.set(0, 10, 60);
// モデルとモーションの読み込み準備
var modelFile = "./mmd/miku/Lat式ミクVer2.31_Normal.pmd";
var onProgress = function(xhr) {};
var onError = function(xhr) {
console.log("load mmd error");
};
//MMDLoaderをインスタンス化
var loader = new THREE.MMDLoader();
//loadModelメソッドにモデルのPATH
//コールバックに画面に描画するための諸々のプログラムを書く
loader.loadModel(
modelFile,
function(object) {
mesh = object;
mesh.position.set(0, -10, 0);
mesh.rotation.set(0, 0, 0);
scene.add(mesh);
},
onProgress,
onError
);
// リサイズ時
window.addEventListener("resize", onWindowResize, false);
}
function onWindowResize() {
windowWidth = window.innerWidth;
windowHeight = window.innerHeight;
camera.aspect = windowWidth / windowHeight;
camera.updateProjectionMatrix();
renderer.setSize(windowWidth, windowHeight);
}
function render() {
requestAnimationFrame(render);
renderer.clear();
renderer.render(scene, camera);
}
気をつけなければいけないところは、MMDLoaderが内部的に、TGALoaderとmmdparserに依存しているので、その2つを書いてから、MMDLoaderを書かなければならないというところです。
それ以外は、そこまで難しいコードもなくできるかと思います。
###〇 アニメーションの付け方
アニメーション関連は、モデル読み込みのコールバックでモデルにアニメーションを関連付けします。
またアニメーションの管理はMMDHelperを使います。
###ファイル構成
- index.html
- js
- main.js
- three.min.js
- TGALoader.js
- mmdparser.min.js
- ammo.js
- CCDIKSolver.js
- MMDPhysics.js
- MMDLoader.js
- css
- main.css
- mmd
- miku
- vmd
まず、htmlの変更部分。
変更前
<script src="js/three.min.js"></script>
<script src="js/TGALoader.js"></script>
<script src="js/mmdparser.min.js"></script>
<script src="js/MMDLoader.js"></script>
<script src="js/main.js"></script>
変更後
<script src="js/three.min.js"></script>
<script src="js/mmdparser.min.js"></script>
<script src="js/TGALoader.js"></script>
<script src="js/ammo.js"></script>
<script src="js/CCDIKSolver.js"></script>
<script src="js/MMDPhysics.js"></script>
<script src="js/MMDLoader.js"></script>
<script src="js/main.js"></script>
次にjsは、流れを見たほうがわかりやすいと思うので、全部載せます。
var mesh, camera, scene, renderer;
var helper;
var windowWidth = window.innerWidth;
var windowHeight = window.innerHeight;
var clock = new THREE.Clock();
var controls;
var modelReady = false;
var vmdIndex = 0;
window.onload = function() {
init();
render();
};
function init() {
// シーンの作成
scene = new THREE.Scene();
// 光の作成
var ambient = new THREE.AmbientLight(0xeeeeee);
scene.add(ambient);
// 画面表示の設定
renderer = new THREE.WebGLRenderer({ alpha: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xcccccc, 0);
document.body.appendChild(renderer.domElement);
// カメラの作成
camera = new THREE.PerspectiveCamera(40, windowWidth / windowHeight, 1, 1000);
camera.position.set(0, 10, 60);
// モデルとモーションの読み込み準備
var modelFile = "./mmd/miku/Lat式ミクVer2.31_Normal.pmd";
//vmdのファイルPATHとそれに対応するタグの配列
var vmdFiles = [
{
name: "あはははは",
file: "./mmd/vmd/あはははは.vmd"
}
];
var onProgress = function(xhr) {};
var onError = function(xhr) {
console.log("load mmd error");
}; //アニメーションをつけるためのヘルパー
helper = new THREE.MMDHelper(); //MMDLoaderをインスタンス化
var loader = new THREE.MMDLoader();
//loadModelメソッドにモデルのPATH
//コールバックに画面に描画するための諸々のプログラムを書く
loader.loadModel(
modelFile,
function(object) {
mesh = object;
mesh.position.set(0, -10, 0);
mesh.rotation.set(0, 0, 0);
scene.add(mesh);
helper.add(mesh);
//vmdFileがあれば対応付けする
if (vmdFiles && vmdFiles.length !== 0) {
function readAnime() {
var vmdFile = vmdFiles[vmdIndex].file;
//vmdのローダー
loader.loadVmd(
vmdFile,
function(vmd) {
loader.createAnimation(mesh, vmd, vmdFiles[vmdIndex].name);
vmdIndex++;
if (vmdIndex < vmdFiles.length) {
//配列分読み込むまで再帰呼び出し
readAnime();
} else {
//読み込み終わったらmesh(モデル)に対してアニメーションをセット
helper.setAnimation(mesh);
helper.setPhysics(mesh);
helper.unifyAnimationDuration({ afterglow: 1.0 });
mesh.mixer.stopAllAction();
//実行
selectAnimation(mesh, 0, true);
}
},
onProgress,
onError
);
}
readAnime();
}
modelReady = true;
},
onProgress,
onError
);
// リサイズ時
window.addEventListener("resize", onWindowResize, false);
}
//通常の関連付けだとモーフファイル(表情のモーションファイル)と分離されてしまうのでくっつけて実行するためのヘルパー
function selectAnimation(mesh, index, loop) {
var clip, mclip, action, morph, i;
i = 2 * index;
//一つのアニメーションを抜き出し(モーフじゃない方)
clip = mesh.geometry.animations[i];
//ミキサーにセット
action = mesh.mixer.clipAction(clip);
//対応するモーフを抜き出し
mclip = mesh.geometry.animations[i + 1];
//ミキサーにセット
morph = mesh.mixer.clipAction(mclip);
//ループの設定、
if (loop) {
action.repetitions = "Infinity";
morph.repetitions = "Infinity";
} else {
action.repetitions = 0;
morph.repetitions = 0;
}
//一旦全部止めて
mesh.mixer.stopAllAction();
//同時に動かす
action.play();
morph.play();
}
function onWindowResize() {
windowWidth = window.innerWidth;
windowHeight = window.innerHeight;
camera.aspect = windowWidth / windowHeight;
camera.updateProjectionMatrix();
renderer.setSize(windowWidth, windowHeight);
}
function render() {
requestAnimationFrame(render);
if (modelReady) {
//モデルが読み込まれていればアニメーションを動かす
helper.animate(clock.getDelta());
renderer.render(scene, camera);
} else {
//そうじゃ無ければいつも通り
renderer.clear();
renderer.render(scene, camera);
}
}
まず、jsファイルの増えた部分ですが、
ammo.jsはthree.js/examples/js/libsの中に、
CCDIKSolver.jsとMMDPhysics.jsはthree.js/examples/js/animationの中にあります。
これらは、MMDHelperを動かすために必要なので例のごとくMMDLoaderの前に読み込みましょう。
あとは、ソースのコメントを読んでいただければなんとなくわかると思います。
vmdファイルは、内部的にアクションファイル(身体を動かすファイル)とモーフファイル(表情を動かすファイル)に分かれており、それがLoaderで読み込んだ際に分離してしまいます。
そのため、配列の中身は [action1, morph1, action2, morph2] といった形になってしまいます。
ここでは、それを一つのアクションとして読み込むために、selectAnimationといったヘルパーとなる関数を定義しています。
〇 アニメーションの発火・切り替え
ここでは、ゲームのホーム画面などでよくあるユースケースを想定する。
通常時は、リピートアニメーションが動いていて、画面タップ時にランダムなアニメーションを発火し、そのアニメーションが終了したときにもとのリピートアニメーションに戻る。
###ファイル構成
- index.html
- js
- main.js
- three.min.js
- TGALoader.js
- mmdparser.min.js
- ammo.js
- CCDIKSolver.js
- MMDPhysics.js
- MMDLoader.js
- css
- main.css
- mmd
- miku
- vmd
- vmd_repeat
追記部分
グローバル変数に下記を追加
var miku = new Object();
var setFinish = false;
init()を下記のように編集
function init() {
// シーンの作成
scene = new THREE.Scene();
// 光の作成
var ambient = new THREE.AmbientLight(0xeeeeee);
scene.add(ambient);
// 画面表示の設定
renderer = new THREE.WebGLRenderer({ alpha: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xcccccc, 0);
document.body.appendChild(renderer.domElement);
// カメラの作成
camera = new THREE.PerspectiveCamera(40, windowWidth / windowHeight, 1, 1000);
camera.position.set(0, 10, 60);
// モデルとモーションの読み込み準備
var modelFile = "./mmd/miku/Lat式ミクVer2.31_Normal.pmd";
//vmdのファイルPATHとそれに対応するタグの配列
var vmdFiles = [
{name: 0, file: './mmd/vmd_repeat/repeat.vmd'},
{name: 1, file: './mmd/vmd/あはははは.vmd'},
{name: 2, file: './mmd/vmd/ばーん!.vmd'},
{name: 3, file: './mmd/vmd/かがむ1.vmd'},
{name: 4, file: './mmd/vmd/投げキッス.vmd'},
{name: 5, file: './mmd/vmd/ポンと手を打つ.vmd'}
];
var onProgress = function(xhr) {};
var onError = function(xhr) {
console.log("load mmd error");
};
//アニメーションをつけるためのヘルパー
helper = new THREE.MMDHelper();
//MMDLoaderをインスタンス化
loader = new THREE.MMDLoader();
//loadModelメソッドにモデルのPATH
//コールバックに画面に描画するための諸々のプログラムを書く
loader.loadModel(
modelFile,
function(object) {
mesh = object;
mesh.position.set(0, -10, 0);
mesh.rotation.set(0, 0, 0);
scene.add(mesh);
helper.add(mesh);
miku = mesh;
//vmdFileがあれば対応付けする
if (vmdFiles && vmdFiles.length !== 0) {
function readAnime() {
var vmdFile = vmdFiles[vmdIndex].file;
//vmdのローダー
loader.loadVmd(
vmdFile,
function(vmd) {
loader.createAnimation(mesh, vmd, vmdFiles[vmdIndex].name);
vmdIndex++;
if (vmdIndex < vmdFiles.length) {
//配列分読み込むまで再帰呼び出し
readAnime();
} else {
//読み込み終わったらmesh(モデル)に対してアニメーションをセット
helper.setAnimation(mesh);
helper.setPhysics(mesh);
helper.unifyAnimationDuration({ afterglow: 1.0 });
mesh.mixer.stopAllAction();
//実行
selectAnimation(mesh, 0, true);
}
},
onProgress,
onError
);
}
readAnime();
}
modelReady = true;
},
onProgress,
onError
);
// リサイズ時
window.addEventListener("resize", onWindowResize, false);
document.body.onclick = function(){
//アニメーションが終わったときはループアニメーションに再セット
if(!setFinish){
miku.mixer.addEventListener('finished', event => {
miku.mixer.clipAction(THREE.AnimationClip.findByName(miku.geometry.animations,0)).repetitions = 'Infinity';
miku.mixer.clipAction(THREE.AnimationClip.findByName(miku.geometry.animations,0)).play();
});
setFinish = true;
}
//Clickでランダムなアニメーションを再生
var index = Math.floor(Math.random()*5+0.9);
//アニメーションをすべて止める
miku.mixer.stopAllAction();
//リピートしないように設定
miku.mixer.clipAction(THREE.AnimationClip.findByName(miku.geometry.animations,index)).repetitions = 0;
//選択したアニメーションが途中で止まった状態かもしれないので一度リセット
miku.mixer.clipAction(THREE.AnimationClip.findByName(miku.geometry.animations,index)).reset();
//アニメーションを開始
miku.mixer.clipAction(THREE.AnimationClip.findByName(miku.geometry.animations,index)).play();
};
}
アニメーションが終了したときに再セットする部分で確か詰まった記憶があったりなかったり、、、、。
ここはmxierに対してfinishでイベントリスナーをつけてあげるとうまく行きます。
(たしかここはデフォルトで用意されてるメソッドじゃ動かなくてイベント変数を受け取るように書き直した気がするので本家のどこかに似たようなメソッドがあるかも知れない)
何しろ一年前に組んだコードの記事を書いてるので曖昧ですみません。
中身のコードはすべてテストして動くことを確認してあるのでそれでご容赦ください。