Skip to content

u-manager

u-manager 是一套全面的加载器和解析器,用于从服务器路径流式传输、解密并显示结构化场景数据。支持场景、拓扑、动画、属性、相机视点、楼层、井道和设备语义模型。

所有加载器均继承自 Three.js 的 Loader,并提供 setPath(path) 方法,在调用 loadAsync() 前配置基础数据目录。

UManagerLoader

UManagerLoader 是推荐的一体化入口。它会在同一路径下同时加载 SemanticLoaderSceneLoader:语义文件中已经存在的 id 会由 SceneLoader 自动跳过,避免建筑、楼层、设备被重复创建;非语义场景树节点仍会正常加载。返回值是一个 UManagerSceneGroup,可直接通过 semanticGroup 访问 SemanticGroup,通过 sceneGroup 访问非语义场景组。

typescript
import { UManagerLoader } from 'u-space/plugins/u-manager';

const loader = new UManagerLoader(viewer);
loader.setPath('./scenes/my-scene');
loader.setKey('YOUR_LICENSE_KEY');

const group = await loader.loadAsync();
viewer.scene.add(group);

const semanticGroup = group.semanticGroup;
const sceneGroup = group.sceneGroup;
const facilityLayer = semanticGroup.getDefaultFacilityLayer();
const sceneLayer = sceneGroup.getDefaultSceneLayer();

完整可运行示例见 examples/test_umanager_loader.html。该示例展示了如何从返回根组中直接读取 semanticGroup / sceneGroup,通过 getDefaultFacilityLayer() / getDefaultSceneLayer() 获取默认合批层,并通过 viewer.objectManager 获取 SceneInstanceObject / FacilityInstanceObject 后调用统一 viewer.controls.flyToObject()setInstanceHighlight()setInstanceVisible()setInstanceOpacity()。运行时动态新增/删除 batch 实例的示例见 examples/test_umanager_dynamic_instances.html

SemanticLoader

加载 db/semantic_model.json 楼层语义数据。loadAsync() 返回顶层 SemanticGroup,用于组织整次语义解析产生的建筑、楼层以及 scene-level facility layer。每个建筑包含多个 FloorMesh,每个楼层把墙、柱、空间、楼梯等语义对象合并为一个 TSL 材质驱动的网格;建筑级电梯井和通风井会使用 ExtrudeMesh 独立渲染。语义模型包含 Facilities 时,SemanticLoader 会额外读取 SceneMetadata.json 指向的 tree_models,按场景授权配置解密,再把对应设备模型挂载到所属楼层。

typescript
import { Vector3, Vector4 } from 'three';
import { SemanticLoader } from 'u-space/plugins/u-manager';

const loader = new SemanticLoader(viewer);
loader.setPath('./scenes/my-scene');
loader.setKey('YOUR_LICENSE_KEY'); // 官方授权场景解析 tree_models 时必需

const semanticGroup = await loader.loadAsync(); // SemanticGroup
viewer.scene.add(semanticGroup);

const building = semanticGroup.getBuildingAt(0);
const floor = building.getFloorAt(0);

building.isolateFloor(floor);
await viewer.controls.flyToObject(floor);

floor.addEventListener('click', ({ event }) => {
  const { currentTarget, intersect } = event;
  if (!intersect) return;

  const index = intersect.semanticIndex;

  currentTarget.setColorAt(index, new Vector4(1, 0, 0, 0.5));
  currentTarget.setScaleAt(index, new Vector3(1, 0.1, 1));
  viewer.render();
});

所有可按 ID 操作的实例都会尽量暴露统一的 InstanceObject API。楼层多边形语义使用轻量 FloorSemanticInstanceObject 代理合并几何中的一个实例;Facilities 和 SceneLoader path instancing 使用同一套模型实例化底层。业务代码优先使用 setInstanceVisible()setInstanceColor()setInstanceOpacity()viewer.controls.flyToObject(instance),而不是关心底层是合并几何、InstancedMesh 还是 fallback Model

完整的继承字段、方法、颜色模式、包围盒和 dirty 同步语义见 核心 InstanceObject API

InstanceObject 也按 Three.js 对象级包围盒约定提供 boundingBoxboundingSpherecomputeBoundingBox()computeBoundingSphere()。它们缓存的是实例本地坐标包围体,Box3.setFromObject() 会自动调用 computeBoundingBox() 并应用 matrixWorld,因此 viewer.controls.flyToObject() 可以直接飞向 SceneInstanceObjectFacilityInstanceObject 和楼层实例代理。需要手动处理世界包围盒时,仍可使用 getInstanceBoundingBox(target, true)

SemanticGroup

SemanticGroup 继承自 BaseGroup,是单次 SemanticParser 解析结果的顶层容器。它管理所有 BuildingGroup,并把跨楼层共享的 Facilities InstancedMesh 渲染层作为建筑的同级对象挂在 facilityLayer,而不是挂到某个 BuildingGroup 内。

建筑 ID 和设备 ID 查询使用内部 Map 索引维护;getBuildingById()getFacilityById() 不会通过数组扫描查找语义对象。

属性 / 方法说明
buildings当前语义场景内的 BuildingGroup[]
facilityLayerscene-level 设备渲染层;同模型 URL 的静态设备会合并为 InstancedMesh batch。
addBuilding(group) / removeBuilding(group)添加或移除建筑,并同步更新 group 子节点。
getBuildingAt(index) / getBuildingById(id) / getBuildings()按下标、Building.ID 或整体列表获取建筑。
floorMeshes聚合返回所有建筑内的 FloorMesh[]
setFacilityLayer(layer)设置跨楼层设备渲染层;传入 null 会移除已有 layer。
getDefaultFacilityLayer()返回当前默认 FacilityInstancedLayer;没有可实例化 Facilities 时返回 null
getFacilityById(id)在整次语义解析结果中按 Facility.ID 或显式别名查找设备。
showAllFacilities() / hideAllFacilities()显示或隐藏整次语义解析结果中的全部设备。
showAllFloors()显示所有建筑的所有楼层。
planishFloors(kind?) / unplanishFloors(kind?)压扁或恢复所有楼层;默认覆盖所有合并语义实例和 Facilities,并按包围盒贴到楼层平面附近,同时添加稳定的轻微 Y 偏移以减少共面闪烁;传入 kind 时只作用于该语义类型,kind 可包含 Facilities

BuildingGroup

BuildingGroup 继承自 BaseGroup,用于组织一个建筑下的所有楼层、电梯井和通风井。设备语义对象不会直接挂到 BuildingGroup;能 instancing 的设备会渲染到 SemanticGroup.facilityLayer,同时在对应 FloorMesh 下保留可检索、可控制的轻量引用。

