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

シーンの詳細設定

最後に、シーンの詳細設定を構成します。このステップでは、以下のタスクを実行します:

  • ローディングスクリーンの表示: モデルとアニメーションの読み込み中にローディングスクリーンを表示します。
  • SDEF (球面変形) サポートの追加: SDEF を使用するモデルのためにエンジンにシェーダーサポートを追加します。
  • BMP テクスチャローダーの登録: MMD モデルから BMP テクスチャを正しく読み込むために BMP テクスチャローダーを登録します。
  • プレイヤーコントロールの表示: アニメーション再生を制御するためのプレイヤーコントロール UI を表示します。

ローディングスクリーンの表示

シーンの読み込み中にローディングスクリーンを表示し、読み込みステータスを更新する方法を見てみましょう。

まず、"@babylonjs/core/Loading/loadingScreen" をインポートしてローディングスクリーン機能を有効にします。

src/sceneBuilder.ts
import "@babylonjs/core/Loading/loadingScreen";
//...

ローディングスクリーンを表示するには engine.displayLoadingUI() を呼び出し、読み込みが完了したら engine.hideLoadingUI() を呼び出します。

ローディングスクリーンを非表示にするタイミングは、scene.onAfterRenderObservable を使用してシーンの最初のレンダリングが完了した後に設定するのが最適です。

src/sceneBuilder.ts
//...
export class SceneBuilder implements ISceneBuilder {
public async build(_canvas: HTMLCanvasElement, engine: AbstractEngine): Promise<Scene> {
engine.displayLoadingUI();

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

const [[mmdRuntime, physicsRuntime], mmdAnimation, modelMesh] = await Promise.all([
//...
]);

scene.onAfterRenderObservable.addOnce(() => engine.hideLoadingUI());
//...
}
}

ローディングステータスの更新

vmdLoader の loadAsync メソッドと LoadAssetContainerAsync 関数は、読み込み進捗情報を提供する onProgress コールバックをサポートしています。

これを使用してローディングステータスを更新できます。

ただし、WebAssembly 実装の MMD 物理エンジンの初期化には進捗を追跡する方法がないため、読み込み開始と完了のポイントでのみステータスを更新します。

ローディングステータスを表示するために engine.loadingUIText を使用します。

src/sceneBuilder.ts
//...
export class SceneBuilder implements ISceneBuilder {
public async build(_canvas: HTMLCanvasElement, engine: AbstractEngine): Promise<Scene> {
//...
const loadingTexts: string[] = [];
const updateLoadingText = (updateIndex: number, text: string): void => {
loadingTexts[updateIndex] = text;
engine.loadingUIText = "<br/><br/><br/><br/>" + loadingTexts.join("<br/><br/>");
};

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

const [[mmdRuntime, physicsRuntime], mmdAnimation, modelMesh] = await Promise.all([
(async(): Promise<[MmdRuntime, MultiPhysicsRuntime]> => {
updateLoadingText(0, "Loading mmd runtime...");
const wasmInstance = await GetMmdWasmInstance(new MmdWasmInstanceTypeMPR());
updateLoadingText(0, "Loading mmd runtime... Done");

const physicsRuntime = new MultiPhysicsRuntime(wasmInstance);
physicsRuntime.setGravity(new Vector3(0, -98, 0));
physicsRuntime.register(scene);

const mmdRuntime = new MmdRuntime(scene, new MmdBulletPhysics(physicsRuntime));
mmdRuntime.loggingEnabled = true;
mmdRuntime.register(scene);
mmdRuntime.setAudioPlayer(audioPlayer);
mmdRuntime.playAnimation();
return [mmdRuntime, physicsRuntime];
})(),
vmdLoader.loadAsync("motion",
[
"res/private_test/motion/メランコリ・ナイト/メランコリ・ナイト_カメラ.vmd",
"res/private_test/motion/メランコリ・ナイト/メランコリ・ナイト_表情モーション.vmd",
"res/private_test/motion/メランコリ・ナイト/メランコリ・ナイト_リップモーション.vmd",
"res/private_test/motion/メランコリ・ナイト/メランコリ・ナイト.vmd"
],
(event) => updateLoadingText(0, `Loading motion... ${event.loaded}/${event.total} (${Math.floor(event.loaded * 100 / event.total)}%)`)),
LoadAssetContainerAsync(
"res/private_test/model/YYB Hatsune Miku_10th/YYB Hatsune Miku_10th_v1.02.pmx",
scene,
{
onProgress: (event) => updateLoadingText(1, `Loading model... ${event.loaded}/${event.total} (${Math.floor(event.loaded * 100 / event.total)}%)`),
pluginOptions: {
mmdmodel: {
loggingEnabled: true,
materialBuilder: materialBuilder
}
}
}
).then(result => {
result.addAllToScene();
return result.rootNodes[0] as MmdMesh;
})
]);
//...
}
}

