🎯 目的
- 既存の都市モデルデータ(3D Tiles 1.0 / B3DM形式)を最新の標準仕様である 3D Tiles 1.1 に移行する。
- deck.gl v9 の WebGPU レンダラで発生する互換性エラーを解消し、表示を最適化する。
💥 遭遇した主な課題
1. Error: size: 1 によるレンダリング失敗
deck.gl v9 (luma.gl v9) は WebGPU をサポートするため、属性データのバリデーションが非常に厳格になりました。
-
現象: タイルをロードしようとすると
ScenegraphLayerでError: size: 1というエラーが発生し、描画が止まる。 -
原因: 変換元の
.b3dmに含まれていたレガシーな_BATCHID属性(各頂点がどの建物に属するかを示すID)が、GLTFのアクセサとして残存していました。この属性はスカラ(1成分)ですが、WebGPU のパイプライン設定と不整合を起こし、無効な属性として拒否されました。 -
複雑化要因: データが Draco圧縮 されていたため、単純に JSON の
attributesからキーを削除しても、Draco 拡張定義 (KHR_draco_mesh_compression) 側に定義が残っており、ローダーが展開時に属性を復活させてしまっていました。
2. 破壊的なバイナリ操作によるファイル破損
.glb ファイルはバイナリデータ(BINチャンク)のアライメント(4バイト境界)に厳密です。手作業や単純なスクリプトで JSON 部分のみを編集してサイズが変わると、後続の BIN チャンクへのオフセットがずれ、パースエラー(No valid loader found)を引き起こしました。
3.「地球2つ分」の座標ズレ (Double Transform Problem)
WebGPU 対応のために拡張機能 CESIUM_RTC (Center Coordinate) を削除し、そのオフセット値を標準の tileset.json の transform 行列に移動させました。
- 現象: ズームインするとモデルが消える(あるいは遥か上空や地下に飛ぶ)。
- 原因: 親タイルにも子タイルにも「絶対座標(ECEF: 地球中心固定座標)」の移動行列を設定してしまったため。3D Tiles のトランスフォームは階層的に乗算されるため、「親の絶対座標」×「子の絶対座標」となり、地球の直径の数倍の彼方にモデルが配置されていました。
🛠️ 解決策のアプローチ
アプローチ1: 完全な「バニラ化」 (Compression & Extension Removal)
互換性問題を根本から断つため、特定のローダーや拡張機能に依存しない、最も標準的な 「バニラな GLB (Uncompressed GLB)」 形式への変換を選択しました。
-
Draco 圧縮の解除:
gltf-transformやgltf-pipelineを使用して、幾何データを一度完全に解凍しました。これにより、ブラックボックス化していた_BATCHID属性をメモリ上の明確なアクセサとして展開し、安全に削除可能にしました。また、deck.gl v9 側のデコーダー依存も排除できました。 -
拡張機能の全廃:
CESIUM_RTCなどのベンダー拡張をファイルから削除し、純粋なPOSITION(頂点) とNORMAL(法線) のみを持つシンプルな GLTF 構造にしました。
アプローチ2: 再帰的な相対座標計算
tileset.json 側で座標系を正しく管理するための修正を行いました。
-
RTC Center の抽出: 各 GLB ファイルが持っていた
CESIUM_RTC.center(絶対座標) を抽出。 -
相対トランスフォームの計算:
-
Rootタイル: 絶対座標をそのまま
transformに適用(世界座標系への配置)。 -
Childタイル: 親タイルの絶対座標と、自分自身の本来の絶対座標(RTC)の差分(相対ベクトル) を計算し、それを
transformに設定。
Child.transform = Translation(Child.RTC - Parent.AbsolutePos)
-
Rootタイル: 絶対座標をそのまま
これにより、階層が深くなっても、「親の位置 + 子の相対位置 = 正しい絶対位置」が成立するようになりました。
💻 実施した処理フロー (自動化スクリプト概略)
今回の解決のために作成したスクリプト(apply_fix_batch.js)のロジック概要です。
function processTileHierarchy(tile, parentAbsolutePosition) {
let myAbsolutePosition = parentAbsolutePosition; // デフォルトは親と同じ
if (tile.hasContent) {
// 1. GLBを読み込み、CESIUM_RTC (絶対座標) を抽出
const rtcCenter = extractRTC(tile.uri);
// 2. GLBから不要な属性(_BATCHID)と拡張(Draco, RTC)を削除・解凍
// (gltf-transform copy --no-compress 等を使用)
cleanGLB(tile.uri);
// 3. このタイルの「あるべき絶対位置」をRTC中心とする
myAbsolutePosition = rtcCenter;
// 4. 親からの相対位置を計算して transform に設定
// Relative = Target - Parent
const relativeOffset = subtract(myAbsolutePosition, parentAbsolutePosition);
tile.transform = matrixFromTranslation(relativeOffset);
}
// 子タイルへ再帰。このタイルの絶対位置を「親の位置」として渡す
tile.children.forEach(child =>
processTileHierarchy(child, myAbsolutePosition)
);
}
📝 まとめと推奨事項
WebGPU 時代の 3D Tiles / deck.gl 利用において、以下の点が重要になります。
-
レガシー属性のクリーンアップ: 古いコンバータで生成されたデータには、現在のグラフィックスパイプラインで予期せぬエラーを引き起こす属性が含まれていることがあります。移行時には
gltf-transform等で属性を検査・掃除することを推奨します。 -
座標系の理解:
tileset.jsonのtransformは階層的に作用します。RTC (Model-Space) から Tileset (World-Space) への移行を行う際は、「絶対座標」の連鎖になっていないか、数学的な整合性を常に確認する必要があります。