建筑内设备 ID 到楼层的关系由内部 Map 索引维护,并通过楼层设备索引版本自动同步。

属性 / 方法说明
floorMeshes当前建筑内的 FloorMesh[]
elevatorMeshes当前建筑内由 Elevators 语义生成的 ExtrudeMesh[]
ventMeshes当前建筑内由 Vents 语义生成的 ExtrudeMesh[]
addFloor(floor)添加楼层,并同步加入 group。
removeFloor(floor)移除楼层,并同步从 group 移除。
getFloorAt(index)按楼层下标获取 FloorMesh
getFloorById(id)按楼层语义 ID 获取 FloorMesh,内部使用 Map 索引。
addElevator(elevator) / removeElevator(elevator)添加或移除电梯井,并同步更新 group。
getElevatorAt(index)按下标获取电梯井 ExtrudeMesh
getElevatorById(id)按电梯井语义 ID 获取电梯井,内部使用 Map 索引。
addVent(vent) / removeVent(vent)添加或移除通风井,并同步更新 group。
getVentAt(index)按下标获取通风井 ExtrudeMesh
getVentById(id)按通风井语义 ID 获取通风井,内部使用 Map 索引。
isolateFloor(floor | index)只显示指定楼层,隐藏其他楼层;也兼容传入楼层数组。
isolateFloors(floors | indexes)只显示指定多个楼层,隐藏其他楼层。
showAllFloors()显示全部楼层。
getFacilityById(id)在当前建筑的楼层中按 Facility.IDFloorMesh.addFacility() 显式传入的别名查找设备。
showAllFacilities()显示整栋建筑内全部楼层设备。
hideAllFacilities()隐藏整栋建筑内全部楼层设备。
planishFloors(kind?)压扁所有楼层;默认覆盖所有合并语义实例和 Facilities,并按包围盒贴到楼层平面附近,同时添加稳定的轻微 Y 偏移以减少共面闪烁;传入 kind 时只压扁该语义类型,kind 可包含 Facilities
unplanishFloors(kind?)恢复所有楼层;默认恢复所有合并语义实例和 Facilities;传入 kind 时只恢复该语义类型,kind 可包含 Facilities

电梯井和通风井

SemanticLoader 会读取 semantic_model.json 中建筑引用的 ElevatorsVents。每个井道使用 Polygon.shape 作为底面轮廓,Elevation 作为底面高度,TotalHeight 作为拉伸高度,并以 building 级 ExtrudeMesh 加入 BuildingGroup

typescript
const elevator = building.getElevatorAt(0);
const elevatorById = building.getElevatorById('ELEVATOR_001');
const vent = building.getVentAt(0);
const ventById = building.getVentById('VENT_001');

if (elevator) {
  await viewer.controls.flyToObject(elevator);
}

if (vent) {
  building.removeVent(vent);
}

电梯井和通风井会在渲染阶段沿法线轻微内收,减少与墙体共面时的闪面。Spaces 实例也会使用稳定的 per-instance 渲染偏移抖动,缓解完全重叠空间之间的 z-fighting;这些偏移只影响渲染顶点,不改变语义 ID、实例矩阵和包围体查询的使用方式。

设施设备

SemanticLoader 会从 Stories[].Facilities 读取设备索引,再从顶层 Facilities 数组取得 Facility 数据,并使用 Facility.ID 在解密后的 tree_models 中查找同 ID 节点。只有带 renderType: '3D' 和模型 path 的节点会被加载。

tree_models.matrix 是相对父节点的本地矩阵;SemanticLoader 会沿树父级累乘得到设备世界矩阵,再转换为对应 FloorMesh 的局部矩阵。因此楼层移动、隔离或显示隐藏时设备会跟随楼层一起工作。即使某个 story 只有 Facilities、没有墙柱空间等合并几何,也会创建一个空的 FloorMesh 作为设备容器。

Facilities 会在单次 SemanticParser 解析范围内按模型 URL 自动分组。静态模型会被转换为 scene-level InstancedMesh batch 并挂到 SemanticGroup.facilityLayer;每个设备仍会在所属 FloorMesh 下保留一个 FacilityInstanceObject 实例引用,用于 getFacilityById()、显隐、颜色、透明度、包围盒和 viewer.objectManager 检索。设备 batch 使用普通 Group 包裹,以保证每帧渲染前都能执行相机实例裁剪和 hide/show 后的实例同步;带动画、骨骼、morph target 或缺少合法 geometry.groups 的多材质 Mesh 会自动回退为普通 Model,但普通 Model 也会挂到同一个 FacilityInstanceObject wrapper 下,对外 API 不变。

facilityLayer 使用 FacilityInstancedLayer,并继承自 BaseGroup。当 facilityLayer.visible = false 时,layer 会阻止内部 InstancedMesh 继续参与射线检测,避免隐藏的批处理设备仍被点击命中;单个设备的显隐应先通过 getFacilityById()viewer.objectManager.getById() 取回实例,再调用 setInstanceVisible() 控制。

Instanced 设备通过 per-instance opacity attribute 控制单个设备透明度。batch 材质默认沿用源材质的透明状态;只有源材质本身透明,或当前参与渲染的设备存在 opacity < 1 时,材质才会切到 transparent: truesetInstanceOpacity(1) 可在没有其他半透明实例时回到不透明渲染队列。resetInstanceColor() 只清除颜色 override,不影响透明度 override。FacilityInstancedLayer 会在设备显隐、颜色、透明度、floor planish/unplanish 改变设备 transform,或相机矩阵、投影矩阵、viewport 高度变化时,在下一次渲染前同步实例矩阵、颜色、透明度和当前 active instance count。普通 fallback Model 的颜色和透明度控制由 wrapper 委托到 MaterialEffects,单材质和多材质 Mesh 都会应用 override。

加载成功后,BuildingGroupFloorMesh 仍会保留自身的 semanticIdsemanticKindsemanticName;设备引用则使用通用 InstanceObject 身份字段 instanceIdinstanceKindinstanceNameSemanticGroup.getBuildingById()BuildingGroup.getFloorById()BuildingGroup.getFacilityById()FloorMesh.getFacilityById() 以及 ModelInstancedLayer 的查询、删除和 raycast remap 都使用这些对象字段或显式传入的别名,不再从 userData.iduserData.semanticIduserData.facilityId 里扫描 ID。userData 只保留原始业务元数据,例如 facilityIdspacestwinsIdentifierstoryIdfloorIndexinstancedmodelUrl

