Appearance
Batches API
u-space 的 src/batches 模块提供两种共享渲染聚合层。ModelInstancedLayer 面向重复模板的 InstancedMesh 渲染;EditableGeometryBatchLayer 面向大量普通、主要静态但仍需按对象控制的 Geometry 烘焙合并。两者都从顶层 u-space 导出,并使用 InstanceObject 作为逻辑对象协议。
typescript
import {
EditableGeometryBatchLayer,
InstanceObject,
ModelInstancedLayer,
type EditableGeometryBatchOptions,
type EditableGeometryBatchStats,
} from 'u-space';职责边界
| 模块 | 职责 |
|---|---|
src/instances | InstanceObject 的身份、transform、样式、bounds、dirty 和 materialize 协议。 |
src/batches | InstanceObject 对应的批量渲染、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 字段 | 默认值 | 说明 |
|---|---|---|
maxVerticesPerBatch | 1_500_000 | 单个 merged Geometry 的最大顶点数。 |
maxIndicesPerBatch | 4_500_000 | 单个 merged Geometry 的最大索引数。 |
freezeAnimations | false | 是否允许烘焙带 animation clip/mixer 模板的当前姿态;SkinnedMesh 和 morph target 仍 fallback。 |
stats / build().stats 包含 instances、sourceMeshes、batches、drawCallsSaved、vertices、indices、unsupportedInstances 和 unsupportedByReason。
创建和构建
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() 接收 key、Model 模板和同一模板对应的实例数组。全部 source 添加完成后调用一次 build();layer 是 one-shot builder,重复 build()、build 后继续 addSource() 或 dispose 后复用都会抛出错误。同一个 InstanceObject 在一个 layer 中只能出现一次,重复 source 会直接抛错,避免生成无法独立隐藏或拾取的重复烘焙几何。build() 返回统计信息,以及没有完全进入 merged Geometry 的 unsupported 数组。每个 unsupported 项包含 key、fallback template、instances 和 reasons,由调用方决定继续 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。