---
# System prepended metadata

title: 20241224_Babylon.jsでMMDに挑戦してみた。
tags: [ブログ記事]

---

# 20241224_Babylon.jsでMMDに挑戦してみた。
(例：20210901_LT会を開催しました)
###### tags: `ブログ記事`

- [ ] 公開（ブログ公開担当者がいじるやつ）


太字斜体で書いてある内容を埋めて行ってください．
文章，画像は太字斜体の下の行に入れてください．
最初に書く時はREADMEを読んだら読むといいと思います．

<br>

## 表示されない情報
***書いた人の名前(自己紹介文と同じ名前)***
{
やま
}


***記事の簡単な説明(検索した時にタイトルの下に出てくる文章)***
{
Babylon.jsとbabylon-mmdを使用して、Webブラウザ上でMMDモデルを表示し、モーションを再生する方法を紹介します。
}


<br>

## 表示される部分
***サムネイル画像***
{
![adocare-3adokare3](https://hackmd.io/_uploads/SJwMIHwEyl.png)
}

***カテゴリ***
以下の中から該当しそうなカテゴリを選択してください
※一つだけ選択してください
- [ ] 対外活動
- [ ] 活動の様子
- [x] メンバーの趣味
- [ ] 実務訓練体験記
- [x] NUTMEG Advent Calendar 2024

***タグ***
以下の中から該当しそうなカテゴリを選択してください．当てはまる物がない場合は適宜追加してください.
言語
- [x] HTML
- [x] CSS
- [ ] Python
- [ ] Go
- [ ] Ruby
- [ ] JavaScript
- [x] TypeScript
- [ ] Dart
- [ ] Rust
- [ ] Kotlin
- [ ] Swift

フレームワーク・ライブラリ
- [ ] Ruby on rails
- [ ] Vue.js
- [ ] Nuxt.js
- [ ] React.js
- [ ] Next.js
- [ ] Gin
- [ ] Flluter
- [x] Babylon.js

ツール
- [x] GitHub
- [ ] ターミナル
- [ ] WSL
- [ ] Ubuntu
- [ ] Docker
- [ ] Raspberry Pi
- [ ] Figma
- [x] MMD

分野
- [ ] チームづくり
- [x] フロントエンド
- [ ] バックエンド
- [ ] インフラ
- [ ] Web-design
- [ ] API関係
 


---

***以下に本文を記載してください***

## はじめに

こんにちは！NUTMEGの「やま」です。アドベントカレンダー24日目を担当します。

JavaScriptのWebGLライブラリといえば[Three.js](https://threejs.org/)が有名ですが、Microsoft社が開発している[Babylon.js](https://www.babylonjs.com/)も活発に開発されています。

3D制作やゲーム開発にはUnityが有名どころですが、Web開発であるとThree.jsやBabylon.jsは開発環境の構築やデプロイが手軽なところが良いですね。

久しぶりにMMDを触っていたこともあり、追加のソフトウェアなしで実現できるWebアプリケーションとしてのMMDビューワーの実装に取り組んでみました。Babylon.jsでMMDモデルやモーションを動作させることについては[babylon-mmd](https://github.com/noname0310/babylon-mmd)というライブラリが存在しており、ドキュメントも整備されているため比較的容易に実装することができました。

ドキュメントやサンプル例を参考にしながら作成した、初学者の記録として見ていただければ幸いです。MMDモデルが表示され、音楽に合わせて踊らせることができるところまでの過程を紹介していこうと思います。

今回作成したリポジトリは以下からアクセスできます。
[test-babylon-mmd](https://github.com/TkymHrt/test-babylon-mmd)

## 1. 環境構築

今回使用した主要な技術は以下の通りです。

-   **Vite**: 高速なビルドツールと開発サーバー。
-   **TypeScript**: 型安全な JavaScript
-   **Biome**: コードのフォーマットとリンティングのためのツール。
-   **Babylon.js**: 強力な 3D レンダリングエンジン。
-   **babylon-mmd**: Babylon.js で MMD モデルを読み込み、表示するためのライブラリ。

---

### プロジェクトのセットアップ

:::info
Bunを使用していますがnpmなどお好みのランタイムで大丈夫です。
:::

#### テンプレートの作成

1. Bun + Vite のテンプレートを作成します。

    ```bash
    bun create vite [your-project-name]
    ```
    **実行結果**

    ```bash
    ✔ Select a framework: › Vanilla
    ✔ Select a variant: › TypeScript

    Scaffolding project in /home/your-project-name

    Done. Now run:

      cd your-project-name
      bun install
      bun run dev
    ```

2. 続けて指示通りプロジェクトのあるディレクトリに移動してコマンドを実行します。

    ```bash
      cd your-project-name
      bun install
    ```

#### Biomeのインストール

1. Biomeのセットアップを行います。

    ```bash
    bun add --dev --exact @biomejs/biome
    bunx biome init
    ```

    `biome.json`はお好みで設定してください。

    **設定例**
    ```json
    {
        "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
        "vcs": {
            "enabled": false,
            "clientKind": "git",
            "useIgnoreFile": false
        },
        "files": {
            "ignoreUnknown": false,
            "ignore": []
        },
        "formatter": {
            "enabled": true,
            "indentStyle": "tab"
        },
        "organizeImports": {
            "enabled": true
        },
        "linter": {
            "enabled": true,
            "rules": {
                "recommended": true
            }
        },
        "javascript": {
            "formatter": {
                "quoteStyle": "double"
            }
        }
    }

    ```

#### Babylon.jsなどの依存関係をインストール

1. Babylon.jsとbabylon-mmdに必要なライブラリを追加します。

    ```bash
    bun add @babylonjs/core @babylonjs/havok @babylonjs/inspector @babylonjs/materials babylon-mmd
    ```
    **実行結果**
    ```bash
     bun add @babylonjs/core @babylonjs/havok @babylonjs/inspector @babylonjs/materials babylon-mmd
    bun add v1.1.0 (5903a614)

     installed @babylonjs/core@7.39.1
     installed @babylonjs/havok@1.3.10
     installed @babylonjs/inspector@7.39.1
     installed @babylonjs/materials@7.39.1
     installed babylon-mmd@0.59.1

     18 packages installed [10.18s]
    ```

#### Viteの設定

1. `vite.config.ts`を作成し、MMDのロードをサポートするための設定を記載します。

    ```typescript
    import { defineConfig } from 'vite';

    export default defineConfig({
      server: {
        headers: {
          "*.wasm": {
            "Content-Type": "application/wasm"
          }
        }
      },
      optimizeDeps: {
        exclude: ['babylon-mmd']
      }
    });

    ```

## 2. MMD関連のアセットの準備

1. **モデルデータの準備**
    MMDではPMXファイルを用いてMMDモデルをインポートしますが、babylon-mmdではPMXを更に最適化したBPMXファイルを使用することができます。
    PMXファイルの変換についてはbabylon-mmdの作者様が変換ページを公開していますので以下のリンクから変換してください。

    [PMX to BPMX Converter](https://noname0310.github.io/babylon-mmd/pmx_converter/)

2. **モーションデータの準備**
    同様に、babylon-mmdではVMDを更に最適化したBVMDファイルを使用することができます。
    babylon-mmdの作者様が変換ページを公開していますので以下のリンクから変換してください。

    [VMD to BVMD Converter](https://noname0310.github.io/babylon-mmd/vmd_converter/)

    :::info
    babylon-mmdでは通常のPMX・VMDファイルもサポートしています。
    :::

3. **音声ファイルの準備**
    お好みの楽曲等の音源を用意してください。

    :::info
    Babylon.jsでサポートされているサウンド形式は、.mp3、.ogg、.wav、.m4a、.mp4です。
    :::

## 3. Babylon.jsの基本的な設定

まずは、Babylon.jsで基本的なシーンを構築します。

1. **エンジンの初期化**
    `src/main.ts`を作成し編集します。

    ```typescript
    import { Engine } from "@babylonjs/core/Engines/engine";
    import { createBaseRuntime } from "./baseRuntime";
    import { buildScene } from "./sceneBuilder";

    async function initializeEngine() {
      const canvas = document.createElement("canvas");
      canvas.id = "renderCanvas";
      document.body.appendChild(canvas);

      const engine = new Engine(canvas, true, {
        preserveDrawingBuffer: false,
        stencil: false,
        antialias: true,
        alpha: false,
        powerPreference: "high-performance",
      });

      const runtime = await createBaseRuntime({
        canvas,
        engine,
        sceneBuilder: { build: buildScene },
      });

      runtime.run();

      window.addEventListener("resize", () => {
        engine.resize();
      });
    }

    window.addEventListener("DOMContentLoaded", () => {
      initializeEngine().catch(console.error);
    });
    ```

2. **ランタイムの作成**
    `src/baseRuntime.ts`を作成し編集します。

    ```typescript
    import type { AbstractEngine } from "@babylonjs/core/Engines/abstractEngine";
    import type { Scene } from "@babylonjs/core/scene";

    export interface ISceneBuilder {
      build(
        canvas: HTMLCanvasElement,
        engine: AbstractEngine,
      ): Scene | Promise<Scene>;
    }

    export interface BaseRuntimeInitParams {
      canvas: HTMLCanvasElement;
      engine: AbstractEngine;
      sceneBuilder: ISceneBuilder;
    }

    interface BaseRuntime {
      run(): void;
      dispose(): void;
    }

    export async function createBaseRuntime(
      params: BaseRuntimeInitParams,
    ): Promise<BaseRuntime> {
      const { canvas, engine, sceneBuilder } = params;
      const scene = await sceneBuilder.build(canvas, engine);

      const onResize = (): void => {
        engine.resize();
      };

      const onTick = (): void => {
        if (scene) {
          scene.render();
        }
      };

      const run = (): void => {
        window.addEventListener("resize", onResize);
        engine.runRenderLoop(onTick);
      };

      const dispose = (): void => {
        window.removeEventListener("resize", onResize);
        engine.dispose();
      };

      return {
        run,
        dispose,
      };
    }
    ```

## 4. シーンの構築

### 4-1. エンジンの初期設定

`src/sceneBuilder.ts`を作成し、エンジンの初期設定を行います。ここでは、MMDモデルのスキニング変形を正しく処理するためのSDEFインジェクタの設定と、テクスチャローダーの登録を行います。

```typescript
import { AbstractEngine } from "@babylonjs/core";
import { SdefInjector, registerDxBmpTextureLoader } from "babylon-mmd";

// エンジンの初期設定
const initializeEngine = (engine: AbstractEngine): void => {
    SdefInjector.OverrideEngineCreateEffect(engine);
    registerDxBmpTextureLoader();
};
```

`initializeEngine` 関数では、`SdefInjector`でスキニング変形を処理し、`registerDxBmpTextureLoader`でテクスチャローダーを登録します。

### 4-2. シーンの基本設定

次に、シーンの背景色とアンビエントカラーを設定します。

```typescript
import { Color3, Color4, Scene } from "@babylonjs/core";

// シーンの基本設定を行う
const setupScene = (scene: Scene): void => {
    scene.clearColor = new Color4(0.95, 0.95, 0.95, 1.0);
    scene.ambientColor = new Color3(0.5, 0.5, 0.5);
};
```

`setupScene` 関数では、`scene.clearColor` で背景色を、`scene.ambientColor` でアンビエントカラーを定義します。

### 4-3. MMDルートノードの作成

MMDモデルとカメラを整理するために、ルートとなるトランスフォームノードを作成します。

```typescript
import { Scene, TransformNode } from "@babylonjs/core";

// MMDのルートノードを作成する
const createMmdRoot = (scene: Scene): TransformNode => {
    const mmdRoot = new TransformNode("mmdRoot", scene);
    mmdRoot.position.z = 20;
    return mmdRoot;
};
```

`createMmdRoot` 関数で`mmdRoot` という名前の `TransformNode` を作成し、MMDモデルとカメラの親ノードとします。

### 4-4. MMDカメラの作成

MMDモデルを映すためのカメラを作成し、基本的な設定を行います。

```typescript
import { Scene, TransformNode, Vector3, MmdCamera } from "@babylonjs/core";

// MMDのカメラを作成する
const createMmdCamera = (
    scene: Scene,
    canvas: HTMLCanvasElement,
    mmdRoot: TransformNode
): MmdCamera => {
    const mmdCamera = new MmdCamera("mmdCamera", new Vector3(0, 10, 0), scene);
    mmdCamera.maxZ = 300;
    mmdCamera.minZ = 1;
    mmdCamera.parent = mmdRoot;
    mmdCamera.attachControl(canvas, false);
    mmdCamera.inertia = 0.8;
    return mmdCamera;
};
```

`createMmdCamera` 関数で`MmdCamera`を作成し、位置や描画範囲、操作や動きの慣性などを設定します。

## 5. ライティングとシャドウの設定

シーンにライティングとシャドウを追加して、よりリアルな見た目を作成します。

### 5-1. ディレクショナルライトの作成

シーン全体を照らすディレクショナルライトを作成します。

```typescript
import { DirectionalLight, Scene, Vector3 } from "@babylonjs/core";

// ディレクショナルライトを作成する
const createDirectionalLight = (scene: Scene): DirectionalLight => {
    const directionalLight = new DirectionalLight(
        "DirectionalLight",
        new Vector3(0.5, -1, 1),
        scene
    );
    directionalLight.intensity = 1.0;
    directionalLight.autoCalcShadowZBounds = false;
    directionalLight.autoUpdateExtends = false;
    return directionalLight;
};
```

`createDirectionalLight` 関数で`DirectionalLight`を作成し、方向や明るさを設定します。

### 5-2. シャドウジェネレーターの作成

影を生成するためのシャドウジェネレーターを作成し、設定します。

```typescript
import { DirectionalLight, ShadowGenerator } from "@babylonjs/core";

// シャドウジェネレーターを作成する
const createShadowGenerator = (
    directionalLight: DirectionalLight
): ShadowGenerator => {
    const shadowGenerator = new ShadowGenerator(4096, directionalLight, true);
    shadowGenerator.usePoissonSampling = true;
    shadowGenerator.useBlurExponentialShadowMap = true;
    shadowGenerator.usePercentageCloserFiltering = true;
    shadowGenerator.transparencyShadow = true;
    shadowGenerator.forceBackFacesOnly = true;
    shadowGenerator.frustumEdgeFalloff = 0.1;
    return shadowGenerator;
};
```

`createShadowGenerator` 関数で`ShadowGenerator`を作成し、シャドウの品質や計算方法を調整します。

### 5-3. 地面の作成

影を受けるための地面を作成します。

```typescript
import {
    CreateGround,
    DirectionalLight,
    Scene,
    TransformNode,
} from "@babylonjs/core";
import { ShadowOnlyMaterial } from "@babylonjs/materials";

// 地面を作成する
const createGround = (
    scene: Scene,
    directionalLight: DirectionalLight,
    mmdRoot: TransformNode
): TransformNode => {
    const ground = CreateGround(
        "ground1",
        { width: 100, height: 100, subdivisions: 2, updatable: false },
        scene
    );
    const shadowOnlyMaterial = new ShadowOnlyMaterial("shadowOnly", scene);
    ground.material = shadowOnlyMaterial;
    shadowOnlyMaterial.activeLight = directionalLight;
    shadowOnlyMaterial.alpha = 0.4;
    ground.receiveShadows = true;
    ground.parent = mmdRoot;
    return ground;
};
```

`createGround` 関数で地面を作成し、影を受けるように設定します。

## 6. MMDモデルの表示

MMDモデルをシーンにロードして表示します。

### 6-1. アセットの並行ロード

MMDモデル、モーション、カメラモーションを並行してロードします。

```typescript
import { Scene, SceneLoader, TransformNode, loadAssetContainerAsync } from "@babylonjs/core";
import type { MmdAnimation, MmdMesh, MmdWasmInstance } from "babylon-mmd";
import { BpmxLoader, BvmdLoader, MmdStandardMaterialBuilder, getMmdWasmInstance, MmdWasmInstanceTypeSPR } from "babylon-mmd";

// ロードするファイルのパスを定数として定義
const MOTION_FILE_PATH = "/gimme_gimme_motion.bvmd";
const CAMERA_MOTION_FILE_PATH = "/GimmeGimmeC.bvmd";
const MODEL_FILE_PATH = "/sour_miku_black.bpmx";

// アセットを並行してロード
const loadAssets = async (
    scene: Scene,
    _mmdRoot: TransformNode
): Promise<[MmdWasmInstance, MmdAnimation, MmdAnimation, MmdMesh]> => {
    const materialBuilder = new MmdStandardMaterialBuilder();
    const bvmdLoader = new BvmdLoader(scene);
    bvmdLoader.loggingEnabled = true;
    SceneLoader.RegisterPlugin(new BpmxLoader());

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

    return Promise.all([
        getMmdWasmInstance(new MmdWasmInstanceTypeSPR()),
        bvmdLoader.loadAsync("motion", MOTION_FILE_PATH, (event) =>
            updateLoadingText(
                scene.getEngine(),
                0,
                `モーションを読み込み中... ${event.loaded}/${
                    event.total
                } (${Math.floor((event.loaded * 100) / event.total)}%)`
            )
        ),
        bvmdLoader.loadAsync("cameraMotion", CAMERA_MOTION_FILE_PATH, (event) =>
            updateLoadingText(
                scene.getEngine(),
                1,
                `カメラモーションを読み込み中... ${event.loaded}/${
                    event.total
                } (${Math.floor((event.loaded * 100) / event.total)}%)`
            )
        ),
        loadAssetContainerAsync(MODEL_FILE_PATH, scene, {
            onProgress: (event) =>
                updateLoadingText(
                    scene.getEngine(),
                    2,
                    `モデルを読み込み中... ${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;
        }),
    ]);
};
```

`loadAssets` 関数で`Promise.all`を使用して、MMDモデル、モーション、カメラモーションを並行してロードし、ロード進行状況をユーザーに表示します。

## 7. モーションとカメラの適用

ロードしたモーションとカメラモーションをMMDモデルとカメラに適用します。

### 7-1. MMDランタイムの設定

MMDモデルのアニメーションを制御するためのMMDランタイムを設定します。

```typescript
import {
    Scene,
    TransformNode,
    DirectionalLight,
} from "@babylonjs/core";
import type { MmdAnimation, MmdMesh, MmdWasmInstance } from "babylon-mmd";
import {
    MmdCamera,
    MmdPlayerControl,
    MmdWasmAnimation,
    MmdWasmPhysics,
    MmdWasmRuntime,
    StreamAudioPlayer,
} from "babylon-mmd";

// MMDランタイムを設定する
const setupMmdRuntime = (
    scene: Scene,
    wasmInstance: MmdWasmInstance,
    mmdAnimation: MmdAnimation,
    cameraAnimation: MmdAnimation,
    modelMesh: MmdMesh,
    mmdRoot: TransformNode,
    mmdCamera: MmdCamera,
    audioPlayer: StreamAudioPlayer,
    directionalLight: DirectionalLight
): void => {
    const mmdRuntime = new MmdWasmRuntime(
        wasmInstance,
        scene,
        new MmdWasmPhysics(scene)
    );
    mmdRuntime.loggingEnabled = true;
    mmdRuntime.register(scene);

    mmdRuntime.setAudioPlayer(audioPlayer);
    mmdRuntime.playAnimation();

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

    mmdRuntime.setCamera(mmdCamera);

    const mmdWasmAnimation = new MmdWasmAnimation(
        mmdAnimation,
        wasmInstance,
        scene
    );
    const cameraWasmAnimation = new MmdWasmAnimation(
        cameraAnimation,
        wasmInstance,
        scene
    );

    mmdCamera.addAnimation(cameraWasmAnimation);
    mmdCamera.setAnimation("cameraMotion");

    modelMesh.parent = mmdRoot;

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

    const mmdModel = mmdRuntime.createMmdModel(modelMesh);
    mmdModel.addAnimation(mmdWasmAnimation);
    mmdModel.setAnimation("motion");

    mmdRuntime.physics?.createGroundModel?.([0]);

    optimizeScene(scene);
};
```

`setupMmdRuntime` 関数で`MmdWasmRuntime`を初期化し、モーションとカメラモーションを適用、`MmdPlayerControl`でアニメーションの制御を可能にします。

## 8. VR対応

シーンをVRに対応させます。

### 8-1. XRエクスペリエンスの設定

VRエクスペリエンスを作成し、基本的な設定を行います。

```typescript
import { Scene, TransformNode, WebXRDefaultExperience, WebXRFeatureName, WebXRState, Vector3 } from "@babylonjs/core";

// XRエクスペリエンスを設定する
const setupXRExperience = async (
    scene: Scene,
    ground: TransformNode,
    mmdCamera: MmdCamera
): Promise<WebXRDefaultExperience> => {
    const xr = await WebXRDefaultExperience.CreateAsync(scene, {
        uiOptions: {
            sessionMode: "immersive-vr",
            referenceSpaceType: "local-floor",
        },
        disableDefaultUI: true,
        disableTeleportation: true,
    });

    // カメラのルートノードを作成し、カメラの親に設定
    const cameraRoot = new TransformNode("cameraRoot", scene);
    xr.baseExperience.camera.parent = cameraRoot;

    const featuresManager = xr.baseExperience.featuresManager;
    featuresManager.enableFeature(WebXRFeatureName.POINTER_SELECTION, "stable", {
        xrInput: xr.input,
        enablePointerSelectionOnAllControllers: true,
    });

    featuresManager.enableFeature(WebXRFeatureName.TELEPORTATION, "stable", {
        xrInput: xr.input,
        floorMeshes: [ground],
        defaultTargetMeshOptions: {
            teleportationRadius: 2,
            torusArrowMaterial: null,
        },
        useMainComponentOnly: true,
        snapPositions: [new Vector3(2.4 * 3.5 * 1, 0, -10 * 1)],
    });

    xr.input.onControllerAddedObservable.add((controller) => {
        controller.onMotionControllerInitObservable.add((motionController) => {
            const thumbstick = motionController.getComponent(
                "xr-standard-thumbstick"
            );
            if (thumbstick) {
                if (motionController.handedness === "right") {
                    // 移動操作
                    thumbstick.onAxisValueChangedObservable.add((axes) => {
                        if (xr.baseExperience.state === WebXRState.IN_XR) {
                            const forward = xr.baseExperience.camera.getDirection(
                                Vector3.Backward()
                            );
                            forward.y = 0;
                            forward.normalize();

                            const right = xr.baseExperience.camera.getDirection(
                                Vector3.Right()
                            );
                            right.y = 0;
                            right.normalize();

                            const movement = forward
                                .scale(axes.y * 0.1)
                                .add(right.scale(axes.x * 0.1));

                            cameraRoot.position.addInPlace(movement);
                        }
                    });
                } else if (motionController.handedness === "left") {
                    // 視点の回転操作
                    thumbstick.onAxisValueChangedObservable.add((axes) => {
                        if (xr.baseExperience.state === WebXRState.IN_XR) {
                            const rotationSpeed = 0.05;
                            cameraRoot.rotation.y -= axes.x * rotationSpeed;
                            cameraRoot.rotation.x -= axes.y * rotationSpeed;
                            // 角度を制限
                            cameraRoot.rotation.x = Math.max(
                                -Math.PI / 2,
                                Math.min(Math.PI / 2, cameraRoot.rotation.x)
                            );
                        }
                    });
                }
            }
        });
    });

    xr.input.onControllerAddedObservable.add((controller) => {
        controller.onMotionControllerInitObservable.add((motionController) => {
            const componentIds = motionController.getComponentIds();
            for (const id of componentIds) {
                const component = motionController.getComponent(id);
                if (component && component.type !== "thumbstick") {
                    component.onButtonStateChangedObservable.add(() => {
                        if (component.pressed) {
                            xr.baseExperience.exitXRAsync();
                        }
                    });
                }
            }
        });
    });

    xr.baseExperience.onStateChangedObservable.add((state) => {
        if (state === WebXRState.NOT_IN_XR) {
            const defaultPipeline = scene.postProcessRenderPipelineManager
                .supportedPipelines[0] as DefaultRenderingPipeline;
            defaultPipeline.fxaaEnabled = true;
            defaultPipeline.chromaticAberrationEnabled = true;

            const enterVrButton = document.getElementById("enterVrButton");
            if (enterVrButton) {
                enterVrButton.style.display = "block";
            }

            scene.activeCamera = mmdCamera;
        }
    });

    return xr;
};
```

`setupXRExperience` 関数で`WebXRDefaultExperience`を作成し、VRモードを有効にします。コントローラーの入力を検知し、移動や視点変更を実装します。

### 8-2. VRモードに入るボタンの設定

ユーザーがVRモードに入るためのボタンを作成し、設定します。

```typescript
import { Scene, WebXRDefaultExperience, DefaultRenderingPipeline } from "@babylonjs/core";

// VRモードに入るボタンを設定する
const setupEnterVrButton = (scene: Scene, xr: WebXRDefaultExperience): void => {
    const enterVrButton = document.createElement("button");
    enterVrButton.id = "enterVrButton";
    enterVrButton.textContent = "VRモード";
    document.
```

`setupEnterVrButton`関数で`enterVrButton`を作成し、VRモードへの切り替えを実装します。

以上の内容を組み合わせたものを[リポジトリ](https://github.com/TkymHrt/test-babylon-mmd)に公開しています。

## デモ動画
実際に動作するデモです。

<iframe width="560" height="315" src="https://www.youtube.com/embed/9-nIZsC3OKQ?si=lk9dym3dskL8XtMY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

<iframe width="560" height="315" src="https://www.youtube.com/embed/c_FyB_odMZA?si=MYmleDNCEwO2RXQx" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>



## まとめ

Babylon.jsとbabylon-mmdを活用することで、Webブラウザ上でMMDモデルの表示とモーション再生を実現できました。TypeScriptやViteなどのモダンなツールを使用することで、効率的かつ快適な開発が可能でした。Babylon.jsやMMDに興味を持っていいただけていれば幸いです。

## 参考

1. [Babylon.js 公式ドキュメント](https://doc.babylonjs.com/)
2. [babylon-mmd 公式ドキュメント](https://noname0310.github.io/babylon-mmd/)
3. [babylon-mmd GitHub リポジトリ](https://github.com/noname0310/babylon-mmd)
4. [babylon-mmd 作者の登壇スライド](https://onedrive.live.com/edit?id=95C741421F306C96!s668eef439f0e4f43ae15d6dcd7edd08c&resid=95C741421F306C96!s668eef439f0e4f43ae15d6dcd7edd08c&cid=95c741421f306c96&ithint=file%2Cpptx&redeem=aHR0cHM6Ly8xZHJ2Lm1zL3AvYy85NWM3NDE0MjFmMzA2Yzk2L0VVUHZqbVlPbjBOUHJoWFczTmZ0MEl3QnBoSXNIWUxxYnVwcnN5N0l2TW5ZSHc_ZT1DdHhwRVQ&migratedtospo=true&wdo=2)

### デモで使用しました。
1. [Sour式初音ミクVer.1.02](https://bowlroll.net/file/146103)
2. [GimmeGimmeモーション配布用](https://bowlroll.net/file/215305?rf=nvpc&rp=watch&ra=video_detail)
3. [八王子P × Giga「Gimme×Gimme feat. 初音ミク・鏡音リン」
](https://www.youtube.com/watch?v=ERo-sPa1a5g)
