[Astro #77] Three.jsで創る電脳魔女STG:プロシージャル夜空背景のコンポーネント化とマスコットのライティング制御

[Astro #77] Three.jsで創る電脳魔女STG:プロシージャル夜空背景のコンポーネント化とマスコットのライティング制御

はじめに:器(ハードウェア)の完全なる深化

前回までで劇場の基本的な大緞帳(カーテン)シーケンスやBGMインフラが整った、電脳魔女シューティング『Witchadius』。今回は、中央のドット絵STG画面を包み込む「特設ステージ(3D外枠)」のクオリティを極限まで高めるため、リファクタリングと数式によるプロシージャルエンジニアリングを敢行した。

ベタ書きコードの徹底的なデータ駆動化から、アセット容量ゼロで駆動する「生きた夜空背景」の完全コンポーネント化まで、今日の開発ログをここにまとめる。

スクリーンショット:

[Astro #77] Three.jsで創る電脳魔女STG:プロシージャル夜空背景のコンポーネント化とマスコットのライティング制御 [Astro #77] Three.jsで創る電脳魔女STG:プロシージャル夜空背景のコンポーネント化とマスコットのライティング制御 [Astro #77] Three.jsで創る電脳魔女STG:プロシージャル夜空背景のコンポーネント化とマスコットのライティング制御 [Astro #77] Three.jsで創る電脳魔女STG:プロシージャル夜空背景のコンポーネント化とマスコットのライティング制御 [Astro #77] Three.jsで創る電脳魔女STG:プロシージャル夜空背景のコンポーネント化とマスコットのライティング制御

1. 舞台袖の拡充と新たなマスコットの召喚

中央のゲーム画面を包み込む「3Dの器(ハードウェア)」をビルドアップしていく中で、今回はフロントエンド的にもグラフィックス的にも、非常に濃厚なディテールアップを敢行した。舞台の「ガワ」の完成度を決定づけた、3つのアプローチを詳細に記録する。


1-1. お茶目なログバグのクリーンアップ

ゲーム画面外の左袖に佇む「旅の魔女イレイナ」の3Dモデル(Sketchfabから拉致してきたGLBアセット)だが、どうにも初期姿勢がうつむき加減で顔がよく拝めないという問題があった。モデルに内包されているアニメーションのインデックスを切り替えようと、コンソールに以下のログを仕込んだことから今回のデバッグドラマが始まる。

console.log("🔮 イレイナのアニメーション一覧:", gltf.animations.map(a => a.name));

しかし、意気揚々とリロードした画面のコンソールに表示されたのは、無情にも length: 0 の空っぽの配列

🚨 原因:自機アバターとステージアセットの「混同」

原因は、コードを急いで書いたときによくやるお茶目なミスだった。ログを仕込んだ場所が、本編でプレイヤーが操作する自機アバター(VRMファイル)のロード処理の中になっていたのだ。VRM規格のモデルファイル自体には当然ステージ用のアニメーションは内包されていないため、空の配列が返ってくるのは自明である。

本物のイレイナは、その下層にある 🌿 【地形GLBプリロード】 の一括キャッシュループ(205行目付近)の中でロードされていた。さっそく、以下のように正しいループ内へログの引っ越しを行った。

// 🌿 【地形GLBプリロード】のループ内(205行目付近)
for (const obj of stageObjs) {
  loader.load(url, (gltf) => {
    // 👑 イレイナのデータを読み込んだ瞬間だけピンポイントでログを出す
    if (obj.id === "WIT_TERRAIN_ELAINA") {
      console.log("🔮 本物のイレイナのアニメーション一覧:", gltf.animations.map(a => a.name));
    }
    resolve();
  });
}

🔍 判明したアセットの真実

修正後、コンソールに吐き出されたのは ['Action'] という一本勝負のトラック名だった。Sketchfab等の配布アセットでは、すべてのモーションが1つのタイムラインに結合されて書き出されているケースが非常に多い。

選択肢が1つ(Action)しかないと判明したため、アニメーションのインデックス切り替えによるアプローチは断念。姿勢制御はコード側からオイラー角を直接ハックする方向へとシフトすることになった。


1-2. 物理的な説得力を生む「床の拡張」

モーションの選択肢がない以上、うつむいたイレイナの顔をプレイヤー側へ向かせるには、モデル全体のピッチ(X軸)をのけ反らせるように少しマイナスに傾ける力技ハック(objRotation={[-0.18, Math.PI / 9, 0]} など)が有効となる。しかし、これをそのまま実行すると、別の構造的違和感が牙を剥いた。

📐 浮遊感の解消とパースペクティブの調和

デバッグカメラ(c キー)で斜め上から舞台装置を見下ろした際、足元の土台(黒いステージの縁)の奥行きが狭く、イレイナの足先が今にも宙に浮きそうな危ういバランスになっていたのだ。

そこで、黒いステージの床(縁)のポリゴンを、グッと手前方向(Z軸のマイナス方向)へ物理的に拡張する修正を施した。この数センチの床の拡張が、画面にもたらした効果は絶大だった。

変更前の課題床の拡張による視覚的効果
土台の奥行きが足りず、3Dアセットが浮いて見える足元にどっしりとした接地感が生まれ、舞台の安定感が劇的に向上
2D画面と3D外枠の境界が唐突に途切れる中央の2Dゲーム、左右の3Dマスコット、最背面の部屋の背景が美しい階層構造で繋がる

引きの定位置(Z = 7.5)から見たときのパッと見の違和感は完全に消え去り、むしろ「うつむき加減で舞台袖からゲームを見守るクールな魔女」という最高のニュアンスへと昇華された。ミニチュアの特設ステージとしてのビルドクオリティが、ワンランク引き上げられた瞬間である。


1-3. 魔法使いの黒猫(Wizard Cat)の召喚と専用ライトハック

左袖にイレイナが鎮座したことで、劇場のレイアウトとして完璧なシンメトリー(左右対称)を構築したくなり、右袖の拡張された床の上へと新たなマスコット「Wizard Cat(魔法使いの黒猫)」を召喚した。

{/* 🐈 【舞台脇のマスコット:魔法使いの黒猫】 */}
{terrainScenes.get("WIT_TERRAIN_WIZARD_CAT") && (
  <TerrainInstance
    obj={{ x: 1.25, y: -0.69, z: 0.25, id: "fixed_wizard_cat_mascot" }}
    baseScene={terrainScenes.get("WIT_TERRAIN_WIZARD_CAT")}
    objScale={[0.25, 0.25, 0.25]}
    objRotation={[0, -Math.PI / 4, 0]} // 舞台の内側(画面中心)に顔を向けさせる
  />
)}

🖤 3Dグラフィックス特有の闇「黒潰れ問題」

しかし、画面にレンダリングされた黒猫を見て愕然とする。Sketchfabのビューア上(img15593.jpg)ではあんなに愛くるしかったデザインが、本番環境(img15594.jpg)では暗い茶色の床(#2b2018)と同化し、ディテールが一切見えない単なる真っ黒な不気味なシルエットに成り下がってしまったのだ。

Sketchfabのプレビューでは、以下の要素が奇跡のバランスで噛み合っていた。

  1. 背景が明るいグレーであるため、黒いシルエットがハッキリ立つ。
  2. 独自のシェーダーによる強いツヤ(鏡面反射)が計算されている。
  3. 胸と帽子の鈴が自発光(Emission)し、自身の毛並みをライトアップしている。

対して、現在のWitchadius劇場の内部ライトは一律(ambientLight)に近いため、陰影が死んでしまっていた。

💡 解決策:専用ステージライト(pointLight)の密輸

外部テクスチャの書き換えや重いシェーダーの追加を避け、コード側からのアプローチで「鈴による自発光感」を完全に偽装するハックを考案した。黒猫の至近距離(少し手前・上方向)に、猫の顔と足元の床だけをピンポイントで照らす専用のウォームカラーライトを1本ドッキングしたのである。

{/* 👑 魔法の専用ステージライト(スポット照明) */}
<pointLight
  position={[1.15, -0.55, 0.4]}
  intensity={2.5}       // 暗闇を切り裂く強めの輝度
  distance={0.4}        // 光が広がりすぎてステージ全体のトーンを壊さないようクランプ
  color="#ffaa00"       // 鈴の金色に合わせたエモいオレンジ
/>

✨ 劇的ビフォーアフター

この数行のライトを追加した瞬間、画面の質感が一変した。 均一な環境光とは異なり、至近距離の点光源から放たれた光が、黒猫の顎の下や顔の輪郭に美しいハイライト(ツヤ)を乗せ、潰れていた立体感が見事に復活。

さらに素晴らしいのは、前項で拡張した「黒い床」への照り返しである。オレンジ色の光が床にグラデーション状に漏れ出したことで、「本当に胸の鈴がボウッと熱を持って輝いている」かのような幻想的な空気感が100%再現された。

左端でクールに見守る旅の魔女イレイナと、右端で怪しく光る Wizard Cat。この対比構造が完成したことで、中央のサイバーなドット絵STG画面が、まるで高級な額縁に収まったかのようにグッと引き締まった。

2. アセットインフラのデータ駆動化(SystemAssets.json)

タイトルロゴの画像パスや、赤・青のカーテンテクスチャのパスがコード内にベタ書き(ハードコーディング)されていたため、これらをシステム共通アセットとして完全に分離。 今後のスタッフロール自動化やライセンス管理を見据え、データの身元を証明できる本格的な配列構造の SystemAssets.json を新設した。

{
  "system_assets": [
    {
      "id": "SYS_IMG_TITLE_LOGO",
      "name": "Witchadius Title Graphic",
      "file": "/assets/images/witchadius-title.webp",
      "creator": "LAIN-LAB / WIRED PROTOCOL",
      "comment": "メインタイトル画面の超美麗ドットロゴグラフィック"
    },
    {
      "id": "SYS_TEX_CURTAIN_RED",
      "name": "Stage Curtain Red",
      "file": "/assets/textures/curtain_r.webp",
      "creator": "Kenney",
      "comment": "劇場のウイング(舞台袖)およびメイン大緞帳用の赤フリルテクスチャ"
    }
  ]
}

コンポーネント側からは .find() を用いた安全なフック関数を1本挟み、内部ロジックを1ミリも汚すことなく、身元確かなパスを引き当てる鉄壁のインフラへとクリーンアップを完了。


3. 数式で支配する「完全プロシージャル背景」の錬成

3-1. AI生成画像から「数式による2D Canvas描画」へのパラダイムシフト

STGの最背面にベルトスクロール(無限ループ)する遠景が欲しくなり、当初はAIによるシームレス画像の生成を試みた。しかし、出来合いの静止画をただ裏でシュルシュル滑らせるだけでは退屈であり、何よりこの硬派な電脳空間に対して安直すぎる。

そこで、メモリ上に canvas を生成し、HTML5 Canvas 2D APIで毎フレーム描画した結果を Three.js の CanvasTexture にリアルタイム投影する完全プロシージャル背景(数式ハック)へ舵を切った。

  • ベース夜空: ディープパープルから漆黒へ溶ける、16ビットSTG風の縦リニアグラデーション。
  • たなびく薄雲: 周波数の異なる3つの正弦波(サイン波・コサイン波)を足し算で合成し、有機的でモコモコした霧のウネリを再現。
  • 魔力粒子(蛍の群生): 60個の半透明な光の粉の配列を走らせ、個別フェーズのサイン波をY軸オフセットに噛み合わせることで、不規則な不浮遊感を演出。
  • 遠景の森のシルエット: (x % 3 === 0) のモジュロ演算によるスパイクを周波数に混ぜる数学ハックにより、画像素材を一切使わずに「無限にループする尖ったモミの木の群生」を完全自動で描き出した。

3-2. 視差(パララックス)の隙間を埋める安全マージンハック

構築した背景を3D空間の奥(z = -0.15)に配置した際、手前のカーテン(z = 0.2)との間の前後関係による視差(パースペクティブ)によって、舞台裏の虚無(黒い隙間)が露出するバグが発生。 3D舞台演出の鉄則に則り、呼び出し側の props で背景の板(Mesh)のサイズに安全マージンをガバッと上乗せ(MONITOR_WIDTH + WING_WIDTH * 2 + 0.4)し、見えないカーテンの裏側まで完全に回り込ませることで隙間を100%シャットアウトした。

3-3. 視認性を極める「月の引き算」

試作段階では中央に巨大な陰影付きの満月を描画していたが、実機で動かすと中央の光源が目立ちすぎ、ゲーム本編のドット絵や今後実装される弾幕の視認性を殺してしまうことが判明。 「デザインの引き算」を行い、あえて月をオミット。ディープな夜空、薄雲、流れる粒子、森のシルエットだけに絞り込むことで、最背面の空気感を「奥ゆかしい静寂」へと洗練させ、手前のキャラクターやステージギミックの存在感をビビッドに引き立たせることに成功した。


4. 独立型背景コンポーネント(ProceduralBackground.tsx)への完全外出し

この100行を超える Canvas の重厚な描画ロジックやRefの管理変数を、メインの WITManager.tsx にベタ書きするとコードの認知負荷が爆発するため、完全に独立した ProceduralBackground.tsx として別ファイルへ外出しリファクタリングを敢行。

これにより、本番環境の WITManager.tsx 側は余計な描画エンジンで汚されることなく、わずか1行を記述するだけで、ゲーム本編のスクロール速度(SCROLL_SPEED)とデルタ時間(dt)に完全同期して動き出す「生きた夜空」を最背面に召喚できるようになった。

{/* 🌌 【舞台装置:外出し共通化した完全プロシージャル背景】 */}
<ProceduralBackground
  monitorWidth={MONITOR_WIDTH + WING_WIDTH * 2 + 0.4}
  monitorHeight={MONITOR_HEIGHT + 0.25}
  scrollSpeed={WIT_TERRAIN.scrollSpeed || 0.3}
  active={!showTitleScreen}
/>