typescript
const floor = building.getFloorAt(0);

const facility = floor.getFacilityById('FACILITY_001') ?? viewer.objectManager.getById('FACILITY_001');
const facilityEntity = floor.getSemanticById('FACILITY_001');

if (facility && facilityEntity?.type === 'object') {
  await viewer.controls.flyToObject(facility, {
    viewpoint: 'rightFrontTop',
    padding: 0.2,
  });
}

const facilities = floor.getSemanticsByKind('Facilities');

facility
  .setInstanceColor('#ff5533')
  .setInstanceOpacity(0.45)
  .resetInstanceColor()
  .setInstanceVisible(true);

对 Facilities 推荐直接使用 controls.flyToObject() 飞向设备。无论设备实际渲染来自 InstancedMesh 还是 fallback ModelFacilityInstanceObject 都会暴露本地 boundingBox / boundingSphere,让 Three.js 的 Box3.setFromObject() 能计算完整世界包围盒。

FacilityInstanceObject

FacilityInstanceObject 继承自统一的 InstanceObject,表示一个设备实例引用。它会挂在所属 FloorMesh 下,用于 ID 检索、事件派发、显隐、颜色、透明度和包围盒查询;如果设备可以 instancing,真正几何由 SemanticGroup.facilityLayer 里的 InstancedMesh 统一渲染;如果不能 instancing,fallback Model 会作为它的子对象挂载。

完整的继承字段、方法、颜色模式、包围盒和 dirty 同步语义见 核心 InstanceObject API

通常不需要手动创建 FacilityInstanceObjectSemanticParser 在解析 Facilities 时会自动创建它,并通过 setInstanceIdentity() 写入 instanceIdinstanceKindinstanceNameuserData.instanced 表示当前设备是否由 InstancedMesh 渲染;fallback 时仍然保留同一套实例 API。

加载完成后,设备可以按 Facility.ID 从全局 objectManager 取回,并直接传给 viewer.controls.flyToObject()。如果需要自己合并多个对象或调整包围盒,也可以继续调用 getInstanceBoundingBox(box, true)

typescript
import { FacilityInstanceObject } from 'u-space/plugins/u-manager';

const facility = viewer.objectManager.getById<FacilityInstanceObject>('FACILITY_001');

if (facility?.isFacilityInstanceObject) {
  await viewer.controls.flyToObject(facility, {
    viewpoint: 'rightFrontTop',
    padding: 0.2,
    enableTransition: true,
  });
}
属性 / 方法说明
isFacilityInstanceObject类型标记,值为 true
typeThree.js 对象类型名,值为 'FacilityInstanceObject'
instanceId实例独立 ID,设备默认等于 Facility.ID;layer 的按 ID 查询、删除和 raycast remap 都使用该字段。
instanceKind实例类型,设备为 'Facilities'
instanceName实例显示名称,设备默认使用 Facility.Name
loader-created boundsModelInstancedLayer.addInstances() / reserveBatch() 会在建 batch 时通过继承的 setInstanceBounds() 写入模板包围盒,因此实例可直接传给 viewer.controls.flyToObject()
userData.instancedtrue 表示由 FacilityInstancedLayer 批量渲染;false 表示 fallback Model 挂在当前对象下。
typescript
import { FacilityInstanceObject } from 'u-space/plugins/u-manager';

const facility = floor.getFacilityById('FACILITY_001');

if (facility instanceof FacilityInstanceObject) {
  facility
    .setInstanceColor('#29ccff')
    .setInstanceOpacity(0.6)
    .setInstanceVisible(true);

  await viewer.controls.flyToObject(facility, {
    viewpoint: 'rightFrontTop',
    padding: 0.2,
  });
}

FacilityInstancedLayer

FacilityInstancedLayer 继承自 ModelInstancedLayer,是 scene-level Facilities 的批量渲染层。SemanticGroup.facilityLayer 默认就是该类型;它会按模型 URL 把同一模板的静态设备合并为一个或多个 InstancedMesh,并用普通 Group 包裹每个模型 URL batch,保证设备显隐、颜色、透明度和相机实例裁剪能在每帧渲染前同步,同时保留每个楼层下的 FacilityInstanceObject 引用用于业务 API 和事件冒泡。

使用 SemanticLoader 时通常不需要直接调用 addInstances();只有自定义语义解析、运行时新增设备或自建设备 batch 时才需要手动创建 layer,然后通过 semanticGroup.setFacilityLayer(layer) 挂回语义场景。

属性 / 方法说明
isFacilityInstancedLayer类型标记,值为 true
typeThree.js 对象类型名,值为 'FacilityInstancedLayer'
name构造函数默认设为 'Facilities'
getInstanceCulling()返回当前实例裁剪配置:enabledfrustumminScreenRadius
setInstanceCulling(options)设置实例裁剪配置,并返回自身。默认 enabled: falsefrustum: trueminScreenRadius: 0;默认不按相机裁剪设备,避免相机距离影响设备显隐。需要性能压缩时可设置 enabled: true 开启视锥裁剪,或再把 minScreenRadius 设为大于 0 的值开启屏幕尺寸裁剪。
getInstances()返回当前 layer 内全部实例对象数组。
getInstanceById(id)根据实例对象的 instanceId 快速获取实例;找不到时返回 undefined
hasInstance(id)判断当前 layer 是否包含对应 instanceId 的实例。
reserveBatch(url, template, capacity)为某个模型 URL 预分配 batch 容量。适合接下来会多次运行时新增实例的场景,成功返回 true,模板不支持 instancing 时返回 false
addInstance(url, template, instance)向某个模型 URL 的 batch 新增一个实例。成功返回 true,失败时调用方应回退为普通模型渲染。
addInstances(url, template, instances)向某个模型 URL 的 batch 批量新增实例。推荐运行时新增多个实例时优先使用,避免连续单个新增造成重复扩容检查。
removeInstance(instanceOrId)删除一个实例,可传实例对象或完整 instanceId。删除成功返回 true
removeInstances(instancesOrIds)删除一个或多个实例。可传单个实例对象、单个字符串 instanceId,或混合实例对象和 ID 的 iterable;返回实际删除数量。
removeBatch(url)删除某个模型 URL 的完整 batch,释放对应内部 InstancedMesh 和 cloned materials。
clearBatches()清空当前 layer 的所有模型 batch,并返回自身。

