物理碰撞
从 3DGS 场景生成体素或网格碰撞体,并做射线、胶囊体等实时碰撞查询。
背景
3DGS 重建场景是高斯点云,没有实体边界,无法直接用于行走与碰撞。物理碰撞模块把重建空间转换为可查询的碰撞数据,用体素或网格描述地面、墙体、遮挡物和可通行区域,为行走模式、相机避障、区域限制和空间交互提供约束。
体素碰撞体由 splat-transform 的 Voxel 任务从 3DGS 资产生成。下文说明体素文件格式与运行时查询方式。也可额外提供 collision.glb 网格碰撞体。
概述
体素数据将场景占用编码为 稀疏体素八叉树(SVO),用于运行时碰撞与射线检测,例如:
- Raycast:拾取、贴地、视线检测
- 球体 / 胶囊体:角色穿透修正(depenetration)
编码采用 Laine–Karras 紧凑布局,与 playcanvas/splat-transform、playcanvas/supersplat-viewer 约定一致。
稀疏八叉树结构
八叉树在均匀体素网格上划分空间,只存储非空区域,以压缩大场景数据。
层级
- 根到叶共
treeDepth层;最细一层体素边长为voxelResolution。 - 每个叶节点覆盖 4×4×4 体素(
leafSize = 4),共 64 个占用位。
节点类型(nodes 数组中每个 uint32)
| 类型 | 含义 |
|---|---|
| 内部节点 | 高 8 位为 childMask(8 个子八分体是否存在),低 24 位为第一个子节点在数组中的索引;其余子节点下标由 popcount 推算。 |
| 全实心叶 | 值为 0xFF000000(SOLID_LEAF_MARKER),表示该 4×4×4 块全部占用。 |
| 混合叶 | childMask == 0,低 24 位指向 leafData 中的 64 位掩码,按位标记块内实心体素。 |
leafData
- 每个混合叶占 2 个
uint32(lo、hi),共 64 bit。 - 块内体素
(vx, vy, vz)的位索引:vx + vy * 4 + vz * 16(各分量 ∈ [0, 3])。
遍历
nodes 按广度优先紧凑存储,只保留 childMask 标记存在的子节点。查询时从根沿一条路径下潜 treeDepth 层,无需遍历整棵树。
查询单个体素是否占用:世界坐标 → 体素 (ix, iy, iz) → 叶块 (⌊ix/4⌋, …)。从 nodes[0] 向下:
- 全实心叶:判定为占用。
- 混合叶:用
(ix&3, iy&3, iz&3)查leafData对应位。 - 内部节点:按当前层从叶块坐标取八分体编号;若无子节点则为空,否则下一节点下标为
baseOffset加 popcount。
射线遍历:在网格包围盒内用 3D DDA 逐格前进,每格重复上述占用查询。
输出文件
voxel-meta.json:网格边界、体素尺寸、treeDepth、nodeCount、leafDataCount等。voxel.bin:连续二进制,nodes在前、leafData在后(均为uint32数组)。- 可选
collision.glb网格碰撞体。
Raycast 与碰撞查询
splat-transform 只负责生成体素数据;射线检测、穿透修正等由运行时加载八叉树后实现。常见流程如下。
射线检测(Raycast)
- 射线与网格包围盒求交,限定搜索范围;
- 用 3D DDA 沿射线逐格前进:比较与各轴体素边界的参数
t,进入t最小的一格; - 每格沿八叉树下潜,查询是否占用;
- 遇到第一个实心体素即为交点;走出包围盒仍未命中则视为无交。
射线方向不必归一化。常用于贴地、拾取、短距离障碍检测。
位置占用
给定世界坐标,映射为体素索引后,按上文方式查询是否在物体内部。
球体 / 胶囊体
在包围体内的体素格中,对每个实心体素的轴对齐方块,计算与球心或胶囊轴线的距离;小于半径则存在穿透,沿最短方向累加推出位移。多面接触时可迭代若干次。
致谢
体素管线、八叉树编码及文件格式主要参考 PlayCanvas 开源项目:
| 项目 | 链接 | 说明 |
|---|---|---|
| splat-transform | https://github.com/playcanvas/splat-transform | 体素化、导航填充/雕刻、八叉树导出与碰撞网格生成 |
| supersplat-viewer | https://github.com/playcanvas/supersplat-viewer | 运行时体素碰撞(raycast、球/胶囊查询)参考实现;见 Issues |