SDEF サポートの追加

SDEF (球面変形) は、MMD モデルで使用されるスキニングメソッドの 1 つです。SDEF を使用するモデルを適切にレンダリングするには、SDEF のシェーダーサポートが必要です。

babylon-mmd は、シェーダーコンパイル関数をオーバーライドすることで SDEF サポートを追加する SdefInjector ユーティリティを提供しています。これは非常にトリッキーな方法ですが、MMD の動作を適切に再現するために必要です。

src/sceneBuilder.ts
import { SdefInjector } from "babylon-mmd/esm/Loader/sdefInjector";
//...
export class SceneBuilder implements ISceneBuilder {
public async build(_canvas: HTMLCanvasElement, engine: AbstractEngine): Promise<Scene> {
SdefInjector.OverrideEngineCreateEffect(engine);
//...
}
}

BMP テクスチャローダーの登録

MMD とブラウザ間の BMP テクスチャローダー実装の違いのため、Babylon.js で MMD モデルから BMP テクスチャを正しく読み込むには、個別の BMP テクスチャローダーを登録する必要があります。

この例で現在使用している "YYB Hatsune Miku_10th" モデルは BMP テクスチャを使用していないため、このステップをスキップしてもモデルは正しく表示されます。しかし、BMP テクスチャを使用するモデルを読み込む場合、このステップを実行しないとテクスチャが正しく表示されない可能性があります。

src/sceneBuilder.ts
import { RegisterDxBmpTextureLoader } from "babylon-mmd/esm/Loader/registerDxBmpTextureLoader";
//...
export class SceneBuilder implements ISceneBuilder {
public async build(_canvas: HTMLCanvasElement, engine: AbstractEngine): Promise<Scene> {
//...
RegisterDxBmpTextureLoader();
//...
}
}

プレイヤーコントロールの表示

babylon-mmd は、MMD アニメーションの再生を制御するための MmdPlayerControl ユーティリティを提供しています。このユーティリティを使用して、ビデオプレイヤーに似たコントロール UI を表示できます。

src/sceneBuilder.ts
import { MmdPlayerControl } from "babylon-mmd/esm/Runtime/Util/mmdPlayerControl";
//...
export class SceneBuilder implements ISceneBuilder {
public async build(_canvas: HTMLCanvasElement, engine: AbstractEngine): Promise<Scene> {
//...
const mmdPlayerControl = new MmdPlayerControl(scene, mmdRuntime, audioPlayer);
mmdPlayerControl.showPlayerControl();
//...
}
}

MmdPlayerControlプロダクション対応の UI コンポーネントではなく、単に MMD アニメーションの再生をテストするために提供されています。そのため、プロダクション環境では独自の UI を実装することをお勧めします。

結果

シーンの読み込み中にローディングスクリーンが表示されプレイヤーコントロール UI が表示されるようになりました。

完全なコード
src/sceneBuilder.ts
import "@babylonjs/core/Loading/loadingScreen";
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 { RegisterDxBmpTextureLoader } from "babylon-mmd/esm/Loader/registerDxBmpTextureLoader";
import { SdefInjector } from "babylon-mmd/esm/Loader/sdefInjector";
import { VmdLoader } from "babylon-mmd/esm/Loader/vmdLoader";
import { StreamAudioPlayer } from "babylon-mmd/esm/Runtime/Audio/streamAudioPlayer";
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 { MmdWasmInstanceTypeMPR } from "babylon-mmd/esm/Runtime/Optimized/InstanceType/multiPhysicsRelease";
import { GetMmdWasmInstance } from "babylon-mmd/esm/Runtime/Optimized/mmdWasmInstance";
import { MultiPhysicsRuntime } from "babylon-mmd/esm/Runtime/Optimized/Physics/Bind/Impl/multiPhysicsRuntime";
import { MotionType } from "babylon-mmd/esm/Runtime/Optimized/Physics/Bind/motionType";
import { PhysicsStaticPlaneShape } from "babylon-mmd/esm/Runtime/Optimized/Physics/Bind/physicsShape";
import { RigidBody } from "babylon-mmd/esm/Runtime/Optimized/Physics/Bind/rigidBody";
import { RigidBodyConstructionInfo } from "babylon-mmd/esm/Runtime/Optimized/Physics/Bind/rigidBodyConstructionInfo";
import { MmdBulletPhysics } from "babylon-mmd/esm/Runtime/Optimized/Physics/mmdBulletPhysics";
import { MmdPlayerControl } from "babylon-mmd/esm/Runtime/Util/mmdPlayerControl";

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

