Vue.js + Cubism SDK For Web でブラウザ上で動くノベルゲーム風のデモ

その他

Vue.js + Cubism SDK For Web を使って、Live2Dモデルを表示・制御するサンプル的なものを作ってみました。

前置き

それぞれの単語について簡単に説明します。

Cubism SDK For Web

Cubism SDK for Webは、Live2D社が提供するソフトウェア開発キット(SDK)の一つで、Webブラウザ上でLive2Dモデルを表示・操作するためのツールです。https://live2d.com

主な特徴:

  • WebGL対応:主要なWebブラウザ(Google Chrome、Firefox、Safari、Microsoft Edgeなど)で動作し、幅広い環境で利用可能です。
  • TypeScriptで実装:ソースコードはTypeScriptで書かれており、トランスパイルすることでJavaScriptからも扱うことができます。
  • サンプル実装の提供:公式のGitHubリポジトリでサンプル実装が公開されており、開発の参考にすることができます。GitHub

Vue.js

Vue.js(ヴュー・ジェイエス)は、ユーザーインターフェースの構築を目的としたオープンソースのJavaScriptフレームワークです。Vue.js

主な特徴:

  • 親しみやすさ:直感的なAPIと充実したドキュメントにより、標準的なHTML、CSS、JavaScriptの知識があれば容易に学習・導入が可能です。
  • 高パフォーマンス:仮想DOM(Virtual DOM)の採用により、効率的なレンダリングが可能で、Webページの高速な読み込みと実行を実現します。
  • 多用途性:ライブラリとしての軽量さと、フル機能のフレームワークとしての拡張性を兼ね備え、シングルページアプリケーション(SPA)の開発にも適しています。

メリット:

  • 学習コストの低さ:他のフレームワークと比較して習得が容易で、初心者にも扱いやすいとされています。
  • 豊富なエコシステム:公式のルーティング(Vue Router)や状態管理(Pinia)などのライブラリが充実しており、開発を効率化できます。
  • 高いパフォーマンス:仮想DOMの採用により、Webページの高速な読み込みと実行が可能です。

Vue.js で機能をコンポーネント化

1つのコンポーネントにすべての機能を詰め込もうとすると、コンポーネントが肥大化してしまい保守性が悪くなります。

そこで、Vue.jsでコンポーネントを機能ごとに分割し、それぞれのコンポーネントが1つの機能を担当するようにしました。

分割するにあたって、以下のような方針を立てました。

オブジェクトの生存期間

Vue3のライフサイクルフック(onMounted, onBeforeUnmount, onUnmounted)を使って、各リソースの初期化と破棄を行うことでリソースの生存期間の管理を容易にします。
コンポーネントの生存期間=リソースの生存期間とすることで、リソースの管理を行いやすくしようという考えです。

また、子要素は v-if で初期化が完了しているかを確認してからマウント(=初期化処理)を行うようにします。

具体例としてフレームワークの初期化と終了処理を行うコンポーネントは以下のようになります。

Vue
<template>
  <slot v-if="initialized"></slot>
</template>

<script setup lang="ts">
import { logger } from '@/logger';
import { CubismFramework } from '@framework/live2dcubismframework';
import { onBeforeUnmount, onMounted, onUnmounted, ref } from 'vue';

const initialized = ref(false);

onMounted(() => {
  // Live2Dの初期化処理
  CubismFramework.startUp()
  CubismFramework.initialize()
  logger.debug('CubismFramework has been initialized')

  initialized.value = true;
});

onBeforeUnmount(() => {
  initialized.value = false;
});

onUnmounted(() => {
  // Live2Dの終了処理
  CubismFramework.dispose()
  CubismFramework.cleanUp()
  logger.debug('CubismFramework has been disposed')
});
</script>

オブジェクトの共有

Vue3には provide()inject() という仕組みがあり、親コンポーネントから子コンポーネントにオブジェクトを渡すことができます。

これを使うと例えば、親コンポーネントがリソースを管理し、処理は子コンポーネントが行うというように分業することができます。

以下の例では親コンポーネントから子コンポーネントにモデルアセットを provide() しています。

Vue
<template>
    <slot v-if="initialized" />
</template>

<script setup lang="ts">
/**
  * モデルアセットを提供するコンポーネント
  */
import { provide } from 'vue'

const props = defineProps<{
  modelHomeDir: string;
  modelFileName: string;
}>()

const initialized = ref(false)

// モデルアセット
const modelAssets = computed<CubismModelAssets>(() => { ... })

// 子コンポーネントで利用するためにモデルアセットを提供
export type ProvidedCubismModelAssets = Ref<CubismModelAssets>;
provide<ProvidedCubismModelAssets>('CubismModelAssets', modelAssets);

// モデルアセットのロード処理
async function loadAssets(modelHomeDir: string, modelFileName: string) {
  // ...
}

// マウント時にモデルアセットをロード
onMounted(async () => {
  await loadAssets(props.modelHomeDir, props.modelFileName)
  initialized.value = true
})
</script>

子コンポーネント側では inject() を使ってリソースを取得し、各種処理を行います。
以下のように inject() を使うことで、状態の更新と描画処理を別コンポーネントに分けることが可能になります。

Vue
<script setup lang="ts">
/**
  * モデルアセットの状態を更新するコンポーネント
  */
import { inject } from 'vue'

