← ブログに戻る

Bloom の現在地: 設計上の選択、進捗、そして次に来るもの

Bloom は今週で 5 週目を迎えました。311 コミットを経て、何ができて、なぜこのような形で作ったのか、そしてどの部分がまだ粗いのかを書き留めておきたいと思いました。

ピッチを 1 文で

ゲームを TypeScript で書き、事前コンパイルし、macOS、Windows、Linux、iOS、tvOS で本物のネイティブバイナリを出荷します — もしくは Web 向けの WASM バンドルとしても。Electron も、WebView も、出荷されるゲームに組み込まれた JavaScript ランタイムもありません。

この 1 文には多くの意味が込められています。それはまた、初期の設計判断の多くが今のような形になっている理由でもあります。

なぜ TypeScript なのか

JavaScript が好きだから TypeScript を選んだのではありません。構造的型システム、巨大なツールエコシステム、そして誰も怖がらせない構文を持つ、最も広く使われている静的型付け言語だから選びました。私たちが一緒にゲームを作りたいと思っている人たちのほとんどは、TypeScript で何かを出荷した経験があります。C++ で出荷した経験のある人はごくわずかです。

また、ガベージコレクタやバイトコードインタプリタを出荷バイナリに引きずり込むことなく、クリーンに事前コンパイルできる言語が欲しいと考えていました。これにより、重いランタイムを必要とするもの (Python、C#、JS そのもの) は除外されました。動的な既定動作の部分を取り除いた TypeScript は、ちょうど良いスイートスポットだったのです。

なぜ Perry なのか

Perry は、TypeScript をネイティブコードに変換する事前コンパイラです。TS からバイナリへの経路こそが、私たちが “ランタイムオーバーヘッドなし” を約束できる理由です。ゲームは単一のバイナリとなり、安定した C ABI を介して Rust コアを呼び出します。V8 も、Bun も、JIT もありません。

Perry を選んだことで、公開する言語面に対して容赦なくなれました。Bloom の API は関数とプレーンなインターフェースだけです — クラスも、デコレータも、プロキシも、eval もありません。TypeScript の機能がネイティブ呼び出しにクリーンにコンパイルできないのであれば、API では使いません。その結果、API はチートシート 1 枚に収まり、ビルドの全体像も頭の中に収まります。

なぜ 4 つのカスタムレンダラーではなく wgpu なのか

最初の本能は、Metal レンダラーを書き、次に DirectX 12 レンダラーを書き、その次に Vulkan レンダラーを書く、というものでした。週末のうちに自分たちを説得してやめさせました。4 つのバックエンドというのは、4 つのシェーダー言語、4 つのリソースモデル、4 つのバグの表面、そして HiDPI を忘れる場所が 4 つあるということです。

代わりに、レンダラー全体を wgpu の上に一度だけ書きました。シェーダーは WGSL です。Apple プラットフォームでは Metal、Windows では DirectX 12、Linux と Android では Vulkan、ブラウザでは WebGPU (WebGL フォールバック付き) を 1 つのコードベースから得ています。コストとして wgpu の機能セットに縛られ、いくつかのエキゾチックなもの (メッシュシェーダー、レイトレーシング) は当面利用できません。小さなチームにとっては、これは妥当な取引だと考えています。

今日、実際に箱の中に入っているもの

動かないものをマーケティングサイトに載せないようにしています。今日、実在するのはこちらです:

  • 9 つのインポート可能なモジュールbloom/corebloom/shapesbloom/texturesbloom/textbloom/audiobloom/modelsbloom/mathbloom/physics、そして bloom/scene
  • 本格的な PBR レンダラー — サブストレート方式のレイヤードマテリアル、静的ジオメトリ向けにキャッシュされるカスケードシャドウマップ、ACES/AgX トーンマッピング、自動露出、ブルーム、被写界深度、モーションブラー、SSGI、SSAO、TAA、そして分数レンダースケール用の CAS シャープニングパス。
  • GPU スケルタルアニメーション — glTF 2.0 インポート、GPU 上での 4 ボーンリニアブレンドスキニング、スケルトンあたり最大 128 ジョイント。
  • Jolt 物理 — リジッドボディとソフトボディ、キャラクターコントローラー、車両、レイキャスト、コンストレイント、コンタクトコールバック。Web ターゲットでは JoltPhysics.js フォールバックを使用するため、同じコードがブラウザでも動作します。
  • 6 つのターゲットプラットフォーム — macOS、Windows、Linux、iOS、tvOS、Web。Android は部分的に配線されていますが、まだ出荷可能ではありません。
  • ホットリロード — WGSL シェーダーやマテリアル JSON を保存すると、開発中は変更が 1 秒以内に画面に反映されます。ファイル監視コードはリリースビルドからは取り除かれます。
  • 17 個のサンプルプロジェクト — 170 行の Pong から、Intel Sponza や Bistro シーンの読み込みまで。

最近誇りに思っていること

レンダラーは良い 1 ヶ月でした。いくつかのハイライト:

  • Auto-DRS。 レンダラーがターゲットフレームレートに到達するようレンダースケールを自動調整し、アップスケールに加えて RCAS シャープニングパスを実行することで、画像が鮮明に保たれます。ターゲット FPS を設定すれば、あとはエンジンが処理します。
  • プラナーリフレクション。 斜めクリップを備えた本物のミラープレーンキャプチャと、リフレクション予算が枯渇したときの IBL フォールバック。水面、磨かれた床、店舗のショーウィンドウなどに有用です。
  • テクスチャ配列スプラットマッピング。 地形やディテールレイヤーが、適切なミップを備えたテクスチャ配列をバインドできるようになりました。これにより、レイヤーごとにドローコールを支払うことなく、タイルあたり複数のマテリアルをペイントできます。
  • インポスターベイカー。 遠景 LOD 用に八面体インポスターアトラスをベイクする小さな CLI です。シーンに森を落とし始めた瞬間に、これが必要だと気づきました。
  • クロスプラットフォーム HiDPI。 Windows、Linux、Web もようやく、macOS と iOS が既に備えていた HiDPI 処理を共有するようになりました。あらゆるディスプレイで UI が鮮明に保たれます。

完成したふりをしていないもの

見栄えの悪いリストもお伝えしておきます:

  • Android。 プラットフォームクレートは存在し、FFI 表面のほとんどは配線済みですが、ネイティブアクティビティのグルーコードは完成していません。Bloom で Android を出荷するよう人にお勧めしている段階ではありません。
  • watchOS。 シェーダーはコンパイルでき、プラットフォームのスタブもありますが、これが本物になるには Perry の watchOS 対応待ちです。
  • 仮想化ジオメトリ。 Nanite 相当のものはなく、メッシュシェーダーもなく、ハードウェアレイトレーシングもありません。これはロードマップ上にあり、バイナリには含まれていません。
  • スケルタルアニメーションの上限。 UBO サイズの都合で、スケルトンあたり 128 ジョイントの厳密な上限があります。大きなリグは現在は回避策が必要です。
  • シーングラフはまだ若いです。 トランスフォーム、可視性、シャドウ、マテリアルバインディングはすべて動作します。クエリシステムや、より広範な最適化パスはまだ存在しません。
  • ユーザーシェーダー。 WGSL を手書きしてマテリアルパイプラインから読み込むことはできますが、エンジン内シェーダーグラフはなく、TypeScript からのランタイムシェーダーコンパイルもありません。

API のかたち

API のイミディエイトモード部分については、Raylib から大きく拝借しました。Raylib でゲームを書いたことがあれば、その筋肉の記憶は引き継がれます:

import { initWindow, windowShouldClose, beginDrawing,
         endDrawing, clearBackground, drawText, Colors } from "bloom";

initWindow(800, 450, "Hello Bloom");

while (!windowShouldClose()) {
  beginDrawing();
  clearBackground(Colors.RAYWHITE);
  drawText("Hello, Bloom!", 190, 200, 20, Colors.DARKGRAY);
  endDrawing();
}

3D や物理の作業についても、同じ哲学が当てはまります: プレーンなインターフェース、純粋関数、隠れた状態マシンはありません。カメラはオブジェクトリテラルです。リジッドボディはハンドルです。レンダリングパイプラインは、不可視のオーケストレーターにリスナーを登録するのではなく、特定の順序で関数を呼び出すことで設定されます。

次に来るもの

今後数週間のショートリスト:

  • Android のネイティブアクティビティのグルーコードを完成させ、Android を出荷可能と呼べる状態にします。
  • シーングラフの作業を “基本” を超えて実際に有用な領域へ — クエリ、フラスタムカリングの精緻化、インスタンシングヘルパーへと押し上げます。
  • ゲームを再起動せずにシーンを反復できるよう、エディタ統合の最初のカットをリリースします。
  • サンプルゲームをオープンソース化します。Pong よりも少し大きなデモも含めます。
  • Perry のコンパイルパイプラインを詳しく書き上げます。何人かの方から質問をいただきました。

試してみる

Bloom は MIT ライセンスのオープンソースです。リポジトリ全体は GitHub にあります。フォークせずに動向を追いかけたい場合は、ドキュメント に 12 行のクイックスタートがあり、ショーケース ページには私たち自身が Bloom で作っているものを掲載しています。

いずれにせよ、目を通していただきありがとうございます。より大きなピースが揃い次第、引き続きここで投稿していきます。