addInstances(url, template, instances) 会按模型 URL 复用或创建 batch,从 template 收集可 instancing 的可见 Mesh,克隆源材质,并为每个 batch 建立 instance matrix、颜色和透明度 attribute。没有动画、没有骨骼、没有 morph target,且每个多材质 Mesh 都有合法 geometry.groups 的模板会进入 instancing;不满足条件、模板没有可用 Mesh、instances 为空,或待加入实例的 instanceId 与当前 layer 内已有实例重复时会返回 false

动态新增时,layer 会维护每个 URL batch 的 capacity。容量足够时,新增实例只会写入逻辑数组、注册 dirty callback 并标记下一帧同步;容量不足时才按 2 倍增长策略重建该 URL 下的内部 InstancedMesh 和 attribute buffer。运行时需要连续新增大量实例时,推荐先调用 reserveBatch(url, template, expectedCount),再调用 addInstances()。删除实例不会立即收缩 GPU buffer;只有 batch 为空、调用 removeBatch()clearBatches() 时才释放内部渲染资源。

外部代码不需要直接写内部 InstancedMesh.instanceMatrix。要移动、旋转或缩放实例时,直接修改对应 InstanceObject 的 transform 即可;transform 的世界矩阵发生变化时会自动标记实例 dirty,ModelInstancedLayer 会在下一次渲染前把世界矩阵同步到内部 instance matrix 并设置 instanceMatrix.needsUpdate = true。因此即使底层使用 WebGPU storage instanced buffer,公开更新方式仍然是面向实例对象:

typescript
const instance = layer.getInstanceById('FACILITY_001');

if (instance) {
  instance.position.set(10, 0, 5);
  viewer.render();
}

如果应用为静态大场景关闭了根 Scene 的 matrixWorldAutoUpdate,修改单个实例后还需显式调用 instance.updateWorldMatrix(true, false)viewer.invalidate();修改包含实例的父 Group 时调用 group.updateWorldMatrix(true, true)。这会执行 InstanceObject 的原生方法覆盖并触发 dirty callback,不需要直接调用 layer 内部同步方法。

删除 API 支持单个字符串 ID,不需要为了删除一个实例额外包一层数组。下面两个调用等价,都会按完整 ID 删除实例:

typescript
layer.removeInstance('FACILITY_001');
layer.removeInstances('FACILITY_001');

渲染前,layer 会同步每个 FacilityInstanceObject 的世界矩阵、颜色和透明度,并只在数据变化时上传对应 buffer。默认情况下,相机移动不会改变设备 active instance 集合;显式调用 setInstanceCulling({ enabled: true }) 后,layer 才会按当前渲染相机逐实例压缩 InstancedMesh.count,视锥外设备不会进入本帧实例 buffer;只有同时设置 minScreenRadius > 0 时,屏幕半径小于该阈值的设备才会被跳过。相机矩阵、投影矩阵或 viewport 高度未变化时不会重复压缩;设备显隐、透明度为 0、颜色/透明度变化或 planish('Facilities') / unplanish('Facilities') 改变 transform 时会标记 batch dirty。全部设备不可渲染时,内部 InstancedMesh.visible 会被关闭;只有被当前相机裁掉时,mesh 会保留可见并把 count 设为 0,以便相机移动后自动恢复。射线检测命中 batch 内实例时,hit.object 会被改写为对应的 FacilityInstanceObject,并从实例对象字段附加 instanceObjectinstanceObjectIdinstanceKindinstanceNameinstanceIndex,事件随后会按楼层引用对象继续冒泡。

typescript
import { FacilityInstancedLayer, FacilityInstanceObject } from 'u-space/plugins/u-manager';

const layer = new FacilityInstancedLayer();
const refs = facilityItems.map(() => new FacilityInstanceObject());

layer.reserveBatch('/models/camera.glb', templateObject, refs.length + 100);

if (layer.addInstances('/models/camera.glb', templateObject, refs)) {
  semanticGroup.setFacilityLayer(layer);
}

// 运行时按完整字符串 ID 删除一个实例;返回实际删除数量。
layer.removeInstances('FACILITY_001');

如果只想整体隐藏或禁用 scene-level batch,可以设置 semanticGroup.facilityLayer.visible = false;如果只想控制单个设备,推荐从 floor.getFacilityById(id)viewer.objectManager.getById(id) 取回 FacilityInstanceObject 后调用 setInstanceVisible() / setInstanceOpacity()

ModelInstancedLayer

ModelInstancedLayer 是核心 src/batches 模块导出的公共类,也是 SceneInstancedLayerFacilityInstancedLayer 的共享实现。它负责模板 mesh 收集、材质克隆、InstancedMesh 创建、运行时实例新增/删除、capacity 扩容、instance matrix/color/opacity buffer 同步、raycast hit remap、fallback 判定和可选相机实例裁剪。模板 mesh 会保持独立,避免跨楼壳合并后破坏透明排序和局部包围体;射线通过实例包围体后,三角形数量较高的静态模板几何会按需建立并复用 three-mesh-bvh,小几何或不受支持的几何布局自动保留 Three.js 默认路径。普通静态模型可以由调用方对明确的热点 root 使用 enableMeshBVHRaycast(root),避免对整个大型场景自动建树造成首击卡顿。layer 内部只依赖 InstanceObjectinstanceIdinstanceKindinstanceName 独立字段,不再扫描 instance.userDatafacilityIdidsidgetInstanceById() / removeInstances(id) 会按需刷新 id 索引;新增实例时如果发现重复 instanceId 会拒绝加入,避免同一个 layer 内出现不确定查询结果。通用 API 见 Batches API

FloorMesh

FloorMesh 继承自 BaseMesh,表示一个楼层。它不是 BatchedMesh,而是把同一楼层内的语义对象合并为一份几何,并通过 TSL + DataTexture 在 GPU 侧按 semanticIndex 控制颜色、透明度、显示隐藏和实例矩阵。楼层语义网格会按实例透明度拆分为不透明和透明两个材质通道;墙、柱、门等默认不透明实例会进入 opaque pipeline,SpacesWindows 或通过 setOpacityAt() 改成半透明的实例会进入 transparent pipeline。Facilities 通过楼层下的引用对象进行检索和控制;实际渲染可能来自 SemanticGroup.facilityLayerInstancedMesh batch,也可能是 fallback 的普通 Model

这种结构的目标是让每个楼层通常只占一个主渲染 draw call,同时仍保留实例级控制能力。xxxAt 方法全部作用于单个语义实例;没有 At 后缀的方法作用于整个楼层 mesh。

