Skip to content

Batches API

u-spacesrc/batches 模块提供两种共享渲染聚合层。ModelInstancedLayer 面向重复模板的 InstancedMesh 渲染;EditableGeometryBatchLayer 面向大量普通、主要静态但仍需按对象控制的 Geometry 烘焙合并。两者都从顶层 u-space 导出,并使用 InstanceObject 作为逻辑对象协议。

typescript
import {
  EditableGeometryBatchLayer,
  InstanceObject,
  ModelInstancedLayer,
  type EditableGeometryBatchOptions,
  type EditableGeometryBatchStats,
} from 'u-space';

职责边界

模块职责
src/instancesInstanceObject 的身份、transform、样式、bounds、dirty 和 materialize 协议。
src/batchesInstanceObject 对应的批量渲染、GPU 状态同步、raycast remap 和资源生命周期。
插件/业务 Loader模型加载、URL/业务 ID、unsupported fallback 和场景树装配策略。

ModelInstancedLayer

ModelInstancedLayer<T extends InstanceObject> 继承自 BaseGroup。它按模板 key 创建 InstancedMesh batch,保留模板中的独立 Mesh 以维持多材质、透明排序和局部包围体,并在实例 dirty 时同步 matrix、颜色、透明度与可见实例集合。

常用方法:

方法说明
reserveBatch(key, template, capacity)为模板预分配实例容量。
addInstance(key, template, instance) / addInstances(...)添加一个或多个逻辑实例。
getInstances() / getInstanceById(id)枚举实例或按 instanceId 查询。
removeInstance() / removeInstances()按对象或 ID 删除实例。
removeBatch(key) / clearBatches()删除一个 batch 或清空全部 batch。
setInstanceCulling(options)配置可选的逐实例视锥和屏幕尺寸裁剪。

复杂静态模板在射线通过实例包围体后会按需建立 three-mesh-bvh;命中会映射回对应 InstanceObject。修改实例 transform 时只需操作实例对象;根 Scene 冻结自动矩阵更新时,再调用 instance.updateWorldMatrix(true, false)viewer.invalidate()

EditableGeometryBatchLayer

EditableGeometryBatchLayer<T extends InstanceObject> 继承自 BaseGroup。它把兼容 Mesh 的 transform 烘焙进克隆 Geometry,按材质、Geometry attribute/index 布局、阴影和 renderOrder 分组,再通过 mergeGeometries() 合并为少量内部 BaseMesh。内部 Mesh 是实现细节,不从公共 API 导出。

配置与统计

EditableGeometryBatchOptions 字段默认值说明
maxVerticesPerBatch1_500_000单个 merged Geometry 的最大顶点数。
maxIndicesPerBatch4_500_000单个 merged Geometry 的最大索引数。
freezeAnimationsfalse是否允许烘焙带 animation clip/mixer 模板的当前姿态;SkinnedMesh 和 morph target 仍 fallback。

stats / build().stats 包含 instancessourceMeshesbatchesdrawCallsSavedverticesindicesunsupportedInstancesunsupportedByReason

创建和构建

typescript
import {
  EditableGeometryBatchLayer,
  InstanceObject,
  Model,
} from 'u-space';

const layer = new EditableGeometryBatchLayer<InstanceObject>({
  maxVerticesPerBatch: 1_500_000,
  maxIndicesPerBatch: 4_500_000,
});

const template = new Model();
const instances = [new InstanceObject(), new InstanceObject()];

layer.addSource({
  key: '/models/building.glb',
  template,
  instances,
});

scene.add(layer, ...instances);
scene.updateMatrixWorld(true);

layer.addEventListener('materialize', ({ instance, object }) => {
  console.log('materialized', instance.instanceId, object);
});

const result = layer.build();
console.table(result.stats);

