体素格式
本页为体素格式 v1.1。输出以 .voxel.json 结尾时由 splat-transform 写出,Viewer 等运行时用作碰撞和射线。
用 Laine–Karras 节点布局的稀疏八叉树保存实心/空体素:JSON 描述网格,紧凑二进制保存树。
生成步骤见碰撞指南。
文件集合
同前缀文件对,以及可选网格:
scene.voxel.json # 头部:边界、分辨率、树统计(本规范)
scene.voxel.bin # 二进制八叉树数据(nodes 与 leafData)
scene.collision.glb # 从体素提取的可选三角网格
- 头名 *.voxel.json,将后缀替为 .voxel.bin 推导二进制名,头不显式引用。
- 可选 .collision.glb 是标准 glTF,此处不展开。
*.voxel.json
interface VoxelMeta {
version: string; // file format version, decimal string, e.g. "1.1"
asset?: {
generator?: string; // tool/version that produced the file, e.g. "splat-transform v3.1.6"
};
gridBounds: { // AABB of the voxel grid, aligned to 4-voxel block boundaries
min: number[]; // [x, y, z] — world position of the min corner of voxel (0, 0, 0)
max: number[]; // [x, y, z]
};
sceneBounds: { // AABB of the source Gaussians (informational)
min: number[];
max: number[];
};
voxelResolution: number; // edge length of one voxel, world units
leafSize: number; // voxels per leaf-block edge; always 4
treeDepth: number; // subdivision levels from the root cube down to leaf blocks (>= 1)
numInteriorNodes: number; // number of interior nodes in `nodes`
numMixedLeaves: number; // number of mixed leaves in `nodes`
nodeCount: number; // total uint32 entries in the `nodes` array
leafDataCount: number; // total uint32 entries in the `leafData` array (= 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 记录源场景,各位置按旋转缩放范围及 sigma 截断扩展,仅供参考;依填充、雕刻、裁剪,gridBounds 可更大或更小。
- nodeCount 为 numInteriorNodes、numMixedLeaves 与未独立保存的实心叶数量之和。
坐标空间
全部边界使用 PlayCanvas 渲染坐标系,相比源 PLY 差一个标准导入变换,即绕 Z 180 度。
*.voxel.bin
两个小端 uint32 数组串接,无头无填充:
| 字节偏移 | 字节长度 | 内容 |
|---|---|---|
0 | nodeCount * 4 | nodes |
nodeCount * 4 | leafDataCount * 4 | leafData |
总大小恰为 (nodeCount + leafDataCount) * 4。无实心时两计数零,二进制空。
八叉树
每轴 2^treeDepth 个块,每块 leafSize³,即 4×4×4,从 gridBounds.min 开始。轴块数不是相同 2 次幂时,根立方体超出 max,多余区域始终空,父掩码中无对应子项。
treeDepth 深度为 4×4×4 叶块,nodes[0] 为根。
八分区编号
按轴半区编号,X 最低位:
oct = x | (y << 1) | (z << 2) // x、y、z 在下半区为 0,在上半区为 1
与 Morton 序一致,从根到块 bx/by/bz 按高位到低位消费坐标位。深度 d(根 0)时,oct = ((bx >> s) & 1) | (((by >> s) & 1) << 1) | (((bz >> s) & 1) << 2),s = treeDepth − 1 − d。
节点字
每条 uint32 按顺序测试:
| 条件 | 类型 | 含义 |
|---|---|---|
word == 0xFF000000 | 实心叶 | 全体积实心,无子或叶数据,任意深度,表示折叠子树。 |
(word >>> 24) == 0 | 混合叶 | word 是叶对索引 i,低高掩码为 leafData[2i]、leafData[2i+1],只在 treeDepth。 |
| 其他 | 内部节点 | childMask = word >>> 24,每八分区一位;firstChild = word & 0xFFFFFF,索引 nodes。 |
编码无歧义:
- 内部节点至少一子,顶部字节不为零,零唯一识别混合叶。
- 广度优先且子始终在父后,firstChild 不为零,0xFF000000 唯一识别实心叶。
子节点
childMask 已置位子节点从 nodes[firstChild] 连续保存,按八分区升序。oct 位存在时位置为:
nodes[firstChild + popcount(childMask & ((1 << oct) - 1))]
未置位表示该八分区全空,不保存节点。
混合叶掩码
64 个体素以两个 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 位,下一项高 32–63 位,置位为实心。混合掩码不会全零或全一,分别编码为缺失分区或实心叶。
查询域
gridBounds 外不属于格式定义域,约定依用途。导航碰撞运行时如 Viewer 将其视为实心。导航输出依赖此行为,写入器正因如此裁掉可导航区之外全实体块。
限制
firstChild 与混合叶索引为 24 位,上限各 16,777,216 节点/混合叶,写入器超出时报错。
JSON 示例
5 厘米分辨率、块 0.2、32×14×32 块:
{
"version": "1.1",
"asset": {
"generator": "splat-transform v3.1.6"
},
"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 为十进制 major.minor 字符串,本规范 1.1,应拒绝更高主版本。
- 1.0 → 1.1:旧版源 PLY 坐标,1.1 改引擎坐标,二进制不变,见坐标。
- asset 在 1.1 发布后加入,早期文件可能缺失。
- 忽略未知字段,允许兼容增量。