属性 / 方法说明
semanticCount / instanceCount当前楼层语义实例数量。
semanticInstancesMap<string, number>,从语义对象 ID 映射到实例下标。
wallsInstances / columnsInstances / spacesInstances / doorsInstances / windowsInstances / staircasesInstances按语义类型维护的 ID 到实例下标映射。
semanticEntitiesMap<string, FloorSemanticEntity>,统一索引楼层内合并几何语义和 Facilities 对象语义。
facilityObjectsByIdMap<string, FacilityInstanceObject>,按 Facility.ID 或显式别名查找设备引用。
facilityObjects当前楼层挂载的设备引用数组。
getSemanticIndexById(id)根据语义 ID 获取实例下标。
getSemanticById(id)根据语义 ID 或设备别名获取统一语义实体。
getSemantics()获取楼层内全部语义实体,包含合并几何和设备对象。
getSemanticsByKind(kind)按语义类型过滤实体;kind 可以是 WallsColumnsSpacesDoorsWindowsStaircasesFacilities
getSemanticIdAt(index)获取实例语义 ID。
getSemanticKindAt(index)获取合并几何实例类型:'Walls' | 'Columns' | 'Spaces' | 'Doors' | 'Windows' | 'Staircases'
getSemanticNameAt(index)获取实例名称;当前 Spaces 会从语义数据的 Name 写入,其他类型可能为 undefined
getSemanticObjectAt(index)获取合并几何实例对应的 FloorSemanticInstanceObject 轻量对象。
showAllFacilities()显示当前楼层的全部设备对象。
hideAllFacilities()隐藏当前楼层的全部设备对象。
addFacility(object, ids?)把设备对象挂到楼层,并按对象 instanceId 和显式 ids 建立检索别名。不会从 object.userData 自动扫描 ID。
getFacilityById(id)根据 Facility.ID 或别名获取设备对象。
getFacilityAt(index)按楼层设备数组下标获取设备对象。
getFacilities()获取当前楼层全部设备对象。
removeFacility(facility)按设备对象或 ID 从楼层移除设备,并同步清理检索索引。
setColorAt(index, color) / getColorAt(index, target?)修改或读取实例颜色,支持 ColorVector3Vector4
setOpacityAt(index, opacity) / getOpacityAt(index)修改或读取实例透明度。
setVisibleAt(index, visible) / getVisibleAt(index)修改或读取实例显隐状态。
setMatrixAt(index, matrix) / getMatrixAt(index, target?)设置或读取实例附加矩阵。
setScaleAt(index, scale, y?, z?)修改实例缩放,支持 numberVector3
setScaleYAt(index, y)只修改实例 Y 轴缩放,保留 X/Z 缩放和已有位移、旋转。
getFinalMatrixAt(index, target?)读取实例最终矩阵,包含语义对象原始基点矩阵。
getBoundingBoxAt(index, target?, world?)获取实例包围盒;world = true 时包含 FloorMesh.matrixWorld
getBoundingSphereAt(index, target?, world?)获取实例包围球;world = true 时包含 FloorMesh.matrixWorld
computeBoundingBox() / computeBoundingSphere()计算整个楼层的包围体。
planish(kind?) / unplanish(kind?)压扁或恢复楼层内所有合并语义实例和 Facilities;压平时按各自包围盒贴到楼层平面附近,并加入稳定的轻微 Y 偏移;传入 kind 时只作用于该语义类型,kind 可包含 Facilities

飞向语义实例

点击、射线检测命中 FloorMesh 里的语义几何或设备时,intersect 上会附加语义字段:semanticIndexsemanticIdsemanticKindsemanticObject。普通楼层语义几何的事件目标是 FloorSemanticInstanceObject;instanced Facilities 的事件目标是楼层下的 FacilityInstanceObject 引用,事件会继续冒泡到所属 FloorMeshBuildingGroupSemanticGroup。因此楼层监听器中应使用 event.currentTarget 操作楼层,用 intersect.semanticObjectevent.target 取得具体语义实例。

typescript
import { Box3 } from 'three';

const box = new Box3();

floor.addEventListener('click', async ({ event }) => {
  const { currentTarget, intersect } = event;
  if (!intersect) return;

  if (intersect.semanticKind === 'Facilities') {
    const facility = intersect.semanticObject ?? event.target;

    await viewer.controls.flyToObject(facility, {
      viewpoint: 'rightFrontTop',
      padding: 0.2,
    });
    return;
  }

  if (intersect.semanticObject) {
    await viewer.controls.flyToObject(intersect.semanticObject, {
      viewpoint: 'rightFrontTop',
      padding: 0.2,
    });
    return;
  }

  const semanticBox = currentTarget.getBoundingBoxAt(intersect.semanticIndex, box, true);

  if (semanticBox) {
    await viewer.controls.flyToBox(semanticBox, {
      viewpoint: 'rightFrontTop',
      padding: 0.2,
    });
  }
});

如果要根据 ID 操作对象,先通过 getSemanticIndexById(id) 转为实例下标。

typescript
const index = floor.getSemanticIndexById('SPACE_001');

if (index !== undefined) {
  floor.setVisibleAt(index, false);
  viewer.render();
}

楼层压扁

planish() / unplanish() 默认作用于当前楼层的全部合并语义实例,包含墙、柱、空间、门窗、楼梯等语义几何,也包含楼层下的 Facilities 引用;传入 kind 时只处理该类型,例如 floor.planish('Walls')floor.planish('Facilities')。所有被压平的实例都会按自身包围盒把压平后的薄片贴到楼层平面 FloorMesh.pivot.y 附近,而不是围绕实例自身基点压缩;压平时还会按语义类型和实例 ID 加入稳定的轻微 Y 偏移,减少门、墙或其他重叠实例之间的共面闪烁。unplanish() 会恢复合并几何实例的原始矩阵,unplanish('Facilities') 会恢复设备原始 position.yscale.y。instanced Facilities 会通过楼层下的 FacilityInstanceObject transform 同步到 SemanticGroup.facilityLayer

如果只想调整单个合并几何实例,可以使用 setScaleAt()setScaleYAt(),它们会保留该语义对象自己的基点矩阵,因此压扁墙、柱、房间时不会被压到世界原点。

SceneLoader

从服务端场景包加载完整场景树(模型、组、基础形状),并自动将所有已加载对象按 idsid 注册到 viewer.objectManager。 默认会尝试读取同一路径下的 db/semantic_model.json,如果场景树节点的 id 已存在于语义文件中,则跳过该节点,方便和 SemanticLoader 同时加载而不重复创建建筑或设备。 对语义去重后仍保留的 3D 节点,SceneLoader 默认会按模型 path 自动合并重复静态模型:同一 path 的多个实例只加载一次模板,并由 SceneInstancedLayer 通过共享的 ModelInstancedLayer 批量渲染。原场景树中仍会保留每个 id / sid 对应的 SceneInstanceObject 逻辑对象,用于业务检索、显隐、颜色、透明度、包围盒和事件派发。

