Zenn
🕺

Next.js × Babylon.jsでMMDモデルを動かしてみた

に公開

はじめに

知人から「MMDモデルをWebで表示できないか?」と相談されたのをきっかけに、Babylon.jsbabylon-mmdを使って、Next.js 環境で .pmxモデルの表示やアニメーションを再生する方法を試してみました。
本記事では、モデルの読み込みから基本的なモーション再生までの過程を記録としてまとめています。

使用環境

この記事では、以下のバージョンで動作確認を行いました。

  • Next.js: 14(App Router)
  • @babylonjs/core: 7.54.0
  • babylon-mmd: 0.62.0

@babylonjs/coreとbabylon-mmdの導入方法

※ 今回は MMD モーションの再生のみを扱っており、物理演算(髪や服の揺れ)は使用していないため、@babylonjs/havok は導入していません。

Babylon.jsをNext.jsで動かすまでの過程

Babylon.jsをReactで動かすためのドキュメント があるため、基本的にはこちらに沿って実装しています。

Canvasの準備

Babylon.js は <canvas> 要素上に 3D を描画するライブラリです。
まずは、ドキュメントに沿ってエンジンとシーンの初期化を行うコンポーネントを用意しました。

// src/components/SceneComponent.tsx

'use client'

import {
  Engine,
  type EngineOptions,
  Scene,
  type SceneOptions,
} from '@babylonjs/core'

import React, { useEffect, useRef } from 'react'

type Props = {
  // アンチエイリアスの有無
  antialias?: boolean
  // Babylon.js のエンジン設定
  engineOptions?: EngineOptions
  // デバイスピクセル比に基づいたスケーリングの有無
  adaptToDeviceRatio?: boolean
  // Babylon.js のシーン設定
  sceneOptions?: SceneOptions
  // 毎フレーム呼ばれる描画処理(例:アニメーションの更新など)
  onRender?: (scene: Scene) => void
  // シーンの初期化完了時に呼ばれるコールバック
  onSceneReady?: (scene: Scene) => void
}

/**
 * Babylon.js の Engine と Scene をセットアップし、React コンポーネントとして <canvas> を描画
 */
export const SceneComponent = ({
  antialias,
  engineOptions,
  adaptToDeviceRatio,
  sceneOptions,
  onSceneReady,
}: Props): JSX.Element => {
  // <canvas> への参照
  const reactCanvas = useRef<HTMLCanvasElement>(null)

  useEffect(() => {
    const { current: canvas } = reactCanvas

    if (!canvas) return

    // Babylon.js のエンジンを作成
    const engine = new Engine(
      canvas,
      antialias,
      engineOptions,
      adaptToDeviceRatio,
    )

    // デバイスピクセル比に応じてスケーリング調整
    engine.setHardwareScalingLevel(1 / window.devicePixelRatio)

    const scene = new Scene(engine, sceneOptions)

    // シーンがすでに準備できていれば onSceneReady を即実行
    if (scene.isReady()) {
      onSceneReady?.(scene)
    } else {
      // シーン準備完了を待ってから onSceneReady を実行
      scene.onReadyObservable.addOnce(scene => onSceneReady?.(scene))
    }

    // レンダーループの開始(毎フレーム scene.render() を呼び出す)
    engine.runRenderLoop(() => {
      scene.render()
    })

    // ウィンドウリサイズ時にエンジンもリサイズする
    const resize = () => {
      scene?.getEngine().resize()
    }

    window.addEventListener('resize', resize)

    // コンポーネントのアンマウント時にクリーンアップ
    return () => {
      scene?.getEngine().dispose()
      window.removeEventListener('resize', resize)
    }
  }, [antialias, engineOptions, adaptToDeviceRatio, sceneOptions, onSceneReady])

  return <canvas ref={reactCanvas} />
}

基本的なシーンの設定