addSource() 接收 keyModel 模板和同一模板对应的实例数组。全部 source 添加完成后调用一次 build();layer 是 one-shot builder,重复 build()、build 后继续 addSource() 或 dispose 后复用都会抛出错误。同一个 InstanceObject 在一个 layer 中只能出现一次,重复 source 会直接抛错,避免生成无法独立隐藏或拾取的重复烘焙几何。build() 返回统计信息,以及没有完全进入 merged Geometry 的 unsupported 数组。每个 unsupported 项包含 key、fallback templateinstancesreasons,由调用方决定继续 instancing 还是挂载普通 Model

透明材质、SkinnedMesh、morph target、多材质数组、自定义 NodeMaterial / onBeforeCompile、不兼容 Geometry 布局、负行列式变换和未冻结动画会进入 unsupported 路径。负行列式会按实例拆分,正 determinant 的同模板实例仍可合并;negative-scale 实例必须使用普通 Model fallback,不能继续交给不支持负缩放的 InstancedMesh。混合模板仍会合并兼容的不透明子集,并返回只保留不支持子 Mesh 的 fallback 模板。

对象状态与 materialize

每个逻辑对象在 Float DataTexture 中占一个 RGBA texel。内部 NodeMaterial 通过 TSL 和顶点 batchObjectIndex attribute 读取显隐、颜色模式和颜色,因此普通显隐、颜色和不引入半透明的高亮只更新 dirty texel,不重新合并 Geometry。兼容 batch 只包含不透明源材质,并保留源材质原有的 opacity 路径;对象显隐使用独立布尔 maskNode,避免动态 opacity 同时参与 alpha 输出和 discard 判断而改变楼壳外观。dirty callback 会立即在 CPU 侧同步对应状态,GPU texture 在下一帧上传;transform materialize 不依赖内部 batch 先通过视锥裁剪,因此从屏幕外移动到屏幕内也不会丢失。

实例 transform 偏离烘焙矩阵或有效 opacity 低于 0.999 时,layer 会 clone 完整模板、为本次 materialize 创建独立材质并调用 InstanceObject.setInstanceRenderObject(),同时 mask merged Geometry 中的旧副本。新挂载对象会立即提交世界矩阵,冻结根 Scene 时也不会错过当前帧;若另一个 layer materialize 了同一逻辑实例,旧 layer 会通过 dirty callback 隐藏自己的烘焙副本。不同 materialized 实例可以安全使用不同 opacity/highlight,不会反向修改模板或其他实例。业务也可以主动调用 instance.materialize()。materialize 成功后 layer 会派发类型化的 materialize 事件;如果需要观察 build 期间由初始 opacity 触发的 materialize,应像上例一样在 build() 前注册监听:

typescript
const model = instances[0].materialize();

setInstanceMaterializer() / clearInstanceMaterializer(materializer?) 是 batching 实现连接 InstanceObject 的低层协议,普通业务不需要直接设置。带 delegate 参数的 clear 只会解除同一个 materializer,避免旧 layer dispose 时清除后来接管实例的新 layer。materialize 是单向操作;恢复原 transform 或 opacity 不会自动重新进入 batch。

Raycast 与释放

内部 merged Mesh 首次拾取时按需建立 three-mesh-bvh,再通过 faceIndex → vertexIndex → batchObjectIndex 将命中映射回原始 InstanceObject。隐藏 layer 或内部 batch Mesh 会遵循 ignoreInvisibleWhenRaycast;已经 materialize 的 state 会从 merged BVH 命中过滤,旧烘焙位置不会残留 ghost picking。

不再使用 layer 时必须调用 dispose()。它会取消 dirty 订阅、仅解除仍由当前 layer 持有的 instance materializer、释放状态纹理、Geometry 和克隆材质,并清空内部节点。materialized 普通对象及其独立材质已经交给对应 InstanceObject,不由 layer dispose。

THREE.BatchedMesh 的区别

EditableGeometryBatchLayer 不是 THREE.BatchedMesh。它选择把主要静态 Geometry 真正烘焙合并,并只同步 dirty 对象状态,以减少 draw submission 和逐对象 render-list 工作;代价是展开重复 Geometry、增加 GPU 顶点内存,并在 transform 或半透明变化时 materialize 当前对象。高重复、较大且经常变换的同模板对象通常更适合 ModelInstancedLayer

u-space docs