物理碰撞

从 3DGS 场景生成体素或网格碰撞体,并做射线、胶囊体等实时碰撞查询。

背景

3DGS 重建场景是高斯点云,没有实体边界,无法直接用于行走与碰撞。物理碰撞模块把重建空间转换为可查询的碰撞数据,用体素或网格描述地面、墙体、遮挡物和可通行区域,为行走模式、相机避障、区域限制和空间交互提供约束。

体素碰撞体由 splat-transformVoxel 任务从 3DGS 资产生成。下文说明体素文件格式与运行时查询方式。也可额外提供 collision.glb 网格碰撞体。

概述

体素数据将场景占用编码为 稀疏体素八叉树(SVO),用于运行时碰撞与射线检测,例如:

  • Raycast:拾取、贴地、视线检测
  • 球体 / 胶囊体:角色穿透修正(depenetration)

编码采用 Laine–Karras 紧凑布局,与 playcanvas/splat-transformplaycanvas/supersplat-viewer 约定一致。

稀疏八叉树结构

八叉树在均匀体素网格上划分空间,只存储非空区域,以压缩大场景数据。

层级

  • 根到叶共 treeDepth 层;最细一层体素边长为 voxelResolution
  • 每个叶节点覆盖 4×4×4 体素(leafSize = 4),共 64 个占用位。

节点类型nodes 数组中每个 uint32

类型含义
内部节点高 8 位为 childMask(8 个子八分体是否存在),低 24 位为第一个子节点在数组中的索引;其余子节点下标由 popcount 推算。
全实心叶值为 0xFF000000SOLID_LEAF_MARKER),表示该 4×4×4 块全部占用。
混合叶childMask == 0,低 24 位指向 leafData 中的 64 位掩码,按位标记块内实心体素。

leafData

  • 每个混合叶占 2 个 uint32lohi),共 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:网格边界、体素尺寸、treeDepthnodeCountleafDataCount 等。
  • voxel.bin:连续二进制,nodes 在前、leafData 在后(均为 uint32 数组)。
  • 可选 collision.glb 网格碰撞体。

Raycast 与碰撞查询

splat-transform 只负责生成体素数据;射线检测、穿透修正等由运行时加载八叉树后实现。常见流程如下。

射线检测(Raycast)

  1. 射线与网格包围盒求交,限定搜索范围;
  2. 3D DDA 沿射线逐格前进:比较与各轴体素边界的参数 t,进入 t 最小的一格;
  3. 每格沿八叉树下潜,查询是否占用;
  4. 遇到第一个实心体素即为交点;走出包围盒仍未命中则视为无交。

射线方向不必归一化。常用于贴地、拾取、短距离障碍检测。

位置占用

给定世界坐标,映射为体素索引后,按上文方式查询是否在物体内部。

球体 / 胶囊体

在包围体内的体素格中,对每个实心体素的轴对齐方块,计算与球心或胶囊轴线的距离;小于半径则存在穿透,沿最短方向累加推出位移。多面接触时可迭代若干次。

致谢

体素管线、八叉树编码及文件格式主要参考 PlayCanvas 开源项目:

项目链接说明
splat-transformhttps://github.com/playcanvas/splat-transform体素化、导航填充/雕刻、八叉树导出与碰撞网格生成
supersplat-viewerhttps://github.com/playcanvas/supersplat-viewer运行时体素碰撞(raycast、球/胶囊查询)参考实现;见 Issues