Babylon.js の Scene に対して、カメラ・ライトなどの基本設定を行います。
ドキュメントのCreate Basic Sceneを参考に、カメラやライトなどの設定を追加していきます。

// src/onSceneReady.ts

import {
  DirectionalLight,
  HemisphericLight,
  MeshBuilder,
  type Scene,
  ShadowGenerator,
  Vector3,
} from '@babylonjs/core'
import { MmdCamera } from 'babylon-mmd/esm/Runtime/mmdCamera'

/**
 * シーン初期化時に呼ばれる処理
 */
export const onSceneReady = async (scene: Scene) => {
  // アニメーション付きのカメラモーションを再生できるMmdCameraを使用
  const camera = new MmdCamera('mmdCamera', new Vector3(0, 10, 0), scene)

  // 周囲の環境光の設定。反射方向を指定する
  const hemisphericLight = new HemisphericLight(
    'HemisphericLight',
    new Vector3(0, 1, 0),
    scene,
  )
  hemisphericLight.intensity = 0.3
  hemisphericLight.specular.set(0, 0, 0)
  hemisphericLight.groundColor.set(1, 1, 1)

  // 地面を作成し影を受けるように設定
  const directionalLight = new DirectionalLight(
    'DirectionalLight',
    new Vector3(0.5, -1, 1),
    scene,
  )
  directionalLight.intensity = 0.7
  directionalLight.shadowMaxZ = 20
  directionalLight.shadowMinZ = -15

  const shadowGenerator = new ShadowGenerator(
    2048,
    directionalLight,
    true,
    camera,
  )
  shadowGenerator.transparencyShadow = true
  shadowGenerator.bias = 0.01

  const ground = MeshBuilder.CreateGround(
    'ground1',
    { width: 60, height: 60, subdivisions: 2, updatable: false },
    scene,
  )
  ground.receiveShadows = true
  shadowGenerator.addShadowCaster(ground)
}

設定したシーンを表示してみる

// app/page.tsx

'use client'
import { SceneComponent } from '@/components/SceneComponent'

import React from 'react'
import styles from './page.module.css'

import { onSceneReady } from '../onSceneReady'

export default function Home() {
  return (
    <main className={styles.main}>
      <div>
        <SceneComponent antialias onSceneReady={onSceneReady} />
      </div>
    </main>
  )
}

以下のような画面になっていれば、基本的なシーンの設定は完了です。

MMDモデルの準備

今回使用するMMDモデルはドキュメントに記載されているモデルと同じものを使用します。

MMDモデル「YYB 初音ミク_10thのセクションにダウンロード先のリンクがあるのでダウンロードします。
その後、zipファイルを解凍したら、public/models配下にYYB Hatsune Miku_10thディレクトリごと配置します。

MMDモデルを読み込む

ドキュメントのLoad PMX Modelを参考にMMDモデルを読み込みます。

まず、MMDモデルを表示するために、副作用をインポートします。

もしインポートを忘れると、

ncaught (in promise) RuntimeError: Unable to load from models/YYB Hatsune Miku_10th/YYB Hatsune Miku_10th_v1.02.pmx: loadAssets of undefined from undefined version: undefined, exporter version: undefinedimportScene has failed JSON parse

といったエラーになり、MMDモデルが表示されなくなるので注意が必要でした。

// src/onSceneReady.ts

import {
  DirectionalLight,
  HemisphericLight,
  // 追加
  LoadAssetContainerAsync,
  MeshBuilder,
  type Scene,
  ShadowGenerator,
  Vector3,
} from '@babylonjs/core'
import { MmdCamera } from 'babylon-mmd/esm/Runtime/mmdCamera'
// 追加
import type { MmdMesh } from 'babylon-mmd/esm/Runtime/mmdMesh'

// pmx モデルをロードするために、副作用をインポートする。
import 'babylon-mmd/esm/Loader/pmxLoader'


