この記事について
前回の記事は「ブラウザの3Dゲームは、基本的に手元の端末のGPUがリアルタイムに描画している」という全体像を説明しました。今回はその一歩先、3Dモデルやテクスチャといった「アセット」が、具体的にどうやってブラウザに届き、どこに保存されているのかを掘り下げます。
そもそもアセットとは何か
3Dゲームにおける「アセット」とは、主に以下のようなデータを指します。
- 3Dモデル(メッシュ):キャラクターや建物などの形状データ(頂点の集合)
- テクスチャ:モデルの表面に貼る画像データ
- アニメーション:モデルの骨格の動き
- 音声:効果音やBGM
これらは動画や音楽ファイルと同様に「重いデータ」であり、何も工夫しないとダウンロードだけで数十秒〜数分かかってしまいます。そこで、ブラウザゲームの世界ではいくつもの軽量化・配信の工夫が積み重ねられています。
アセットの軽量化:圧縮フォーマット
モデルの標準フォーマット:glTF / GLB
Web向けの3Dモデルでは、glTF(またはバイナリ版の.glb)というフォーマットが事実上の標準になっています。これはKhronos Group(WebGLなどの標準化団体)が策定したオープンフォーマットです。
ジオメトリ圧縮:Draco
glTFにはさらにDracoという圧縮技術を組み合わせられます。GoogleのDracoライブラリは、3Dのメッシュデータ(頂点座標や面の情報)を圧縮・展開するために開発されたオープンソースライブラリで、2018年にKhronos GroupによってglTF 2.0向けの拡張仕様として正式に採用されました。
イメージとしては、画像でいうところのZIP圧縮のようなものだと考えると分かりやすいです。ジオメトリの情報をそのまま送るのではなく、圧縮した状態で送り、ブラウザ側(JavaScript)で展開してからGPUに渡します。
テクスチャ圧縮:Basis Universal / KTX2
テクスチャ画像についても同様の工夫があります。KHR_texture_basisuという拡張を使うと、テクスチャをKTX2/Basis Universalという形式で保存でき、これによりGPU側にネイティブな圧縮状態のままアップロードできるようになります。通常のPNG/JPEGと比較して、GPUメモリ上での使用量を大きく削減できるのが特徴です。
アセットの届け方:全部先読みか、部分ロードか
圧縮しても、大規模なオープンワールドのアセットを全部合わせると数百MB〜数GBになることも珍しくありません。そこで「届け方」にも工夫があります。大きく2つのパターンがあります。
パターン1: 全部先読み型(プリロード)
シーンを表示する前に、必要なアセットを全部ダウンロードし終えてから描画を開始する方式です。よくある「ローディングバーが100%になるまで待つ」画面がこれにあたります。
JavaScriptの3DライブラリであるThree.jsにはLoadingManagerという仕組みがあり、これを使うとアセットの読み込み進捗を追跡し、すべて読み込み終わった時点でコールバック関数を実行する、という制御ができます。実際のThree.js公式チュートリアルでも、GLTFLoaderやCubeTextureLoaderにLoadingManagerを渡すことで全体の進捗を把握し、進捗バーを表示してから、読み込みが完了した時点でフェードインしてシーンを見せる、という実装パターンが紹介されています。
簡略化したコード例:
const manager = new THREE.LoadingManager();
manager.onProgress = (url, itemsLoaded, itemsTotal) => {
console.log(`${itemsLoaded} / ${itemsTotal} 個のアセットを読み込み中`);
};
manager.onLoad = () => {
console.log("全アセットの読み込み完了。シーンを表示します");
showScene();
};
const gltfLoader = new GLTFLoader(manager);
gltfLoader.load('character.glb', (gltf) => {
scene.add(gltf.scene);
});
パターン2: 部分・オンデマンド型
一方で、最初は必要最低限のアセットだけロードし、プレイヤーが進んだ先のエリアに応じて追加のアセットを読み込んでいく方式もあります。
Unity公式マニュアルでは、AssetBundleという仕組みが解説されています。これはUnityでコンテンツを進めるにつれて必要なアセットをオンデマンドで動的にロードする仕組みで、不要になったアセットデータをメモリから解放することでメモリ管理にも役立つとされています。同時に、あえて全アセットを最初に一括ダウンロードしておく設計にする場合は、メインのデータファイルからアセットを切り出して専用のAssetBundleにまとめることで、より軽量なローダー画面を作れるとも説明されています。
つまり、「全部先読み」か「オンデマンド」かは、開発側がどちらの体験を優先するかで選べる設計上の選択肢である、ということです。
| 全部先読み型 | オンデマンド型 | |
|---|---|---|
| メリット | 一度読み込めば途中で引っかからない | 最初の起動が速い、メモリ効率が良い |
| デメリット | 最初の待ち時間が長い | プレイ中に新しいエリアで追加ロードが発生する |
| 向いているケース | 小〜中規模のシーン | 広大なオープンワールド、MMO |
ブラウザ側でアセットをどこに溜めておくのか
ダウンロードしたアセットは、ブラウザ内のいくつかの「置き場所」に格納されます。用途によって使い分けられています。
一時的な置き場所(セッション中のみ)
- JSヒープ(メモリ):ダウンロードした生データや、パース(解析)した後のオブジェクトが乗る場所
-
GPUメモリ(VRAM):WebGLの
gl.bufferData()などを通じてGPUにアップロードされたテクスチャやメッシュのデータ。ここはJavaScriptの世界から直接は見えない領域
これらはタブを閉じたり、メモリ不足でタブがリロードされたりすると消えてしまいます。
永続的な置き場所(ブラウザを閉じても残る)
Cache API
Service Worker(バックグラウンドで動くスクリプト)と組み合わせて使われることが多い仕組みで、HTTPのレスポンスをそのままキャッシュするのに向いています。MDNの解説では、Cache APIは特定のリクエストに対するHTTPレスポンスの保存に特化した仕組みだと説明されています。glTF/GLBファイルやテクスチャ画像のような「ダウンロードしたファイルそのもの」をキャッシュするのに使われます。
IndexedDB
ブラウザ内の本格的なデータベースシステムです。MDNでは、IndexedDB APIは複雑なデータを扱えるブラウザ内データベースシステムであり、音声・動画のような複雑なデータ型も保存できると説明されています。解凍・解析済みのアセットデータ(Draco展開後のジオメトリなど)をキャッシュしておき、次回訪問時に再度の解凍処理をスキップする、といった用途で使われることが多いです。
両者の使い分けの目安として、Microsoft Edgeの開発者ドキュメントでは、IndexedDB APIは大量の構造化データや暗号化されたメディアオブジェクトのようなバイナリデータの保存に向いていると説明されており、WebSQLやApplication Cacheという古い技術はすでに非推奨で、代わりにIndexedDBやCache APIを使うべきだとも明記されています。
まとめると、こんな構造になります。
なお、localStorage/sessionStorageは文字列しか保存できずサイズ上限も数MB程度のため、3Dアセットのような大きなバイナリデータの保存には使われません。
この記事のポイントまとめ
- 3Dアセットは、そのまま送るには重すぎるため、
glTF/GLB+Draco(ジオメトリ圧縮) +Basis Universal/KTX2(テクスチャ圧縮)のような組み合わせで軽量化されている - アセットの届け方には「全部先読み」と「オンデマンド(部分ロード)」の2パターンがあり、開発側がゲームの規模や体験に応じて選択する
- ブラウザ内の保存場所は「一時的(JSヒープ・GPUメモリ)」と「永続的(Cache API・IndexedDB)」に分かれており、用途によって使い分けられている
次回は、「UnityやUnreal Engineは、Three.jsのようなJavaScriptライブラリを使っているのか?」という疑問に答える形で、その裏側の変換技術を掘り下げます。
参考資料
- Khronos Announces glTF Geometry Compression Extension Using Google Draco Technology
- Draco Compressed Meshes with glTF and 3D Tiles – Cesium
- glTF/GLB 解説記事(KHR_texture_basisu 拡張について)
- MDN: Client-side storage
- Microsoft Edge Developer docs: Store data on the device
- Three.js Journey: Intro and loading progress
- Unity Manual: AssetBundles in WebGL