如果场景主要由大量普通 Mesh 组成,并且比默认的同 URL instancing 更重视 draw call 和可渲染节点数量,可以显式调用 setEditableBatching()。该模式会把兼容 Mesh 的变换烘焙到顶点,再按材质和 Geometry 布局合并为少量 SceneEditableBatchLayer Mesh;逻辑 SceneInstanceObject 仍留在原场景树中。它是静态 Geometry batching,不是 InstancedMesh、WebGPU indirect draw 或 compute rasterizer。

两种 layer 是 SceneLoader 的可替换主渲染策略,不存在继承关系:默认模式把 SceneInstancedLayer 设为 SceneGroup.sceneLayer;启用 editable batching 且至少生成一个 merged batch 时改为 SceneEditableBatchLayer。后者只接收兼容的不透明静态 Mesh;不兼容模板或子 Mesh 会继续尝试放入一个名为 SceneEditableBatchFallback 的独立 SceneInstancedLayer,重复 fallback 无法 instancing 时才退回普通 Model。因此 SceneGroup.sceneLayer / getDefaultSceneLayer() 只暴露主 SceneEditableBatchLayer,fallback SceneInstancedLayer 是同一 SceneGroup 下的辅助渲染层,不会替换主 layer;如果没有生成任何 merged Mesh,主 layer 会被移除并返回 null,即使同级 fallback 仍然存在。

模板包含动画、骨骼、morph target、非法多材质 groups 或没有可实例化网格时,会自动回退为普通 Model 渲染;fallback Model 会挂在同一个 SceneInstanceObject 下,因此调用侧不需要额外配置。

typescript
import { SceneLoader } from 'u-space/plugins/u-manager';

const sceneLoader = new SceneLoader(viewer);
sceneLoader.setPath('./scenes/my-scene');
sceneLoader.setKey('YOUR_LICENSE_KEY'); // 官方授权场景必需
sceneLoader.setEditableBatching({
  maxVerticesPerBatch: 1_500_000,
  maxIndicesPerBatch: 4_500_000,
  freezeAnimations: true,
});
const group = await sceneLoader.loadAsync(); // SceneGroup
viewer.scene.add(group);

const sceneLayer = group.getDefaultSceneLayer();

方法:

方法说明
setKey(key)设置授权场景的 RSA 解密密钥。
setEditableBatching(options?)开启可编辑静态 Geometry batching。传 true 使用默认选项,传 false 关闭;默认关闭。
loadAsync()加载并解析场景,返回去除语义重复节点后的 SceneGroup
clearCache()objectManager 中移除此加载器注册的所有 ID。
dispose()清除缓存并释放水印叠加层。

可编辑静态合批:setEditableBatching()

setEditableBatching(options) 是面向大型、主要静态但仍需按对象操作的场景的 opt-in 路径。开启后,SceneLoader 会把所有保留的 3D 节点创建为 SceneInstanceObject,不再只处理重复 path;每个唯一模型 URL 仍只加载一次模板。

typescript
import {
  SceneLoader,
  type SceneEditableBatchOptions,
  type SceneEditableBatchStats,
} from 'u-space/plugins/u-manager';

const options: SceneEditableBatchOptions = {
  maxVerticesPerBatch: 1_500_000,
  maxIndicesPerBatch: 4_500_000,
  freezeAnimations: true,
};

const sceneLoader = new SceneLoader(viewer).setEditableBatching(options);
const sceneGroup = await sceneLoader.loadAsync();
viewer.scene.add(sceneGroup);

const stats = sceneGroup.userData.editableBatch as SceneEditableBatchStats | undefined;
console.table(stats);

配置:

字段默认值说明
maxVerticesPerBatch1_500_000单个 merged Geometry 的最大顶点数;超过后开始新 chunk。
maxIndicesPerBatch4_500_000单个 merged Geometry 的最大索引数;超过后开始新 chunk。
freezeAnimationsfalsefalse 时含 animation clip 或 mixer 的模板整体 fallback;true 时烘焙当前姿态,后续播放动画前必须先 materialize。SkinnedMesh 和 morph target 始终 fallback。

合并规则

每个候选 Mesh 会生成稳定的 material key 和 geometry-layout key。只有以下状态完全兼容的 Mesh 才进入同一组:

  • 材质类型、颜色/粗糙度等序列化参数,以及贴图源和 sampler/transform 配置一致。
  • Geometry 的 indexed/non-indexed 类型、index 数组类型、attribute 名称、itemSizenormalized、数组类型和 gpuType 一致。Geometry 内容和顶点数量可以不同。
  • castShadowreceiveShadowrenderOrder 一致。

合并前会为每个源 Mesh 克隆 Geometry,并应用以下矩阵:

text
inverse(batchLayer.matrixWorld) × instance.matrixWorld × meshLocalMatrix

随后为全部顶点写入 batchObjectIndex attribute,再通过 mergeGeometries() 生成 merged Mesh。批次达到顶点或索引上限时自动切分,因此每个兼容组可能生成多个 draw call。

该方案会把重复模板的顶点展开到 merged Geometry,通常以更高的 GPU 顶点内存换取更少的 draw calls 和可渲染 Mesh 遍历;重复率极高、几何较大的同模板对象仍可能更适合原生 SceneInstancedLayer

对象级状态与 materialize

合批后,每个逻辑对象在 Float DataTexture 中占一个 RGBA texel,batch NodeMaterial 通过 TSL colorNode 和独立布尔 maskNode 读取 batchObjectIndex 对应的颜色与显隐状态;不透明 batch 保留源材质原有的 opacity 路径。因此以下操作只会把对象标记为 dirty,并立即更新 CPU 状态纹理数据,GPU 在下一帧上传,不会重新合并 Geometry:

  • setInstanceVisible() 和父级显隐变化。
  • setInstanceColor() / resetInstanceColor()
  • 不引入半透明的 setInstanceHighlight() / clearInstanceHighlight()

