ボクセルフォーマット
このドキュメントは、ボクセルフォーマットのバージョン1.1の仕様です。出力パスが .voxel.json で終わる場合にsplat-transformが書き出し、SuperSplat ビューアなどのランタイムがコリジョン検出やレイキャストのために使用する、ディスク上の構造を記述します。
このフォーマットは、Gaussian splatシーンのソリッド/空のボクセル化を、Laine–Karrasノードレイアウトのスパースボクセルオクツリー(SVO)として格納します。グリッドを記述するJSONヘッダと、ツリーを保持するコンパクトなバイナリファイルで構成されます。
このフォーマットの生成手順(ボクセル化、外部フィル、ナビゲーション領域のカーブ、コリジョンメッシュ)については、コリジョンメッシュガイドを参照してください。
ファイルセット
ボクセルデータセットは、共通の語幹を持つファイルのペアと、オプションのコリジョンメッシュで構成されます:
scene.voxel.json # ヘッダ: 境界、解像度、ツリー統計 (本仕様)
scene.voxel.bin # バイナリオクツリーデータ (nodes + leafData)
scene.collision.glb # オプション。ボクセルから抽出された三角形メッシュ
- ヘッダの名前は常に
*.voxel.jsonです。ライターとリーダーは、.voxel.jsonサフィックスを.voxel.binに置き換えてバイナリファイル名を導出します。ヘッダはバイナリファイルを明示的に参照しません。 .collision.glb(コリジョンメッシュ生成が要求された場合のみ書き出されます)は標準のglTFバイナリであり、本仕様ではこれ以上説明しません。
*.voxel.json
interface VoxelMeta {
version: string; // ファイルフォーマットのバージョン。10進文字列、例: "1.1"
asset?: {
generator?: string; // ファイルを生成したツール/バージョン。例: "splat-transform v2.5.2"
};
gridBounds: { // ボクセルグリッドのAABB。4ボクセルブロック境界にアラインされる
min: number[]; // [x, y, z] — ボクセル (0, 0, 0) の最小コーナーのワールド座標
max: number[]; // [x, y, z]
};
sceneBounds: { // ソースGaussianのAABB (参考情報)
min: number[];
max: number[];
};
voxelResolution: number; // ボクセル1個の辺の長さ (ワールド単位)
leafSize: number; // リーフブロックの1辺あたりのボクセル数。常に4
treeDepth: number; // ルートキューブからリーフブロックまでの分割レベル数 (>= 1)
numInteriorNodes: number; // `nodes` 内の内部ノードの数
numMixedLeaves: number; // `nodes` 内の混合リーフの数
nodeCount: number; // `nodes` 配列のuint32エントリの総数
leafDataCount: number; // `leafData` 配列のuint32エントリの総数 (= 2 * numMixedLeaves)
}
gridBoundsはボクセルデータの基準となるフレームです。ボクセル(vx, vy, vz)は、gridBounds.min + (vx, vy, vz) · voxelResolutionからgridBounds.min + (vx+1, vy+1, vz+1) · voxelResolutionまでのワールド空間のボックスを占めます。各軸は4×4×4ボクセルブロックの整数個分の範囲です:nb = round((max − min) / (4 · voxelResolution))。sceneBoundsは、ソースGaussianシーンの境界(各Gaussianの位置を、レンダラーのシグマカットオフにおける回転・スケールされた広がりで拡張したもの)を記録します。これは参考情報です。フィル/カーブ/クロップのオプションによっては、gridBoundsがsceneBoundsより大きい場合も小さい場合もあります。nodeCount=numInteriorNodes+numMixedLeaves+ ソリッドリーフの数(個別には格納されません)。
座標空間
すべての境界は、PlayCanvasエンジンの座標系 — スプラットシーンがレンダリングされるフレーム — で表現されます。これは、エンジンの標準的なスプラットインポート変換(Z軸まわりの180°回転)の分だけ、ソースPLYの座標系と異なります。
*.voxel.bin
バイナリファイルは、ヘッダもパディングもなく連結された、2つのリトルエンディアン uint32 配列です:
| オフセット (バイト) | 長さ (バイト) | 内容 |
|---|---|---|
0 | nodeCount * 4 | nodes 配列 |
nodeCount * 4 | leafDataCount * 4 | leafData 配列 |
ファイルの合計サイズは正確に (nodeCount + leafDataCount) * 4 バイトです。ソリッドボクセルのないシーンは nodeCount = 0、leafDataCount = 0 で、バイナリファイルは空になります。
オクツリー
ツリーは、gridBounds.min を起点とし、1軸あたり 2^treeDepth ブロック(各ブロックは leafSize³ = 4×4×4ボクセル)のキューブを分割します。軸ごとのブロック数が同じ2のべき乗でない場合、ルートキューブは gridBounds.max を超えて広がります。その超過領域は常に空です(そのようなオクタントは親のチャイルドマスクから単に欠落します)。
深さ treeDepth のノードは、4×4×4ボクセルをカバーするリーフブロックです。nodes[0] がルートです。
オクタント番号
内部ノードの8つのオクタントは、Xを最下位として軸の半分ごとに番号付けされます:
oct = x | (y << 1) | (z << 2) // x, y, z = 下半分なら0、上半分なら1
これはモートン順と一致します。ルートからブロック座標 (bx, by, bz) のリーフブロックまでのパスは、座標のビットを最上位から最下位へ消費します — 深さ d(ルート = 0)において、oct = ((bx >> s) & 1) | (((by >> s) & 1) << 1) | (((bz >> s) & 1) << 2)、ここで s = treeDepth − 1 − d です。
ノードワード
nodes の各エントリは1つの uint32 で、以下のようにデコードされます(順番に判定します):
| 条件 | ノード種別 | 意味 |
|---|---|---|
word == 0xFF000000 | ソリッドリーフ | ノードの全体積がソリッドです。子もリーフデータもありません。任意の深さに現れる可能性があります(完全にソリッドなサブツリーの折りたたみ)。 |
(word >>> 24) == 0 | 混合リーフ | word は leafData ペアへのインデックス i です。ノードのボクセルマスクは leafData[2i](lo)と leafData[2i + 1](hi)です。深さ treeDepth にのみ現れます。 |
| それ以外 | 内部ノード | childMask = word >>> 24(8ビット、オクタントごとに1ビット)、firstChild = word & 0xFFFFFF(nodes へのインデックス)。 |
2つのリーフエンコーディングは曖昧さなく区別できます:
- 内部ノードは常に少なくとも1つの子を持つため、その
childMaskが0になることはありません — 上位バイトがゼロであることは混合リーフを一意に識別します。 - ノードは、子が常に親の後に続く幅優先順で出力されるため、内部ノードの
firstChildが0になることはありません —0xFF000000はソリッドリーフを一意に識別します。
子ノード
内部ノードの存在する子(childMask のセットされたビット)は、nodes[firstChild] から始まる連続領域に、オクタントインデックスの昇順で格納されます。オクタント oct の子(childMask & (1 << oct) がセットされている場合)は次の位置にあります:
nodes[firstChild + popcount(childMask & ((1 << oct) - 1))]
childMask のクリアされたビットは、そのオクタントの全体積が空であることを意味します。そのオクタントのノードは格納されません。
混合リーフのボクセルマスク
混合リーフの4×4×4 = 64ボクセルは、2つの uint32 に分割された64ビットの占有マスクとして格納されます。ローカル座標 (lx, ly, lz)(それぞれ [0, 4))のボクセルについて:
bit = lx + (ly << 2) + (lz << 4)
solid = bit < 32 ? (lo >>> bit) & 1 : (hi >>> (bit - 32)) & 1
leafData[2i] がビット0–31(lo)を、leafData[2i + 1] がビット32–63(hi)を保持します。セットされたビットはボクセルがソリッドであることを意味します。混合リーフのマスクがすべてゼロまたはすべて1になることはありません — そのようなブロックは、欠落オクタントまたはソリッドリーフとしてエンコードされます。
クエリ範囲
gridBounds の外側へのクエリはフォーマットの対象範囲外であり、適切な規約はユースケースによって異なります。ナビゲーション/コリジョンのコンシューマ(例:SuperSplat ビューア)は、グリッド外の空間をソリッドとして扱います。ナビゲーションオプション(外部フィル、フロアフィル、カーブ)で生成された出力はこれに依存しています — ランタイムがグリッド外をすべてソリッドとして扱うからこそ、ライターはナビゲーション可能領域の外側にある完全にソリッドなブロックをクロップして除去します。
制限
firstChild と混合リーフの leafData インデックスは24ビット値であり、フォーマットの上限はノードエントリ16,777,216個、混合リーフ16,777,216個です。ライターはいずれかの制限を超える代わりにエラーで失敗します。
*.voxel.json の例
5cm解像度(ブロックサイズ0.2)の32 × 14 × 32ブロックのグリッド:
{
"version": "1.1",
"asset": {
"generator": "splat-transform v2.5.2"
},
"gridBounds": {
"min": [-3.2, -0.2, -3.2],
"max": [3.2, 2.6, 3.2]
},
"sceneBounds": {
"min": [-3.13, -0.08, -3.07],
"max": [3.11, 2.49, 3.08]
},
"voxelResolution": 0.05,
"leafSize": 4,
"treeDepth": 5,
"numInteriorNodes": 1201,
"numMixedLeaves": 5678,
"nodeCount": 9232,
"leafDataCount": 11356
}
バージョン管理と互換性
versionは10進の"major.minor"文字列です。この仕様に準拠するファイルはversion: "1.1"を持ちます。リーダーはより大きなメジャーバージョンのファイルを拒否すべきです。- 1.0 → 1.1:バージョン1.0のファイルは、境界とボクセルデータをソースPLYの座標系で格納します。1.1はPlayCanvasエンジンのフレームでボクセル化します(座標空間を参照)。バイナリレイアウトは変わりません。
assetブロックは1.1の出荷後に追加されたため、古い1.1ファイルには存在しない場合があります。- 未知のフィールドは無視されるべきです。これにより、バージョンを上げずに小規模な追加的変更が可能になります。
関連項目
- コリジョンメッシュ — splat-transformによるボクセルおよびコリジョンデータの生成。
- splat-transform CLIリファレンス — ボクセル出力オプションを含む全オプションのリファレンス。