【ゲーム制作】アセットのバイナリベイクとは?|ロードを速くする仕組みを初心者向けに解説

アセットのバイナリベイクのサムネイル ゲーム制作

今回は、ゲーム制作で「ロードを速くする」ために欠かせない、アセットのバイナリベイク(バイナリ化)の仕組みについて解説していきます。

「FBXやPNGを毎回読むとロードが遅い…」
「市販ゲームの独自拡張子(.pakとか)って何が入ってるの?」
「ベイクって聞くけど、要するに何をしているの?」

こんな悩みはありませんか?

アセットのバイナリベイクとは、ひとことで言うと「実行時にそのまま使える形」に前もって変換して保存しておくことです。FBXやPNGのような汎用フォーマットは、人間や外部ツールに都合よくできていて、ゲームがそのまま使うには「解析(パース)」という重い下ごしらえが必要です。それを起動のたびにやるのは無駄なので、事前に一度だけ変換して独自形式で保存しておくわけです。

この記事では、なぜ汎用形式は遅いのか・ベイクで何をするのか・独自バイナリの中身・実装の考え方・ハマりどころまで、C++の例つきで解説します。読み終えると、ロード時間を大きく縮める仕組みが分かるようになりますので、ぜひ最後まで読んでください。

なぜ汎用フォーマットは遅いのか

FBX・OBJ・glTF・PNGといった汎用フォーマットは、編集ツールとのやり取りを前提に作られています。そのため、ゲームが使うまでに次のような下ごしらえが必要です。

  • 解析(パース)… テキストや複雑な構造を読み解いて意味を取り出す
  • 展開(デコード)… PNGは圧縮されているので、生のピクセルに戻す必要がある
  • 再構築… 頂点やインデックスを、GPUに渡せる並びに組み直す

これらはCPUをそこそこ使う重い処理で、しかも起動するたびに毎回同じことをやり直しているのが無駄の正体です。モデル形式の話はGLBの記事、画像の話はPNGとHDRの記事もあわせて読むと理解が深まります。

ベイクで何をするのか|「使える形」で固める

バイナリベイクは、この重い下ごしらえをビルド時(開発中)に一度だけ済ませ、結果を独自形式でファイルに書き出す作業です。ちょうど、材料を切って下味をつけた状態で冷凍しておくイメージです。実行時は「解凍して並べるだけ」で済みます。

具体的には、ベイクで次のような「済ませておく」処理を行います。

  • 頂点データをGPUのバッファにそのままコピーできる並びに整える
  • PNGをデコード済み、あるいはGPU圧縮テクスチャ(DDS/BC)に変換しておく
  • 文字列名を数値ID(ハッシュ)に置き換え、参照を軽くする

ポイントは、実行時にやることを「メモリに読み込む → ほぼそのまま使う」まで減らすことです。理想は、ファイルの中身をメモリにドンと読んで、そのアドレスを構造体として解釈するだけ、という状態です。

独自バイナリの中身|ヘッダ+データ本体

ベイクした独自ファイルは、たいていヘッダ(目次)+データ本体という構成です。ヘッダには「何が・どこに・どれだけ入っているか」を書いておきます。

// 独自メッシュ形式のヘッダ例
struct MeshHeader
{
    char     magic[4];     // "MESH" など識別子(壊れ検出用)
    uint32_t version;      // フォーマットのバージョン
    uint32_t vertexCount;  // 頂点数
    uint32_t indexCount;   // インデックス数
    uint32_t vertexOffset; // 頂点データの位置(先頭からのバイト数)
    uint32_t indexOffset;  // インデックスデータの位置
};

読み込み側は、このヘッダを読むだけでどこに何バイトあるかが分かるので、あとは該当位置からメモリへ一気にコピーするだけです。テキスト解析のような処理は一切いりません。

// ベイク済みファイルを読む(イメージ)
std::vector<char> blob = ReadAllBytes("player.mesh"); // 丸ごと読む
auto* h = reinterpret_cast<MeshHeader*>(blob.data());

// magicで壊れ・別形式を検出
if (memcmp(h->magic, "MESH", 4) != 0) return false;

// 該当位置を「そのまま」頂点配列として使える
auto* verts   = reinterpret_cast<Vertex*>(blob.data() + h->vertexOffset);
auto* indices = reinterpret_cast<uint32_t*>(blob.data() + h->indexOffset);
// verts / indices をそのままGPUバッファへコピーするだけ

市販ゲームでよく見る .pak のような「まとめファイル」は、これをさらに発展させて複数のアセットを1つの大きなファイルに連結し、目次で管理したものです。ファイルを細かく開かずに済むので、読み込みがさらに速く・安定します。

ベイクのメリットとデメリット

  • メリット…ロードが速い/実行時のCPU負荷が軽い/不要な編集用データを捨ててサイズも減る/中身が見えにくくなる
  • デメリット…変換の仕組み(ベイクツール)を自分で用意する必要がある/フォーマットを変えると作り直し(だからversionを持たせる)/人間が中身を直接見づらい

【重要】私がバイナリベイクでハマった体験談

個人開発でメッシュを独自バイナリ化したとき、自分のPCでは動くのに、別のPCに持っていくとモデルがぐちゃぐちゃに崩れるという怪奇現象に遭いました。データは同じはずなのに、です。

原因は構造体のパディング(詰め物)でした。C++の構造体はメンバの並びによってコンパイラが自動で隙間を空けることがあり、書き出したときと読み込むときで構造体のサイズがズレていたんです。#pragma packで詰め方を固定し、さらにヘッダにmagicとサイズを持たせて検証するようにしたら、二度と崩れなくなりました。

「バイナリはサイズと並びがすべて」と痛感した出来事でした。

バイナリベイクのよくある失敗例と対処法

①環境によって読み込みが崩れる

体験談のとおり、構造体のパディングが原因なことが多いです。#pragma packで詰め方をそろえる、あるいは1メンバずつ明示的に読み書きすると、環境に依存しなくなります。

②フォーマット変更後に古いファイルで落ちる

データ構造を変えたのに古いベイク済みファイルを読むと、位置がズレてクラッシュします。ヘッダにversionを必ず持たせ、読み込み時に想定バージョンと違えば作り直す(またはエラーにする)ようにしましょう。

③元データ(FBX等)を消してしまう

ベイク済みは「調理済み」なので、そこから元のFBXへ戻すのは困難です。元アセットは必ず残し、ベイク結果は「元から自動生成されるもの」として扱いましょう(元を直して再ベイクできる状態を保つ)。

注意点

  • ヘッダにmagic・version・サイズを持たせて検証する
  • 構造体はパディングに注意(詰め方を固定する)
  • 元アセットは捨てない。ベイク結果は再生成できるものとして管理

まとめ

  • 汎用フォーマットは解析・展開が重く、毎起動やり直すのが無駄
  • ベイクは事前に「使える形」へ変換して独自形式で保存する作業
  • 独自ファイルはヘッダ(目次)+データ本体で、読むだけで使える
  • 落とし穴はパディング・バージョン・元データの管理

バイナリベイクは、ロード時間という地味だけど効くところを大きく改善してくれます。まずは1種類のアセット(メッシュか画像)から、ヘッダ付きの独自形式にしてみると仕組みが体で分かりますよ。

関連記事