如果对象的世界 transform 与烘焙矩阵不同,或者有效 opacity 低于 0.999,该对象会自动 materialize():原始完整模板被 clone 为普通 Model,其中每个材质为当前实例独立克隆,然后挂到当前 SceneInstanceObject 下,同时 merged batch 中的旧副本被 mask 掉并从 BVH 命中过滤。根 Scene 使用 matrixWorldAutoUpdate = false 时,transform 修改后必须调用该对象的 updateWorldMatrix(true, false),或在父 Group 上调用 updateWorldMatrix(true, true),才能提交新世界矩阵并立即触发 dirty/materialize 检测。该同步发生在内部 batch 视锥裁剪前,从屏幕外移动到屏幕内也不会丢失。materialize 是单向操作;对象不会在恢复原矩阵或不透明度后自动重新进入 batch,需要重新加载场景。也可以提前显式调用:

typescript
const object = viewer.objectManager.getById<SceneInstanceObject>('SCENE_NODE_ID');
const model = object?.materialize();

if (model) {
  object.position.x += 2;
  object.setInstanceOpacity(0.5);
  await viewer.render();
}

Fallback 与透明材质

透明材质、opacity < 0.999、SkinnedMesh、morph target、多材质数组、自定义 NodeMaterial / onBeforeCompile、非标准 Geometry 布局、负行列式变换和未冻结动画不会进入 merged batch。负 determinant 会按实例拆分;同模板的正 determinant 实例仍可合并,而负缩放实例直接使用普通 Model,不会继续进入同样不支持负缩放的 SceneInstancedLayer。混合模板会只合并兼容的不透明 Mesh,并生成一个隐藏这些已合并 Mesh 的 fallback 模板来保留透明或其他不支持的子 Mesh;build 期间已经因初始 opacity materialize 的实例不会再次加入透明 fallback。其余同 URL fallback 有多个实例时仍由 SceneInstancedLayer 渲染,单例则使用普通 Model

这条边界用于保留透明排序、蒙皮/变形和自定义 shader 行为。不要通过强制改成不透明材质来提高合批率。

Raycast 与统计

merged Mesh 首次实际拾取时会按需建立 three-mesh-bvh。命中结果通过 faceIndex → vertexIndex → batchObjectIndex 映射回原始 SceneInstanceObject,因此 intersect.object 仍是业务逻辑对象;已经 materialize 的对象不会在旧烘焙位置继续产生命中。

SceneEditableBatchStats 写入 sceneGroup.userData.editableBatch

字段说明
instances进入 editable batch 状态表的逻辑对象数。
sourceMeshes合并前的兼容源 Mesh 数。
batches最终 merged Mesh 数。
drawCallsSavedsourceMeshes - batches 的理论 draw-call 减少量,不包含多 pass 放大。
vertices / indices烘焙后的总顶点数和索引数,用于评估显存代价。
unsupportedInstances需要完整或部分 fallback 的逻辑对象数。
unsupportedByReasontransparentanimationskinnedmorphmaterial-arraygeometry-layoutnegative-scale 等原因统计。

WebGPU 首次创建大量 batch material pipeline 时可能出现短暂的 shader 编译开销。如果产品不希望首个可见帧展示未完成的材质,可在需求渲染模式下暂时停止相机控制产生的 render invalidation,完成 compileAsync() 后再提交首帧;examples/test_umanager2.html 展示了这一流程。

SceneGroup

SceneGroup 继承自 BaseGroup,是 SceneLoader.loadAsync() 的返回根组。它保留 tree_models.json 解析后的原始父子层级,并把重复静态模型的默认合批渲染层挂在 sceneLayer,方便外部直接访问,而不需要遍历 children

属性 / 方法说明
isSceneGroup固定为 true,用于判断对象是否为 SceneLoader 根组。
sceneLayer默认 path instancing 下为 SceneInstancedLayer;启用 editable batching 后为 SceneEditableBatchLayer;没有可合批模型时为 null
setSceneLayer(layer)设置 SceneInstancedLayerSceneEditableBatchLayer;传入 null 会移除已有 layer。
getDefaultSceneLayer()返回当前默认场景渲染层;没有可合批模型时返回 null

SceneInstanceObject

SceneInstanceObject 继承自统一的 InstanceObject,表示一个由 SceneInstancedLayerSceneEditableBatchLayer 或 fallback 普通模型渲染的场景模型引用。它会保留在原场景树层级中,并注册到 viewer.objectManager,因此 getById()show() / hide()isolate()showAll()setOpacity()getBoundingBox() 可以继续按单个模型 ID 使用。

完整的继承字段、方法、颜色模式、包围盒和 dirty 同步语义见 核心 InstanceObject API

SceneInstanceObject 本身只增加场景模型标记:isSceneInstanceObject = truetype = 'SceneInstanceObject'instanceKind = 'SceneInstances'。加载器会把场景树节点的 idsid 都注册到 viewer.objectManager,因此同一个对象可以通过任一 ID 取回。对重复模型的 instanced 路径,instanceId 默认等于场景树节点 idinstanceName 默认等于节点名称;userData.instanced = true,并保留 modelPathmodelUrl 等原始加载元数据。如果模板不支持 instancing,会退回普通 Model,但仍挂在同一个 SceneInstanceObject 下,对外 API 不变。

识别字段:

字段说明
isSceneInstanceObject固定为 true,用于判断对象是否来自 SceneLoader 的场景实例。
type固定为 SceneInstanceObject
instanceId实例独立 ID,默认等于场景树节点 id;layer 的按 ID 查询、删除和 raycast remap 都使用该字段。
instanceKind实例类型,场景实例为 'SceneInstances'
instanceName实例显示名称,默认等于场景树节点名称。
userData.id / userData.sid原始场景树节点 ID;两个 ID 都会注册到 viewer.objectManager
userData.instanced加载时选择 batch 路径的元数据。editable batch 对象 materialize 后该字段不作为实时状态;使用 getInstanceRenderObject() 是否非空判断当前是否已有普通渲染对象。
userData.modelPath / userData.modelUrlinstanced 场景实例对应的模型相对路径和完整 URL。

方法:

方法说明
继承的实例 API完整定义见 核心 InstanceObject API;包含身份、bounds、fallback render object、显隐、颜色、透明度、高亮和 dirty callback。
materialize()如果对象由 SceneEditableBatchLayer 渲染,clone 原始完整模板并将当前对象从 merged/fallback batch 中移出;默认 path instancing 或普通 fallback 下返回已有 render object 或 null

MaterialEffects.highlightColor() / removeHighlightColor() 会识别 SceneInstanceObject 并调用 setInstanceHighlight() / clearInstanceHighlight(),不会直接修改共享 batch 材质。ObjectManager.setOpacity()ObjectManager.hide()ObjectManager.show() 也会通过统一 API 作用到单个实例。