export class SceneBuilder implements ISceneBuilder {
public async build(_canvas: HTMLCanvasElement, engine: AbstractEngine): Promise<Scene> {
SdefInjector.OverrideEngineCreateEffect(engine);
RegisterDxBmpTextureLoader();

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 audioPlayer = new StreamAudioPlayer(scene);
audioPlayer.source = "res/private_test/motion/メランコリ・ナイト/melancholy_night.mp3";

// show loading screen
engine.displayLoadingUI();

const loadingTexts: string[] = [];
const updateLoadingText = (updateIndex: number, text: string): void => {
loadingTexts[updateIndex] = text;
engine.loadingUIText = "<br/><br/><br/><br/>" + loadingTexts.join("<br/><br/>");
};

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

const [[mmdRuntime, physicsRuntime], mmdAnimation, modelMesh] = await Promise.all([
(async(): Promise<[MmdRuntime, MultiPhysicsRuntime]> => {
updateLoadingText(0, "Loading mmd runtime...");
const wasmInstance = await GetMmdWasmInstance(new MmdWasmInstanceTypeMPR());
updateLoadingText(0, "Loading mmd runtime... Done");

const physicsRuntime = new MultiPhysicsRuntime(wasmInstance);
physicsRuntime.setGravity(new Vector3(0, -98, 0));
physicsRuntime.register(scene);

const mmdRuntime = new MmdRuntime(scene, new MmdBulletPhysics(physicsRuntime));
mmdRuntime.loggingEnabled = true;
mmdRuntime.register(scene);
mmdRuntime.setAudioPlayer(audioPlayer);
mmdRuntime.playAnimation();
return [mmdRuntime, physicsRuntime];
})(),
vmdLoader.loadAsync("motion",
[
"res/private_test/motion/メランコリ・ナイト/メランコリ・ナイト_カメラ.vmd",
"res/private_test/motion/メランコリ・ナイト/メランコリ・ナイト_表情モーション.vmd",
"res/private_test/motion/メランコリ・ナイト/メランコリ・ナイト_リップモーション.vmd",
"res/private_test/motion/メランコリ・ナイト/メランコリ・ナイト.vmd"
],
(event) => updateLoadingText(0, `Loading motion... ${event.loaded}/${event.total} (${Math.floor(event.loaded * 100 / event.total)}%)`)),
LoadAssetContainerAsync(
"res/private_test/model/YYB Hatsune Miku_10th/YYB Hatsune Miku_10th_v1.02.pmx",
scene,
{
onProgress: (event) => updateLoadingText(1, `Loading model... ${event.loaded}/${event.total} (${Math.floor(event.loaded * 100 / event.total)}%)`),
pluginOptions: {
mmdmodel: {
loggingEnabled: true,
materialBuilder: materialBuilder
}
}
}
).then(result => {
result.addAllToScene();
return result.rootNodes[0] as MmdMesh;
})
]);

scene.onAfterRenderObservable.addOnce(() => engine.hideLoadingUI());

const mmdPlayerControl = new MmdPlayerControl(scene, mmdRuntime, audioPlayer);
mmdPlayerControl.showPlayerControl();

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);
}

const info = new RigidBodyConstructionInfo(physicsRuntime.wasmInstance);
info.motionType = MotionType.Static;
info.shape = new PhysicsStaticPlaneShape(physicsRuntime, new Vector3(0, 1, 0), 0);
const groundBody = new RigidBody(physicsRuntime, info);
physicsRuntime.addRigidBodyToGlobal(groundBody);

return scene;
}
}

次のステップ

babylon-mmd の基本的な使い方をすべて学習しました!次に、リファレンス セクションをご覧ください。このセクションでは、さまざまなオプションや高度な機能の詳細な説明が提供されています。