メインコンテンツまでスキップ

VMD アニメーションの読み込みと再生

ここでは VMD アニメーションを読み込んで再生します。

VMD アニメーションのダウンロード

まず、読み込むための VMD アニメーションが必要です。

この例では、ほうき堂による メランコリ・ナイト の動画と一緒に配布されている VMD アニメーションを使用します。

アニメーションをダウンロードし、解凍して res/private_test/motion/ フォルダーに配置します。

vscode-file-structure
モーションフォルダー構造の例

VMD アニメーションの読み込み

VmdLoader クラスを使用して VMD アニメーションを読み込みます。

src/sceneBuilder.ts
//...
import { VmdLoader } from "babylon-mmd/esm/Loader/vmdLoader";
//...
export class SceneBuilder implements ISceneBuilder {
public async build(_canvas: HTMLCanvasElement, engine: AbstractEngine): Promise<Scene> {
//...
const vmdLoader = new VmdLoader(scene);
vmdLoader.loggingEnabled = true;

const [mmdAnimation, modelMesh] = await Promise.all([
vmdLoader.loadAsync("motion",
[
"res/private_test/motion/メランコリ・ナイト/メランコリ・ナイト_カメラ.vmd",
"res/private_test/motion/メランコリ・ナイト/メランコリ・ナイト_表情モーション.vmd",
"res/private_test/motion/メランコリ・ナイト/メランコリ・ナイト_リップモーション.vmd",
"res/private_test/motion/メランコリ・ナイト/メランコリ・ナイト.vmd"
]),
LoadAssetContainerAsync(
"res/private_test/model/YYB Hatsune Miku_10th/YYB Hatsune Miku_10th_v1.02.pmx",
scene,
{
pluginOptions: {
mmdmodel: {
loggingEnabled: true,
materialBuilder: materialBuilder
}
}
}
).then(result => {
result.addAllToScene();
return result.rootNodes[0] as MmdMesh;
})
]);
//...

return scene;
}

VMD アニメーションPMX モデルの両方がネットワーク経由で読み込まれるため、Promise.all を使用して並列に読み込むことができます。

そのため、vmdLoader.loadAsyncLoadAssetContainerAsync の非同期オペレーションを一緒に実行します。

loadAsync メソッド

loadAsync メソッドの最初の引数アニメーション名です。この名前は後で内部的に識別に使用されます。

2番目の引数は、読み込む VMD ファイル URL の配列または単一の URL です。複数の VMD ファイルを指定すると、指定された順序で1つのアニメーションにマージされます。

備考

この例では、カメラモーションとダンスモーションを1つのアニメーションにマージしています。これは、MMD アニメーションがモデルモーションデータカメラモーションデータを別々に管理しているため可能です。

N人が踊るアニメーションを再生したい場合は、各モデルに対して個別のアニメーションを作成する必要があります。

MMD ランタイムの作成

VmdLoader で読み込んだアニメーションを再生するには、MmdRuntime インスタンスによって制御される MmdModel または MmdCamera が必要です。

そのため、まず MmdRuntime インスタンスを作成します。

src/sceneBuilder.ts
//...
import { MmdRuntime } from "babylon-mmd/esm/Runtime/mmdRuntime";
//...
export class SceneBuilder implements ISceneBuilder {
public async build(_canvas: HTMLCanvasElement, engine: AbstractEngine): Promise<Scene> {
//...
const mmdRuntime = new MmdRuntime(scene);
mmdRuntime.loggingEnabled = true;
mmdRuntime.register(scene);
mmdRuntime.playAnimation();
//...
return scene;
}
}

MmdRuntimeMMD モデルとカメラを管理し、アニメーションの再生を処理します。register メソッドを呼び出してシーンに更新ロジックを登録し、playAnimation メソッドを呼び出してアニメーションの再生を開始します。

データがなくても再生することが可能で、この場合、アニメーション再生中にリソースを動的に追加することができます。

アニメーションのバインド

MmdRuntime インスタンスを作成した後、createRuntimeAnimation メソッドを使用して MmdModelMmdCamera にアニメーションを適用します。

src/sceneBuilder.ts
//...
export class SceneBuilder implements ISceneBuilder {
public async build(_canvas: HTMLCanvasElement, engine: AbstractEngine): Promise<Scene> {
//...
const [mmdAnimation, modelMesh] = await Promise.all([
//...
]);

const cameraAnimationHandle = mmdCamera.createRuntimeAnimation(mmdAnimation);
mmdCamera.setRuntimeAnimation(cameraAnimationHandle);
mmdRuntime.addAnimatable(mmdCamera);

{
for (const mesh of modelMesh.metadata.meshes) mesh.receiveShadows = true;
shadowGenerator.addShadowCaster(modelMesh);

const mmdModel = mmdRuntime.createMmdModel(modelMesh);
const modelAnimationHandle = mmdModel.createRuntimeAnimation(mmdAnimation);
mmdModel.setRuntimeAnimation(modelAnimationHandle);
}

return scene;
}
}

createRuntimeAnimation メソッドを使用して、MmdAnimation をカメラやモデルにバインドできます。

必要なサイドエフェクトのインポート

アニメーションを再生するために必要なサイドエフェクトをインポートします。

src/sceneBuilder.ts
//...
import "babylon-mmd/esm/Runtime/Animation/mmdRuntimeCameraAnimation";
import "babylon-mmd/esm/Runtime/Animation/mmdRuntimeModelAnimation";
//...

babylon-mmd は、MMD モデルとカメラにアニメーションを適用するためのさまざまな実装を提供しています。

mmdRuntimeCameraAnimationmmdRuntimeModelAnimation は、最もよく使われるカメラとモデルのアニメーション実装です。

これらのサイドエフェクトをインポートしないと、createRuntimeAnimation メソッドがランタイムエラーを引き起こします。

結果

シーンを実行すると、アニメーションが再生されていることが確認できます。

result

完全なコード
src/sceneBuilder.ts
import "@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent";
import "babylon-mmd/esm/Loader/pmxLoader";
import "babylon-mmd/esm/Loader/mmdOutlineRenderer";
import "babylon-mmd/esm/Runtime/Animation/mmdRuntimeCameraAnimation";
import "babylon-mmd/esm/Runtime/Animation/mmdRuntimeModelAnimation";

import type { AbstractEngine } from "@babylonjs/core/Engines/abstractEngine";
import { DirectionalLight } from "@babylonjs/core/Lights/directionalLight";
import { ShadowGenerator } from "@babylonjs/core/Lights/Shadows/shadowGenerator";
import { LoadAssetContainerAsync } from "@babylonjs/core/Loading/sceneLoader";
import { Color3, Color4 } from "@babylonjs/core/Maths/math.color";
import { Vector3 } from "@babylonjs/core/Maths/math.vector";
import { CreateGround } from "@babylonjs/core/Meshes/Builders/groundBuilder";
import { Scene } from "@babylonjs/core/scene";
import { MmdStandardMaterialBuilder } from "babylon-mmd/esm/Loader/mmdStandardMaterialBuilder";
import { VmdLoader } from "babylon-mmd/esm/Loader/vmdLoader";
import { MmdCamera } from "babylon-mmd/esm/Runtime/mmdCamera";
import type { MmdMesh } from "babylon-mmd/esm/Runtime/mmdMesh";
import { MmdRuntime } from "babylon-mmd/esm/Runtime/mmdRuntime";

import type { ISceneBuilder } from "./baseRuntime";

export class SceneBuilder implements ISceneBuilder {
public async build(_canvas: HTMLCanvasElement, engine: AbstractEngine): Promise<Scene> {
const materialBuilder = new MmdStandardMaterialBuilder();
const scene = new Scene(engine);
scene.clearColor = new Color4(0.95, 0.95, 0.95, 1.0);
scene.ambientColor = new Color3(0.5, 0.5, 0.5);

const mmdCamera = new MmdCamera("MmdCamera", new Vector3(0, 10, 0), scene);

const directionalLight = new DirectionalLight("DirectionalLight", new Vector3(0.5, -1, 1), scene);
directionalLight.intensity = 1.0;
directionalLight.autoCalcShadowZBounds = true;

const shadowGenerator = new ShadowGenerator(1024, directionalLight, true);
shadowGenerator.transparencyShadow = true;
shadowGenerator.usePercentageCloserFiltering = true;
shadowGenerator.forceBackFacesOnly = true;
shadowGenerator.filteringQuality = ShadowGenerator.QUALITY_MEDIUM;
shadowGenerator.frustumEdgeFalloff = 0.1;

const ground = CreateGround("ground1", { width: 100, height: 100, subdivisions: 2, updatable: false }, scene);
ground.receiveShadows = true;

const vmdLoader = new VmdLoader(scene);
vmdLoader.loggingEnabled = true;

const mmdRuntime = new MmdRuntime(scene);
mmdRuntime.loggingEnabled = true;
mmdRuntime.register(scene);
mmdRuntime.playAnimation();

const [mmdAnimation, modelMesh] = await Promise.all([
vmdLoader.loadAsync("motion",
[
"res/private_test/motion/メランコリ・ナイト/メランコリ・ナイト_カメラ.vmd",
"res/private_test/motion/メランコリ・ナイト/メランコリ・ナイト_表情モーション.vmd",
"res/private_test/motion/メランコリ・ナイト/メランコリ・ナイト_リップモーション.vmd",
"res/private_test/motion/メランコリ・ナイト/メランコリ・ナイト.vmd"
]),
LoadAssetContainerAsync(
"res/private_test/model/YYB Hatsune Miku_10th/YYB Hatsune Miku_10th_v1.02.pmx",
scene,
{
pluginOptions: {
mmdmodel: {
loggingEnabled: true,
materialBuilder: materialBuilder
}
}
}
).then(result => {
result.addAllToScene();
return result.rootNodes[0] as MmdMesh;
})
]);

const cameraAnimationHandle = mmdCamera.createRuntimeAnimation(mmdAnimation);
mmdCamera.setRuntimeAnimation(cameraAnimationHandle);
mmdRuntime.addAnimatable(mmdCamera);

{
for (const mesh of modelMesh.metadata.meshes) mesh.receiveShadows = true;
shadowGenerator.addShadowCaster(modelMesh);
const mmdModel = mmdRuntime.createMmdModel(modelMesh);
const modelAnimationHandle = mmdModel.createRuntimeAnimation(mmdAnimation);
mmdModel.setRuntimeAnimation(modelAnimationHandle);
}

return scene;
}
}