export const onSceneReady = async (scene: Scene) => {
  /** 基本的なシーン設定セクションで定義済みのコードの続き */

  // MMDモデル(PMX)を読み込み
  const pmxModel =
    'models/YYB Hatsune Miku_10th/YYB Hatsune Miku_10th_v1.02.pmx'


  const mmdMesh = await LoadAssetContainerAsync(pmxModel, scene).then(
    result => {
      result.addAllToScene()
      return result.meshes[0] as MmdMesh
    },
  )

  // モデルの各メッシュが影を受けられるように設定
  for (const mesh of mmdMesh.metadata.meshes) mesh.receiveShadows = true
  shadowGenerator.addShadowCaster(mmdMesh)
}

以下のようにMMDモデルが表示されていれば完了です。

MMDモデルを動かすための準備

MMDを動かすためのモーションもドキュメントに合わせて、メランコリ・ナイトを使用します。
ダウンロードアニメーション:「メランコリ・ナイトにリンクがあるのでダウンロードします。

その後、zipファイルを解凍したら、public/models配下に09_メランコリ・ナイトディレクトリごと配置します。

MMDモデルを動かしてみる

MMDを動かすためのランタイムを作成し、ダウンロードしたvmdアニメーションを読み込みます。
MMDを動かすには、ランタイムの副作用をインポートする必要があるため、追加します。

// src/onSceneReady.ts

import {
  DirectionalLight,
  HemisphericLight,
  LoadAssetContainerAsync,
  MeshBuilder,
  type Scene,
  ShadowGenerator,
  Vector3,
} from '@babylonjs/core'

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 { VmdLoader } from 'babylon-mmd/esm/Loader/vmdLoader'
// MmdAnimationアニメーション ランタイムの副作用をインポート
import 'babylon-mmd/esm/Runtime/Animation/mmdRuntimeCameraAnimation'
import 'babylon-mmd/esm/Runtime/Animation/mmdRuntimeModelAnimation'


export const onSceneReady = async (scene: Scene) => {
  /** MMDモデルを読み込むセクションの続き */

  // MMDランタイム初期化
  const mmdRuntime = new MmdRuntime(scene)
  mmdRuntime.register(scene)
  mmdRuntime.setCamera(camera)

  // MMDモデルの作成
  const mmdModel = mmdRuntime.createMmdModel(mmdMesh)

  // VMD(モーション)ローダーを初期化
  const vmdLoader = new VmdLoader(scene)

  // モデル用モーション(本体・表情・リップ)
  const modelMotion = await vmdLoader.loadAsync('model_motion_1', [
    'models/09_メランコリ・ナイト/メランコリ・ナイト.vmd',
    'models/09_メランコリ・ナイト/メランコリ・ナイト_表情モーション.vmd',
    'models/09_メランコリ・ナイト/メランコリ・ナイト_リップモーション.vmd',
  ])

  // カメラ用モーション
  const cameraMotion = await vmdLoader.loadAsync(
    'camera_motion_1',
    'models/09_メランコリ・ナイト/メランコリ・ナイト_カメラ.vmd',
  )

  // モデルとカメラにアニメーションを登録・再生
  mmdModel.addAnimation(modelMotion)
  mmdModel.setAnimation('model_motion_1')

  camera.addAnimation(cameraMotion)
  camera.setAnimation('camera_motion_1')

  mmdRuntime.playAnimation()
}

MMDモデルを動かすことができました🎉

まとめ

今回は Babylon.js と babylon-mmd を使って、Next.js 環境上で MMD モデルを表示し、アニメーションを再生するところまで試してみました。

ドキュメントが充実しており、導入も比較的スムーズにできました。

今回はドキュメントに沿って動かしてみただけですが、今後は物理演算(髪や服の揺れ)などにも挑戦してみたいと思っています。
また、他の MMD モデルやモーションを切り替えて再生するなども試していきたいです。

Discussion

ログインするとコメントできます