加载完成后可以按场景树节点 idobjectManager 取回 SceneInstanceObject,再直接传给 viewer.controls.flyToObject()SceneInstanceObject 内部带有本地 boundingBox 缓存能力,因此不需要加载真实 Mesh 副本也能计算飞行包围盒。

typescript
import { SceneInstanceObject } from 'u-space/plugins/u-manager';

const object = viewer.objectManager.getById<SceneInstanceObject>('SCENE_NODE_ID');

if (object?.isSceneInstanceObject) {
  await viewer.controls.flyToObject(object, {
    viewpoint: 'current',
    padding: 0.2,
    enableTransition: true,
  });
}

颜色、透明度和显隐可以直接链式调用:

typescript
object
  ?.setInstanceColor('#29ccff')
  .setInstanceOpacity(0.55)
  .setInstanceVisible(true);

object?.setInstanceHighlight('#ffb703', 0.75, true);
object?.clearInstanceHighlight();
object?.resetInstanceColor();

如果需要自己控制包围盒,也可以取世界包围盒后调用 flyToBox()

typescript
import { Box3 } from 'three/webgpu';

const box = object.getInstanceBoundingBox(new Box3(), true);
await viewer.controls.flyToBox(box, { viewpoint: 'frontTop', padding: 0.2 });

射线检测命中批处理实例时,intersect.object 会被改写为对应的 SceneInstanceObject,并附加 instanceObjectinstanceObjectIdinstanceKind: 'SceneInstances'instanceNameinstanceIndex。Three.js 原生的 intersect.instanceId 仍保留为内部 InstancedMesh 的数字实例下标,事件会按逻辑对象所在的原场景树继续冒泡。

SceneInstancedLayer

SceneInstancedLayerSceneLoader 内部使用的批量渲染层,默认 name = 'SceneInstances'。它继承自 ModelInstancedLayer,每个重复模型 URL 会生成一个 batch,batch 内按模板的可实例化 Mesh 创建 InstancedMesh。渲染前只在实例矩阵、显隐、颜色或透明度变化时同步 instance buffer;普通相机移动不会重新上传所有实例数据。

SceneEditableBatchLayer

SceneEditableBatchLayer 继承自核心 EditableGeometryBatchLayer<SceneInstanceObject>,后者继承自 BaseGroup。它是 setEditableBatching() 创建的 scene-specific 静态 Geometry 合并层,默认 name = 'SceneEditableBatches';通用的材质/Geometry 分组、变换烘焙、chunk 切分、TSL 状态纹理、materialize、lazy BVH 和资源释放都由核心层实现。内部未导出的 EditableGeometryBatchMesh 继承自 BaseMesh;隐藏整个 layer 或单个 batch Mesh 时,ignoreInvisibleWhenRaycast 会在进入自定义 BVH raycast 前停止命中,阴影属性仍由对应源 Mesh 的 castShadow / receiveShadow 覆盖。

它不继承 SceneInstancedLayer。开启 editable batching 且生成 merged Mesh 时,SceneEditableBatchLayerSceneGroup.sceneLayer 的主层;不兼容的重复 fallback 由同级、名称为 SceneEditableBatchFallbackSceneInstancedLayer 辅助渲染,单例或仍不支持 instancing 的对象使用挂在 SceneInstanceObject 下的普通 Model。没有 merged Mesh 时主 layer 为 null,辅助 fallback 不会被 getDefaultSceneLayer() 返回。

通常由 SceneLoader 管理,不需要业务代码直接调用 scene adapter 的 addTemplate() / build(),也不需要自行处理 materialize 后的 fallback 移除。公开的 statsoptions 可用于诊断;materialize 通知使用继承的类型化 materialize 事件,释放独立创建的 layer 时应调用 dispose()。如果要在其他插件或应用中直接复用同一机制,应使用核心 EditableGeometryBatchLayeraddSource() API。

TopologiesLoader / TopologyParser

加载拓扑图数据并将其转换为 Topology 对象。

typescript
import { TopologiesLoader } from 'u-space/plugins/u-manager';

const loader = new TopologiesLoader();
loader.setPath('./scenes/my-scene');
const topologies = await loader.loadAsync(); // Topology[]
viewer.scene.add(...topologies);

VisionsLoader / VisionsParser

加载命名相机视点,并将相机飞行到指定视点。

typescript
import { VisionsLoader, VisionsParser } from 'u-space/plugins/u-manager';

const visionsLoader = new VisionsLoader();
visionsLoader.setPath('./scenes/my-scene');
const visionsData = await visionsLoader.loadAsync();
// visionsData 是 Record<string, IVisions[]>

const visionsParser = new VisionsParser(viewer);

// 飞行到指定视点
await visionsParser.flyTo(visionsData['HOME'][0]);

// 飞行到主(默认)视点
await visionsParser.flyToPrimary(visionsData['HOME']);

IVisions 字段:

字段类型说明
camera'P' | 'O'相机类型:透视或正交。
positionIVector3相机世界位置。
targetIVector3相机朝向目标点。
zoomnumber相机缩放级别。
primaryboolean是否为默认视点。

AnimationsLoader / AnimationsParser

加载关键帧动画数据,并通过 tweenAnimation 驱动 Object3D 的变换。

typescript
import { AnimationsLoader, AnimationsParser } from 'u-space/plugins/u-manager';

const animLoader = new AnimationsLoader();
animLoader.setPath('./scenes/my-scene');
const animationsData = await animLoader.loadAsync(); // IAnimations[]

// 查找特定对象的动画
const data = animationsData.find((a) => a.modelId === myModel.userData.id);

const parser = new AnimationsParser(viewer, myModel);
parser.initTransform(); // 保存初始位置/旋转/缩放

await parser.play(data.keyframes); // 播放动画序列

// 中途停止
parser.stop();

// 重置到初始变换
parser.reset();

PropertiesLoader

加载与模型关联的结构化属性元数据(如 BIM 属性)。

typescript
import { PropertiesLoader } from 'u-space/plugins/u-manager';

const propsLoader = new PropertiesLoader();
propsLoader.setPath('./scenes/my-scene');
const properties = await propsLoader.loadAsync(); // IProperties[]

// 查找模型的属性
const modelProps = properties.filter(p => p.modelId === myModel.userData.id);

IProperties 字段:

字段类型说明
modelIdstring关联模型的 ID。
groupstring属性分组/类别名称。
keystring属性键名。
valuestring | null属性值。
labelstring | null属性的显示标签。

u-space docs