[Babylon.js #10] スマートボール試作 — 発射レール制御と物理落下

はじめに

Babylon.js + Havok で、スマートボールを作ってみたのでそのメモです。

今回の到達点は以下。

  • 盤面(傾斜つき)を作成
  • ピン配置
  • ポケット(入賞判定)
  • 発射演出(右側レーンを上がっていく)
  • 途中から物理球に切り替えて落下
  • スコア表示(3D表示)
  • 入賞パーティクル
  • 効果音(※最後に Vite キャッシュ問題で詰まったが解決)

見た目や細かい挙動はまだ調整中ですが、ベースとなる処理はほぼ完成してます。 続編があるかどうかは未定。

前回の記事:

動画(Youtube):

動画(PC):

1. 動きの「心地よさ」を追求する:幾何学的な等速運動と物理への統合

スマートボールの「打ち出し」は、プレイヤーが最初に触れる最も重要な体験です。初期のプロトタイプで感じられた不自然さを解消し、実物の質感を再現するために、以下の3つの改善を行いました。

軌道計算の刷新:ベジェ曲線から「正円円弧」へ

  • ベジェ曲線の限界: 当初使用していた2次ベジェ曲線では、レールのつなぎ目で曲率が急激に変化し、ボールがカクつく原因となっていました。
  • 数学的な円弧の採用: 右上のカーブを半径 (後の調整で )の「正円を4分割した円弧(1/4円)」に書き換えました。
  • 滑らかな遷移: これにより、垂直レールから水平レールへの移行が数学的に完璧な曲線となり、歪みのないスムーズな移動が可能になりました。

移動ロジックの変更:時間ベースから「距離ベース」へ

  • 加減速の排除: 以前のコードでは、区間ごとに smoothstep()を使用していたため、各セグメントの開始と終了でボールが一時停止していました。
  • 等速計算の導入: アニメーションの進行度()を「時間」ではなく、フレームごとの「移動距離」として扱うように変更しました。
  • 一貫した速度: レール全体の長さを計算し、どの位置にいても一定のスピード(launchAnim.t += speed)で動くようにしたことで、視覚的な違和感を完全に払拭しました。

物理エンジンへの滑らかな橋渡し:減速と慣性の継承

  • 自然な落下挙動: 等速で動いていたボールが突然物理エンジンに切り替わると、挙動が唐突すぎて不自然に見えます。
  • 減速ゾーンの設置: レールの終点手前(残り 距離付近)から「ブレーキ」をかける処理を追加しました。
  • 最終速度の継承: アニメーション終了時の「わずかな速度」をベクトルとして Havok 物理エンジンに引き継ぐことで、レールからポロッとこぼれ落ちるような、スマートボール特有の重力感を実現しました。

2. 段ボール工作の質感をデジタルで再現:工作的な制約を逆手に取った意匠

最初はただの無機質な青い板だった盤面を、温かみのある「段ボール工作」の質感へと昇華させました。ここでは、3Dモデリング上の技術的な課題を、あえてアナログな「工作」の手順で解決したプロセスを詳しく解説します。

セグメント方式の受け皿:物理演算と造形の両立

  • 初期手法の限界: 当初は CSG による切り抜きや Torus メッシュの描画範囲(arc)調整を試みましたが、法線の乱れによる影のノイズや、物理判定の不安定さが課題となりました。
  • 「Boxの並列配置」への転換: 解決策として、外枠のアーチでも使用した「小さな直方体(Box)を円周上に並べる」手法をポケットの受け皿にも採用しました。
  • 工作精度の再現: 1つのポケットにつき8〜12個のBoxセグメントを配置し、各Boxをわずかに重ねることで、厚手の段ボールに切り込みを入れて曲げたような「カクカクとした工作感」を意図的に作り出しました。
  • 安定した物理挙動: この手法により、複雑な凹凸メッシュではなくシンプルなBoxの集合体として物理演算(PhysicsAggregate)が処理されるため、ボールがポケットの縁に乗って止まってしまうようなバグを劇的に軽減できました。

鏡文字との格闘:3D空間の表裏とUV座標

  • 反転の謎: DynamicTexture を使って Disc メッシュに数字を描画した際、テクスチャが左右反転して表示される「鏡文字」の問題に直面しました。
  • 「消える」罠: 解決のためにメッシュをY軸で180度回転させると、Babylon.jsのデフォルト設定である「バックフェイス・カリング(背面非表示)」によって、メッシュが完全に消失してしまいました。
  • rotation.z による突破: 試行錯誤の末、面を裏返さずに「ダイヤルを回すように」Z軸で180度回転()させることで、カリングを回避しながら文字を正しい向きで表示できることを発見しました。

断面のディテール

  • 二色の使い分け: 表面の明るい段ボール色(cardboardColor)に対し、側面やBoxの断面には少し暗い色(cardboardDarkColor)を割り当て、素材の「切り口」を視覚的に強調しました。
  • ニス塗りの光沢: 最終的には、この段ボールマテリアルに specularPower を調整してわずかな光沢を与え、工芸品のような「ニス塗り」の風合いを擬似的に再現しています。

3. 高級感を演出する「真鍮」と「金箔」:デジタル・クラフトマンシップの極致

「最低限見られる形」であった段ボール工作に、金属の輝きと工芸品の気品を加え、ヴィンテージのアーケードマシンのような風格を目指しました。

真鍮製のピン:光沢と反発力の調和

  • 素材のアップグレード: ピンのマテリアルを段ボールから「真鍮(しんちゅう)」へと変更しました。StandardMaterial を使用し、真鍮特有の重厚な黄色(brassColor)を設定しています。
  • 鋭いハイライト: specularPower を 64 まで引き上げ、specularColor に暖色系の白を割り当てることで、スポットライトを反射してカチッと輝く金属特有の質感を表現しました。
  • 物理的な手応え: 視覚的な変化に合わせ、物理的な反発係数(restitution)も 0.6 まで向上させました。これにより、ボールがピンに当たった際、金属同士がぶつかり合うような小気味よい跳ね返りが実現しています。

金箔の縁取りとニス塗り:積層テクスチャの妙

  • 金箔の描画: DynamicTexture を用い、盤面全体を囲む二重のラインや四隅の優雅な幾何学模様をゴールド(goldFoilColor)で描画しました。
  • 擬似ニス塗り加工: 当初検討した PBR(物理ベースレンダリング)の読み込みエラーを回避するため、StandardMaterial の光沢設定を追い込むことで「ニス塗り」を再現しました。specularPower を 64 に設定し、盤面全体にクリアコート層を重ねたような、濡れたような艶やかな光沢を与えています。
  • ドラマチックな照明: 上部から SpotLight を照射し、金箔の装飾や真鍮パーツの影を立体的に引き立たせるライティング設計を行いました。

真珠のボール:乳白色の深み

  • 素材の安定化: PBRMaterial による外部読み込みエラー(Failed to fetch dynamically)を受け、安定性の高い StandardMaterial でリデザインしました。
  • 深みのある質感: オフホワイトの拡散色に対し、specularPower を 128 という極限値まで高めることで、磨き抜かれた大理石や真珠のような輝きを表現しました。
  • 自己発光(Emissive)の隠し味: わずかに emissiveColor(0.1程度)を加えることで、真珠層の内側から光が滲み出るような、高級感のある乳白色の透明感を擬似的に作り出しています。

4. 五感を刺激するインタラクション

制作の最終段階として、視覚的・聴覚的なフィードバックを強化し、単なる静止したモデルではない「遊べるゲーム機」としての生命を吹き込みました。

3Dスコアボード:半透明のスモークガラス表現

  • 質実剛健なUI: 盤面の左下付近に、高級感のある「半透明のスモークガラス」をイメージしたスコアボードを設置しました。
  • 透過処理の最適化: 背景の scorePlatealpha = 0.4 を設定し、文字盤のマテリアルに transparencyMode = MATERIAL_ALPHABLEND を適用することで、背後の盤面がうっすらと透けて見える洗練された質感を実現しました。
  • 反転問題の終止符: rotation.z = Math.PI によるメッシュ自体の回転と、Canvas描画時の scale(1, 1) 設定を組み合わせることで、鏡文字になる問題を完全に解消し、可読性と意匠性を両立させました。

黄金のパーティクル:入賞の歓喜を視覚化

  • 動的エフェクト: ボールがポケットのトリガーを叩いた瞬間、その座標(pocket.pos)から黄金の火花が散る演出を追加しました。
  • 粒子設計: ParticleSystem を活用し、goldParticleColor(金)からオレンジへと変化する100個の粒子を放射状に放出しています。重力設定(gravity)を加えることで、光の粒が一度舞い上がってからハラハラと落ちる、情緒的な動きを追求しました。

音の壁を越える:物理エンジンとWebオーディオの統合

  • オーディオロックの解除: ブラウザの自動再生制限を回避するため、最初のスペースキー(打ち出し操作)が行われた瞬間に Engine.audioEngine.unlock() を実行し、ユーザー操作に同期した音響再生を可能にしました。
  • Havok衝突通知の有効化: 物理エンジンの負荷を抑えるための標準制限を解除し、setCollisionCallbackEnabled(true) を明示的に呼び出すことで、ボールがピンに当たった瞬間のイベントを取得しました。
  • 素材の音色: 真鍮ピンとの接触には乾いた「カツッ」という打撃音(chipsHandle1.wav)を、入賞時には華やかなファンファーレ(win.wav)をそれぞれ割り当て、視覚的な高級感と聴覚的な満足感を一致させています。

5.ロジックで解決した3つの壁

1. 等速移動を実現する「距離ベース」のアニメーション

当初、打ち出しレールでのボールの動きは、全体の時間(0から1)に依存した計算だったため、直線の長さとカーブの長さの比率によって速度が不自然に変化していました。

解決コード:

// フレームごとの移動距離を一定にする
const maxSpeed = 0.12 + 0.1 * charge01;
launchAnim.t += currentSpeed; // t を「時間」ではなく「累計距離」として扱う

// 各セクションの距離を事前に計算
const len1 = ay1 - ay0; // 垂直レール
const len2 = (Math.PI * radius) / 2; // 円弧
const len3 = Math.max(0, cx - launchAnim.dropX); // 水平レール

この計算により、セクションをまたいでもボールが一定の速度で滑らかに動く「物理的な一貫性」を確保しました。

2. CSG(空間領域構成法)を避けたセグメント配置

ポケットの受け皿を作る際、メッシュを削り取るCSG処理では、影の計算(法線)が乱れたり、物理判定が複雑になりすぎる問題がありました。これを「小さなBoxを数学的に並べる」工作的なアプローチで解決しました。

解決コード:

// PIから2*PIの範囲(U字の下半分)にパーツを並べる
for (let i = 0; i < catcherSegments; i++) {
  const angle = Math.PI + ((i + 0.5) / catcherSegments) * Math.PI;

  segment.position.set(
    d.x + catcherRadius * Math.cos(angle), // 円周上のX座標
    d.y + catcherRadius * Math.sin(angle), // 円周上のY座標
    -0.12
  );
  // 接線方向に回転させる
  segment.rotation.z = angle - Math.PI / 2;
}

三角関数()を用いた配置により、軽量かつ影のつき方が綺麗な、段ボールの「曲げ」を表現した造形が可能になりました。

3. 「鏡文字」を物理的に裏返さず解決する

DynamicTextureDisc メッシュに貼ると、裏側から透かして見たような反転表示(鏡文字)になる問題がありました。

解決コード:

// メッシュ自体を裏返す(rotation.y)と、カリング設定で消えてしまう
// そのため、面をカメラに向けたまま「ダイヤル」のように180度回す
textPlane.rotation.z = Math.PI;

// Canvas描画側での反転設定(scale)との組み合わせで完全一致させる
ctx.save();
ctx.translate(256, 128);
ctx.scale(1, 1); // rotation.z との相乗効果で反転を相殺

この というアプローチは、3D空間における座標系(UV)の理解を深める重要な転換点となりました。

6.まとめ:デジタル工作が「工芸品」に変わる瞬間

今回のプロジェクトを通じて、ただのプログラムが「手触り感のある体験」へと進化する過程には、以下の3つの要素が不可欠であることを学びました。

1. 物理的な「正しさ」よりも「心地よさ」

  • 等速運動の魔法: 数学的に完璧な等速移動を実装することで、視覚的なストレスが消え、操作に対する信頼感が生まれました。
  • 意図的な「溜め」: レール終端での減速処理が、ボールに「重さ」という実在感を与えました。

2. 制約を「意匠」に変える発想

  • セグメント方式: 複雑な凹凸を避けるための「Boxの並列配置」が、結果として厚手の段ボールを曲げて作った工作特有の美しさを生み出しました。
  • 座標軸の逆転: という解決策は、3D空間の表裏一体な性質を逆手に取った、非常にスマートなアプローチでした。

3. 五感を満たすリッチなフィードバック

  • マテリアルの対比: マットな段ボール、輝く真鍮、深みのある真珠。異なる質感をスポットライトで強調することで、画面の中に「高級感」という空気感が漂い始めました。
  • インタラクションの完成: 黄金のパーティクルと、物理エンジンに同期したSEが加わったことで、入賞という「結果」が「最高の体験」へと昇華されました。

結びに

「努力すればどうにかなる」——。 この言葉通り、コード一行、光の強さ一つを丁寧に追い込んでいくことで、想像を超えたクオリティに辿り着くことができました。

デジタルな世界でのモノづくりは、まだ始まったばかりです。