[Astro #96] CreditBoard汎用化・VRM回転機能追加

[Astro #96] CreditBoard汎用化・VRM回転機能追加

はじめに

WIT(グラディウス風STG)で実装済みだったクレジット看板(WITCreditBoard.tsx)を汎用コンポーネントに切り出し、ADV/RPGとパズルゲームにも展開。あわせてトップページのVRMアバターにY軸回転機能を追加。

1. CreditBoard汎用化

課題

  • WITCreditBoard.tsxがWIT専用のJSON(Accessories, Enemies, Sounds, Avatars)を直接importしており、他プロジェクトで使い回せなかった
  • クレジット情報がindex.astroのHTMLタグに手書きで列挙されており、メンテが煩雑だった

設計方針

  • クレジットデータの構造を CreditSection[]{ header: string; items: string[] }[])としてprops化
  • Canvas描画・UVスクロール・Aフレーム看板の3Dジオメトリはそのまま維持
  • 各プロジェクトごとに薄いラッパーを作り、JSONからcreator/authorを抽出して渡す

作成ファイル

ファイル役割
CreditBoard.tsx汎用コンポーネント本体(R3F + CanvasTexture)
WITCreditBoard.tsxWIT専用ラッパー(既存APIを維持)
ADVCreditBoard.tsxADV/RPG専用ラッパー
PuzzleCreditBoard.tsxパズル専用ラッパー(木箱GLB付き)

技術ポイント

  • CreditSectionはTypeScript interfaceのため、import typeで分離しないとVite + Astro環境でランタイムexportエラーになる
  • CreditBoardのprops: credits, title, scrollSpeed, position, rotation, scale
  • パズル版は木箱GLBモデル(low_poly_wood_box.glb)をpuzzleConfig.jsonのmodels配列から自動取得し、その上に看板を配置

2. ADV/RPG — STAGE_MY_ROOMにクレジット看板設置

抽出元JSON → セクション

セクション抽出元フィールド
STAGE MODELStages.json, BattleConfigs.jsonauthor
CHARACTER MODELAvatars.jsonauthor or creator
AUDIOSoundEffects.jsoncreator

ADVManager.tsxへの組み込み

{currentStageId === 'STAGE_MY_ROOM' && (
  <ADVCreditBoard
    position={[2.37, -0.1, 5.0]}
    rotation={[0, 0, 0]}
    scale={1.5}
  />
)}

効果

  • index.astroの手書きクレジットHTMLを大幅削減(トップページ専用アセットの3件のみ残し)
  • JSONのcreator/authorフィールドを書くだけで看板に自動反映される運用に統一

3. パズルゲーム — 木箱+クレジット看板

背景

  • エンディングまでプレイするとクレジットが出るが、難易度が高くクリア困難
  • 素材提供クリエイターのクレジットが実質見られない状態だった

抽出元JSON → セクション

セクション抽出元フィールド
CHARACTER MODELpuzzleConfig.json stages[]creator
3D MODELpuzzleConfig.json models[]creator

PuzzleManager.tsxへの組み込み

<PuzzleCreditBoard
  position={isXR ? [0, -1.0, 0.3] : [0, -1.0, 1.5]}
  scale={0.55}
/>
  • VR/PCでカメラ位置が異なるため、isXRでZ座標を分岐
  • 看板の向きが逆だったため rotation={[0, Math.PI, 0]} で180°回転

4. VRMアバター Y軸回転機能

課題

  • 外部VRMの中にはデフォルトの向きが逆(背面向き)のモデルがある
  • VROID STUDIO出力時のエクスポート設定差異が原因と推測

実装

3ファイルに変更を追加:

WiredScene.tsx

  • avatarRotY state追加(localStorage永続化)
  • handleRotYChange ハンドラ定義
  • WiredAvatar, VRMenuManager, WiredAvatarMenu の両方にpropsを透過

WiredAvatarMenu.tsx

  • SYSタブに ROTATION_Y スライダー追加(0〜360°、内部はラジアン管理)

WiredAvatar.tsx

  • WiredAvatarCoreのreturn内でprimitiveを内側の<group rotation={[0, avatarRotY, 0]}>で包む
  • 外側のgroupRef(箒飛行・STG制御用)とは独立して回転

ハマったポイント

  1. primitiveの重複レンダリング: 新しい回転groupを追加した際、元のprimitiveを消し忘れて2体描画されスライダーが効かないように見えた
  2. PC用メニューへのprops渡し漏れ: VRMenuManagerにはpropsを渡していたが、PC用のWiredAvatarMenu(右クリックメニュー)へのavatarRotY/onRotYChangeが欠落。onRotYChange?.() のオプショナルチェインでサイレントに無視されていた

5. 外部VRM保存の仕組み確認

結論

外部VRMはIndexedDBに正しく永続保存されていた。

保存フロー

  1. handleFileLoadblob:xxx#name=ファイル名&size=サイズ を生成
  2. getCachedAssetUrl(assetCache.ts)がblob URLをfetch → バイナリ取得
  3. LOCAL_VRM_${fileName}_${fileSize} をキーに db.models へ保存
  4. localStorage('wired_model_path') にキー名を記録

リロード時の復元フロー

  1. localStorageLOCAL_VRM_xxx キーを復元
  2. getCachedAssetUrltable.get(cacheKey) でIndexedDBからBlob取得
  3. URL.createObjectURL() で新しいblob URLを生成 → 表示

DevToolsでの確認方法

  • IndexedDB → WiredAssetCache → models → ページネーション(▶ボタン)で後半ページに LOCAL_VRM_* エントリが並ぶ