// モデルアセットを親コンポーネントから注入
const modelAssets = inject<ProvidedCubismModelAssets>('CubismModelAssets');

// モデルアセットの状態を更新する処理
function update(deltaTime: number) {
  const { model } = modelAssets.value;
  // ...
  model.update();
}
Vue
<script setup lang="ts">
/**
  * モデルアセットの描画処理を行うコンポーネント
  */
import { inject } from 'vue'

// モデルアセットを親コンポーネントから注入
const modelAssets = inject<ProvidedCubismModelAssets>('CubismModelAssets');

// モデルの描画処理
function render() {
  const { model, renderer } = modelAssets.value;
  // ...
  renderer.drawModel();
}

分けることで処理をカスタマイズしやすくなり、コンポーネントの再利用性も高まります。

変更のウォッチ

Vue3の watch() を使うことで変数の変更を監視することができます。

以下の例では props の変更を監視し、変更と同時に表情の切り替え処理を行っています。

Vue
<script setup lang="ts">
import { watch } from 'vue'

const props = defineProps<{
  index: number; // 表情インデックス
}>()

function startExpression(index: number) {
  // ... 表情の切り替え処理
}

watch(() => props.index, (index) => {
  startExpression(index)
}))
</script>

このようにすることで、外部からの操作に応じたモデルの制御が容易になります。

コンポーネント構成

私なりにCubism SDK For Webの処理を分解してコンポーネント化していくと、以下のような構成になりました。

Vue
<!-- GallaryView.vue の一部 -->
<template>
<VCubismFramework>
  <!-- WebGL描画用のCanvasをマウントし、WebGLコンテキストを提供 -->
  <VCubismCanvasWebGLProvider class="w-full" style="aspect-ratio: 9/16;" width="720" height="1280">
    <!-- プロジェクション行列を提供 -->
    <VCubismProjectionMatrixProvider>
      <!-- View行列を提供 -->
      <VCubismViewMatrixProvider>
        <!-- 描画ループを提供 -->
        <VCubismRenderLoopProvider :fps="30">
          <!-- モデルアセットを読み込み提供 -->
          <VCubismModelAssetsProvider @loaded="assetsLoaded" :model-home-dir="modelHomeDir"
            :model-file-name="modelFileName">
            <!-- モデルの更新処理 -->
            <VCubismUpdateModel>
              <!-- モーションの更新処理 -->
              <VCubismUpdateModelMotion>
                <!-- まばたきの更新処理 -->
                <VCubismUpdateModelEyeBlink />
              </VCubismUpdateModelMotion>
              <!-- 呼吸の更新処理 -->
              <VCubismUpdateModelBreath />
              <!-- 物理演算の更新処理 -->
              <VCubismUpdateModelPhysics />
              <!-- 表情の更新処理 -->
              <VCubismUpdateModelExpression />
            </VCubismUpdateModel>
            <!-- モデル座標設定用の行列を提供 -->
            <VCubismModelMatrixProvider :scale-x="Number(scale)" :scale-y="Number(scale)"
              :translate-x="Number(translateX)" :translate-y="Number(translateY)">
              <!-- モデルのレンダー処理 -->
              <VCubismModelAssetsRenderer />
              <!-- ヒットエリアのレンダー処理 -->
              <VCubismHitAreaRenderer v-if="showHitBox" />
              <!-- ヒットエリアの管理 -->
              <VCubismHitManager @hit="onHit" />
            </VCubismModelMatrixProvider>
            <!-- モーションを管理するコンポーネント -->
            <VCubismMotionManager :group="motionGroupName" :index="motionIndex" :loop="true" />
            <!-- 表情を管理するコンポーネント -->
            <VCubismExpressionManager :index="expressionIndex" />
          </VCubismModelAssetsProvider>
        </VCubismRenderLoopProvider>
      </VCubismViewMatrixProvider>
    </VCubismProjectionMatrixProvider>
  </VCubismCanvasWebGLProvider>
</VCubismFramework>
</template>

コンポーネント分割とVue.jsのリアクティビリティーを活かすことで、Live2Dモデルの制御を柔軟に行うことができるかと思います。

デモ

Vue.js + Cubism SDK For Webを使って3つのデモを作成しました。

サンプルモデルのギャラリー

Cubism SDK For Webに付属しているサンプルモデルを表示するデモです。

複数のモデルの配置

2つのモデル(Mao, Hiyori)を配置し、それぞれのモデルに対して独立した操作を行うサンプルです。
声と口の動きはVOICEVOX APIを使ってリアルタイムで生成しています。

ノベルゲーム風

ノベルゲーム風の会話シーンを作ってみました。
先程と同じく、声と口の動きはVOICEVOX APIを使ってリアルタイムで生成しています。
会話文と背景はChatGPTに書かせました。

Maoのモデルは ParamA/I/U/E/O に対応しているので、いい感じに口を動かせますね。

最後に

Vue.jsは主にWebでのユーザーインターフェース構築に使われるフレームワークですが、用途はHTML要素の描画だけに限定されません。

今回の例のようにVue.jsの優れたリアクティビティーやライフサイクルフックを用いると、インスタンスの初期化や破棄、リソースの管理を柔軟に行うことができます。

個人的にVue.jsは非常に柔軟で使いやすいフレームワークだと思っているので、もっと使われて成長していってほしいですね。

コメント

タイトルとURLをコピーしました