u-space 0.0.28 → 0.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/Viewer-BKNV67Jj.cjs +1 -0
- package/dist/Viewer-Cs_y7IiH.js +1256 -0
- package/dist/index.cjs +3 -3
- package/dist/index.js +1715 -1497
- package/dist/plugins/atmosphere/AgxToneMapping.d.ts +1 -1
- package/dist/plugins/atmosphere/index.cjs +1 -1
- package/dist/plugins/atmosphere/index.js +4 -4
- package/dist/plugins/curve-movement/index.cjs +1 -1
- package/dist/plugins/curve-movement/index.js +1 -1
- package/dist/plugins/fire/index.cjs +1 -1
- package/dist/plugins/fire/index.js +5 -5
- package/dist/plugins/object-controls/index.cjs +1 -1
- package/dist/plugins/object-controls/index.js +4 -4
- package/dist/plugins/tiles/index.cjs +1 -1
- package/dist/plugins/tiles/index.js +5 -5
- package/dist/plugins/u-manager/index.cjs +51 -51
- package/dist/plugins/u-manager/index.d.ts +1 -1
- package/dist/plugins/u-manager/index.js +8330 -8637
- package/dist/plugins/u-manager/loaders/SceneEditableBatchLayer.d.ts +17 -0
- package/dist/plugins/u-manager/loaders/SceneInstancedLayer.d.ts +3 -3
- package/dist/plugins/u-manager/loaders/SceneLoader.d.ts +20 -3
- package/dist/plugins/u-manager/loaders/UManagerLoader.d.ts +4 -4
- package/dist/plugins/u-manager/semantics/objects/BuildingGroup.d.ts +12 -5
- package/dist/plugins/u-manager/semantics/objects/FacilityInstancedLayer.d.ts +4 -4
- package/dist/plugins/u-manager/semantics/objects/FloorMesh.d.ts +28 -22
- package/dist/plugins/u-manager/semantics/objects/SemanticGroup.d.ts +6 -6
- package/dist/protocol-C-OMPz15.js +10 -0
- package/dist/protocol-IUAzx0lk.cjs +1 -0
- package/dist/src/batches/EditableGeometryBatchLayer.d.ts +50 -0
- package/dist/src/batches/ModelInstancedLayer.d.ts +42 -0
- package/dist/src/batches/index.d.ts +2 -0
- package/dist/src/effects/TSLEffects.d.ts +24 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/instances/InstanceObject.d.ts +57 -0
- package/dist/src/instances/index.d.ts +1 -0
- package/dist/src/interactions/MeshBVHRaycast.d.ts +9 -0
- package/dist/src/interactions/index.d.ts +1 -0
- package/dist/src/viewers/RenderPipeline.d.ts +37 -0
- package/dist/src/viewers/ReversedDepthSSGICompat.d.ts +10 -0
- package/dist/src/viewers/ReversedDepthSSRCompat.d.ts +20 -0
- package/dist/src/viewers/Viewer.d.ts +3 -1
- package/dist/src/viewers/renderInvalidation.d.ts +2 -0
- package/dist/src/worker/FrameTimingWindow.d.ts +16 -0
- package/dist/src/worker/OffscreenViewerHost.d.ts +20 -0
- package/dist/src/worker/WorkerDomTarget.d.ts +44 -0
- package/dist/src/worker/createWorkerViewer.d.ts +18 -0
- package/dist/src/worker/index.d.ts +2 -0
- package/dist/src/worker/installWorkerImageLoader.d.ts +5 -0
- package/dist/src/worker/protocol.d.ts +102 -0
- package/dist/src/worker/runtime.d.ts +2 -0
- package/dist/worker/index.cjs +1 -0
- package/dist/worker/index.js +224 -0
- package/dist/worker/runtime.cjs +1 -0
- package/dist/worker/runtime.js +418 -0
- package/docs/api-batches.md +112 -0
- package/docs/api-effects.md +2 -2
- package/docs/api-interactions.md +8 -0
- package/docs/api-managers.md +1 -1
- package/docs/api-objects.md +207 -0
- package/docs/api-plugin-u-manager.md +245 -96
- package/docs/api-render-pipeline.md +103 -5
- package/docs/api-viewer.md +188 -1
- package/docs/changelog.md +69 -6
- package/docs/examples-guide.md +111 -17
- package/docs/getting-started.md +1 -1
- package/docs/index.md +11 -4
- package/docs/mcp.md +29 -14
- package/docs/release.md +95 -0
- package/package.json +27 -4
- package/dist/plugins/u-manager/instances/SemanticInstanceObject.d.ts +0 -37
- package/dist/plugins/u-manager/instances/SemanticModelInstancedLayer.d.ts +0 -33
- package/dist/plugins/u-manager/instances/index.d.ts +0 -2
|
@@ -20,9 +20,11 @@ viewer.scene.add(group);
|
|
|
20
20
|
|
|
21
21
|
const semanticGroup = group.semanticGroup;
|
|
22
22
|
const sceneGroup = group.sceneGroup;
|
|
23
|
+
const facilityLayer = semanticGroup.getDefaultFacilityLayer();
|
|
24
|
+
const sceneLayer = sceneGroup.getDefaultSceneLayer();
|
|
23
25
|
```
|
|
24
26
|
|
|
25
|
-
完整可运行示例见 [`examples/test_umanager_loader.html`](https://u-space-phi.vercel.app/examples/test_umanager_loader.html)。该示例展示了如何从返回根组中直接读取 `semanticGroup` / `sceneGroup
|
|
27
|
+
完整可运行示例见 [`examples/test_umanager_loader.html`](https://u-space-phi.vercel.app/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`](https://u-space-phi.vercel.app/examples/test_umanager_dynamic_instances.html)。
|
|
26
28
|
|
|
27
29
|
## `SemanticLoader`
|
|
28
30
|
|
|
@@ -57,7 +59,11 @@ floor.addEventListener('click', ({ event }) => {
|
|
|
57
59
|
});
|
|
58
60
|
```
|
|
59
61
|
|
|
60
|
-
所有可按 ID 操作的实例都会尽量暴露统一的 `
|
|
62
|
+
所有可按 ID 操作的实例都会尽量暴露统一的 `InstanceObject` API。楼层多边形语义使用轻量 `FloorSemanticInstanceObject` 代理合并几何中的一个实例;Facilities 和 SceneLoader path instancing 使用同一套模型实例化底层。业务代码优先使用 `setInstanceVisible()`、`setInstanceColor()`、`setInstanceOpacity()` 和 `viewer.controls.flyToObject(instance)`,而不是关心底层是合并几何、`InstancedMesh` 还是 fallback `Model`。
|
|
63
|
+
|
|
64
|
+
完整的继承字段、方法、颜色模式、包围盒和 dirty 同步语义见 [核心 `InstanceObject` API](./api-objects.md#instanceobject)。
|
|
65
|
+
|
|
66
|
+
`InstanceObject` 也按 Three.js 对象级包围盒约定提供 `boundingBox`、`boundingSphere`、`computeBoundingBox()` 和 `computeBoundingSphere()`。它们缓存的是实例本地坐标包围体,`Box3.setFromObject()` 会自动调用 `computeBoundingBox()` 并应用 `matrixWorld`,因此 `viewer.controls.flyToObject()` 可以直接飞向 `SceneInstanceObject`、`FacilityInstanceObject` 和楼层实例代理。需要手动处理世界包围盒时,仍可使用 `getInstanceBoundingBox(target, true)`。
|
|
61
67
|
|
|
62
68
|
### `SemanticGroup`
|
|
63
69
|
|
|
@@ -73,6 +79,7 @@ floor.addEventListener('click', ({ event }) => {
|
|
|
73
79
|
| `getBuildingAt(index)` / `getBuildingById(id)` / `getBuildings()` | 按下标、`Building.ID` 或整体列表获取建筑。 |
|
|
74
80
|
| `floorMeshes` | 聚合返回所有建筑内的 `FloorMesh[]`。 |
|
|
75
81
|
| `setFacilityLayer(layer)` | 设置跨楼层设备渲染层;传入 `null` 会移除已有 layer。 |
|
|
82
|
+
| `getDefaultFacilityLayer()` | 返回当前默认 `FacilityInstancedLayer`;没有可实例化 Facilities 时返回 `null`。 |
|
|
76
83
|
| `getFacilityById(id)` | 在整次语义解析结果中按 `Facility.ID` 或显式别名查找设备。 |
|
|
77
84
|
| `showAllFacilities()` / `hideAllFacilities()` | 显示或隐藏整次语义解析结果中的全部设备。 |
|
|
78
85
|
| `showAllFloors()` | 显示所有建筑的所有楼层。 |
|
|
@@ -102,7 +109,7 @@ floor.addEventListener('click', ({ event }) => {
|
|
|
102
109
|
| `isolateFloor(floor \| index)` | 只显示指定楼层,隐藏其他楼层;也兼容传入楼层数组。 |
|
|
103
110
|
| `isolateFloors(floors \| indexes)` | 只显示指定多个楼层,隐藏其他楼层。 |
|
|
104
111
|
| `showAllFloors()` | 显示全部楼层。 |
|
|
105
|
-
| `getFacilityById(id)` | 在当前建筑的楼层中按 `Facility.ID`
|
|
112
|
+
| `getFacilityById(id)` | 在当前建筑的楼层中按 `Facility.ID` 或 `FloorMesh.addFacility()` 显式传入的别名查找设备。 |
|
|
106
113
|
| `showAllFacilities()` | 显示整栋建筑内全部楼层设备。 |
|
|
107
114
|
| `hideAllFacilities()` | 隐藏整栋建筑内全部楼层设备。 |
|
|
108
115
|
| `planishFloors(kind?)` | 压扁所有楼层;默认覆盖所有合并语义实例和 Facilities,并按包围盒贴到楼层平面附近,同时添加稳定的轻微 Y 偏移以减少共面闪烁;传入 `kind` 时只压扁该语义类型,`kind` 可包含 `Facilities`。 |
|
|
@@ -135,27 +142,22 @@ if (vent) {
|
|
|
135
142
|
|
|
136
143
|
`tree_models.matrix` 是相对父节点的本地矩阵;`SemanticLoader` 会沿树父级累乘得到设备世界矩阵,再转换为对应 `FloorMesh` 的局部矩阵。因此楼层移动、隔离或显示隐藏时设备会跟随楼层一起工作。即使某个 story 只有 `Facilities`、没有墙柱空间等合并几何,也会创建一个空的 `FloorMesh` 作为设备容器。
|
|
137
144
|
|
|
138
|
-
Facilities 会在单次 `SemanticParser` 解析范围内按模型 URL 自动分组。静态模型会被转换为 scene-level `InstancedMesh` batch 并挂到 `SemanticGroup.facilityLayer`;每个设备仍会在所属 `FloorMesh` 下保留一个 `FacilityInstanceObject`
|
|
145
|
+
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 不变。
|
|
139
146
|
|
|
140
|
-
`facilityLayer` 使用 `FacilityInstancedLayer`,并继承自 `BaseGroup`。当 `facilityLayer.visible = false` 时,layer 会阻止内部 `InstancedMesh` 继续参与射线检测,避免隐藏的批处理设备仍被点击命中;单个设备的显隐应先通过 `getFacilityById()` 或 `viewer.objectManager.getById()` 取回实例,再调用 `
|
|
147
|
+
`facilityLayer` 使用 `FacilityInstancedLayer`,并继承自 `BaseGroup`。当 `facilityLayer.visible = false` 时,layer 会阻止内部 `InstancedMesh` 继续参与射线检测,避免隐藏的批处理设备仍被点击命中;单个设备的显隐应先通过 `getFacilityById()` 或 `viewer.objectManager.getById()` 取回实例,再调用 `setInstanceVisible()` 控制。
|
|
141
148
|
|
|
142
|
-
Instanced 设备通过 per-instance opacity attribute 控制单个设备透明度。batch 材质默认沿用源材质的透明状态;只有源材质本身透明,或当前参与渲染的设备存在 `opacity < 1` 时,材质才会切到 `transparent: true`,`
|
|
149
|
+
Instanced 设备通过 per-instance opacity attribute 控制单个设备透明度。batch 材质默认沿用源材质的透明状态;只有源材质本身透明,或当前参与渲染的设备存在 `opacity < 1` 时,材质才会切到 `transparent: true`,`setInstanceOpacity(1)` 可在没有其他半透明实例时回到不透明渲染队列。`resetInstanceColor()` 只清除颜色 override,不影响透明度 override。`FacilityInstancedLayer` 会在设备显隐、颜色、透明度、floor planish/unplanish 改变设备 transform,或相机矩阵、投影矩阵、viewport 高度变化时,在下一次渲染前同步实例矩阵、颜色、透明度和当前 active instance count。普通 fallback `Model` 的颜色和透明度控制由 wrapper 委托到 `MaterialEffects`,单材质和多材质 Mesh 都会应用 override。
|
|
143
150
|
|
|
144
|
-
|
|
151
|
+
加载成功后,`BuildingGroup`、`FloorMesh` 仍会保留自身的 `semanticId`、`semanticKind` 和 `semanticName`;设备引用则使用通用 `InstanceObject` 身份字段 `instanceId`、`instanceKind` 和 `instanceName`。`SemanticGroup.getBuildingById()`、`BuildingGroup.getFloorById()`、`BuildingGroup.getFacilityById()`、`FloorMesh.getFacilityById()` 以及 `ModelInstancedLayer` 的查询、删除和 raycast remap 都使用这些对象字段或显式传入的别名,不再从 `userData.id`、`userData.semanticId`、`userData.facilityId` 里扫描 ID。`userData` 只保留原始业务元数据,例如 `facilityId`、`spaces`、`twinsIdentifier`、`storyId`、`floorIndex`、`instanced` 和 `modelUrl`。
|
|
145
152
|
|
|
146
153
|
```typescript
|
|
147
|
-
import { Box3 } from 'three';
|
|
148
|
-
|
|
149
154
|
const floor = building.getFloorAt(0);
|
|
150
|
-
const box = new Box3();
|
|
151
155
|
|
|
152
156
|
const facility = floor.getFacilityById('FACILITY_001') ?? viewer.objectManager.getById('FACILITY_001');
|
|
153
157
|
const facilityEntity = floor.getSemanticById('FACILITY_001');
|
|
154
158
|
|
|
155
159
|
if (facility && facilityEntity?.type === 'object') {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
await viewer.controls.flyToBox(facilityBox, {
|
|
160
|
+
await viewer.controls.flyToObject(facility, {
|
|
159
161
|
viewpoint: 'rightFrontTop',
|
|
160
162
|
padding: 0.2,
|
|
161
163
|
});
|
|
@@ -164,32 +166,31 @@ if (facility && facilityEntity?.type === 'object') {
|
|
|
164
166
|
const facilities = floor.getSemanticsByKind('Facilities');
|
|
165
167
|
|
|
166
168
|
facility
|
|
167
|
-
.
|
|
168
|
-
.
|
|
169
|
-
.
|
|
170
|
-
.
|
|
169
|
+
.setInstanceColor('#ff5533')
|
|
170
|
+
.setInstanceOpacity(0.45)
|
|
171
|
+
.resetInstanceColor()
|
|
172
|
+
.setInstanceVisible(true);
|
|
171
173
|
```
|
|
172
174
|
|
|
173
|
-
对 Facilities
|
|
175
|
+
对 Facilities 推荐直接使用 `controls.flyToObject()` 飞向设备。无论设备实际渲染来自 `InstancedMesh` 还是 fallback `Model`,`FacilityInstanceObject` 都会暴露本地 `boundingBox` / `boundingSphere`,让 Three.js 的 `Box3.setFromObject()` 能计算完整世界包围盒。
|
|
174
176
|
|
|
175
177
|
### `FacilityInstanceObject`
|
|
176
178
|
|
|
177
|
-
`FacilityInstanceObject` 继承自统一的 `
|
|
179
|
+
`FacilityInstanceObject` 继承自统一的 `InstanceObject`,表示一个设备实例引用。它会挂在所属 `FloorMesh` 下,用于 ID 检索、事件派发、显隐、颜色、透明度和包围盒查询;如果设备可以 instancing,真正几何由 `SemanticGroup.facilityLayer` 里的 `InstancedMesh` 统一渲染;如果不能 instancing,fallback `Model` 会作为它的子对象挂载。
|
|
178
180
|
|
|
179
|
-
|
|
181
|
+
完整的继承字段、方法、颜色模式、包围盒和 dirty 同步语义见 [核心 `InstanceObject` API](./api-objects.md#instanceobject)。
|
|
180
182
|
|
|
181
|
-
|
|
183
|
+
通常不需要手动创建 `FacilityInstanceObject`。`SemanticParser` 在解析 `Facilities` 时会自动创建它,并通过 `setInstanceIdentity()` 写入 `instanceId`、`instanceKind` 和 `instanceName`。`userData.instanced` 表示当前设备是否由 `InstancedMesh` 渲染;fallback 时仍然保留同一套实例 API。
|
|
184
|
+
|
|
185
|
+
加载完成后,设备可以按 `Facility.ID` 从全局 `objectManager` 取回,并直接传给 `viewer.controls.flyToObject()`。如果需要自己合并多个对象或调整包围盒,也可以继续调用 `getInstanceBoundingBox(box, true)`。
|
|
182
186
|
|
|
183
187
|
```typescript
|
|
184
|
-
import { Box3 } from 'three';
|
|
185
188
|
import { FacilityInstanceObject } from 'u-space/plugins/u-manager';
|
|
186
189
|
|
|
187
190
|
const facility = viewer.objectManager.getById<FacilityInstanceObject>('FACILITY_001');
|
|
188
191
|
|
|
189
192
|
if (facility?.isFacilityInstanceObject) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
await viewer.controls.flyToBox(box, {
|
|
193
|
+
await viewer.controls.flyToObject(facility, {
|
|
193
194
|
viewpoint: 'rightFrontTop',
|
|
194
195
|
padding: 0.2,
|
|
195
196
|
enableTransition: true,
|
|
@@ -201,30 +202,24 @@ if (facility?.isFacilityInstanceObject) {
|
|
|
201
202
|
| :---------- | :--- |
|
|
202
203
|
| `isFacilityInstanceObject` | 类型标记,值为 `true`。 |
|
|
203
204
|
| `type` | Three.js 对象类型名,值为 `'FacilityInstanceObject'`。 |
|
|
204
|
-
| `
|
|
205
|
-
| `
|
|
206
|
-
| `
|
|
207
|
-
| `
|
|
208
|
-
| `
|
|
209
|
-
| `setSemanticOpacity(opacity)` | 设置单个语义实例透明度,并返回自身;值会被限制在 `0..1`。 |
|
|
210
|
-
| `getSemanticColor(target?)` | 读取当前颜色 override;没有 override 时返回白色 `(1, 1, 1)`。 |
|
|
211
|
-
| `hasSemanticColor()` | 判断当前实例是否存在颜色 override。 |
|
|
212
|
-
| `getSemanticOpacity()` | 读取当前透明度 override。 |
|
|
205
|
+
| `instanceId` | 实例独立 ID,设备默认等于 `Facility.ID`;layer 的按 ID 查询、删除和 raycast remap 都使用该字段。 |
|
|
206
|
+
| `instanceKind` | 实例类型,设备为 `'Facilities'`。 |
|
|
207
|
+
| `instanceName` | 实例显示名称,设备默认使用 `Facility.Name`。 |
|
|
208
|
+
| loader-created bounds | `ModelInstancedLayer.addInstances()` / `reserveBatch()` 会在建 batch 时通过继承的 `setInstanceBounds()` 写入模板包围盒,因此实例可直接传给 `viewer.controls.flyToObject()`。 |
|
|
209
|
+
| `userData.instanced` | `true` 表示由 `FacilityInstancedLayer` 批量渲染;`false` 表示 fallback `Model` 挂在当前对象下。 |
|
|
213
210
|
|
|
214
211
|
```typescript
|
|
215
|
-
import { Box3 } from 'three';
|
|
216
212
|
import { FacilityInstanceObject } from 'u-space/plugins/u-manager';
|
|
217
213
|
|
|
218
|
-
const box = new Box3();
|
|
219
214
|
const facility = floor.getFacilityById('FACILITY_001');
|
|
220
215
|
|
|
221
216
|
if (facility instanceof FacilityInstanceObject) {
|
|
222
217
|
facility
|
|
223
|
-
.
|
|
224
|
-
.
|
|
225
|
-
.
|
|
218
|
+
.setInstanceColor('#29ccff')
|
|
219
|
+
.setInstanceOpacity(0.6)
|
|
220
|
+
.setInstanceVisible(true);
|
|
226
221
|
|
|
227
|
-
await viewer.controls.
|
|
222
|
+
await viewer.controls.flyToObject(facility, {
|
|
228
223
|
viewpoint: 'rightFrontTop',
|
|
229
224
|
padding: 0.2,
|
|
230
225
|
});
|
|
@@ -233,9 +228,9 @@ if (facility instanceof FacilityInstanceObject) {
|
|
|
233
228
|
|
|
234
229
|
### `FacilityInstancedLayer`
|
|
235
230
|
|
|
236
|
-
`FacilityInstancedLayer` 继承自 `
|
|
231
|
+
`FacilityInstancedLayer` 继承自 `ModelInstancedLayer`,是 scene-level Facilities 的批量渲染层。`SemanticGroup.facilityLayer` 默认就是该类型;它会按模型 URL 把同一模板的静态设备合并为一个或多个 `InstancedMesh`,并用普通 `Group` 包裹每个模型 URL batch,保证设备显隐、颜色、透明度和相机实例裁剪能在每帧渲染前同步,同时保留每个楼层下的 `FacilityInstanceObject` 引用用于业务 API 和事件冒泡。
|
|
237
232
|
|
|
238
|
-
使用 `SemanticLoader` 时通常不需要直接调用 `
|
|
233
|
+
使用 `SemanticLoader` 时通常不需要直接调用 `addInstances()`;只有自定义语义解析、运行时新增设备或自建设备 batch 时才需要手动创建 layer,然后通过 `semanticGroup.setFacilityLayer(layer)` 挂回语义场景。
|
|
239
234
|
|
|
240
235
|
| 属性 / 方法 | 说明 |
|
|
241
236
|
| :---------- | :--- |
|
|
@@ -244,11 +239,42 @@ if (facility instanceof FacilityInstanceObject) {
|
|
|
244
239
|
| `name` | 构造函数默认设为 `'Facilities'`。 |
|
|
245
240
|
| `getInstanceCulling()` | 返回当前实例裁剪配置:`enabled`、`frustum`、`minScreenRadius`。 |
|
|
246
241
|
| `setInstanceCulling(options)` | 设置实例裁剪配置,并返回自身。默认 `enabled: false`、`frustum: true`、`minScreenRadius: 0`;默认不按相机裁剪设备,避免相机距离影响设备显隐。需要性能压缩时可设置 `enabled: true` 开启视锥裁剪,或再把 `minScreenRadius` 设为大于 0 的值开启屏幕尺寸裁剪。 |
|
|
247
|
-
| `
|
|
242
|
+
| `getInstances()` | 返回当前 layer 内全部实例对象数组。 |
|
|
243
|
+
| `getInstanceById(id)` | 根据实例对象的 `instanceId` 快速获取实例;找不到时返回 `undefined`。 |
|
|
244
|
+
| `hasInstance(id)` | 判断当前 layer 是否包含对应 `instanceId` 的实例。 |
|
|
245
|
+
| `reserveBatch(url, template, capacity)` | 为某个模型 URL 预分配 batch 容量。适合接下来会多次运行时新增实例的场景,成功返回 `true`,模板不支持 instancing 时返回 `false`。 |
|
|
246
|
+
| `addInstance(url, template, instance)` | 向某个模型 URL 的 batch 新增一个实例。成功返回 `true`,失败时调用方应回退为普通模型渲染。 |
|
|
247
|
+
| `addInstances(url, template, instances)` | 向某个模型 URL 的 batch 批量新增实例。推荐运行时新增多个实例时优先使用,避免连续单个新增造成重复扩容检查。 |
|
|
248
|
+
| `removeInstance(instanceOrId)` | 删除一个实例,可传实例对象或完整 `instanceId`。删除成功返回 `true`。 |
|
|
249
|
+
| `removeInstances(instancesOrIds)` | 删除一个或多个实例。可传单个实例对象、单个字符串 `instanceId`,或混合实例对象和 ID 的 iterable;返回实际删除数量。 |
|
|
250
|
+
| `removeBatch(url)` | 删除某个模型 URL 的完整 batch,释放对应内部 `InstancedMesh` 和 cloned materials。 |
|
|
251
|
+
| `clearBatches()` | 清空当前 layer 的所有模型 batch,并返回自身。 |
|
|
252
|
+
|
|
253
|
+
`addInstances(url, template, instances)` 会按模型 URL 复用或创建 batch,从 `template` 收集可 instancing 的可见 `Mesh`,克隆源材质,并为每个 batch 建立 instance matrix、颜色和透明度 attribute。没有动画、没有骨骼、没有 morph target,且每个多材质 `Mesh` 都有合法 `geometry.groups` 的模板会进入 instancing;不满足条件、模板没有可用 Mesh、`instances` 为空,或待加入实例的 `instanceId` 与当前 layer 内已有实例重复时会返回 `false`。
|
|
254
|
+
|
|
255
|
+
动态新增时,layer 会维护每个 URL batch 的 `capacity`。容量足够时,新增实例只会写入逻辑数组、注册 dirty callback 并标记下一帧同步;容量不足时才按 2 倍增长策略重建该 URL 下的内部 `InstancedMesh` 和 attribute buffer。运行时需要连续新增大量实例时,推荐先调用 `reserveBatch(url, template, expectedCount)`,再调用 `addInstances()`。删除实例不会立即收缩 GPU buffer;只有 batch 为空、调用 `removeBatch()` 或 `clearBatches()` 时才释放内部渲染资源。
|
|
256
|
+
|
|
257
|
+
外部代码不需要直接写内部 `InstancedMesh.instanceMatrix`。要移动、旋转或缩放实例时,直接修改对应 `InstanceObject` 的 transform 即可;transform 的世界矩阵发生变化时会自动标记实例 dirty,`ModelInstancedLayer` 会在下一次渲染前把世界矩阵同步到内部 instance matrix 并设置 `instanceMatrix.needsUpdate = true`。因此即使底层使用 WebGPU storage instanced buffer,公开更新方式仍然是面向实例对象:
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
const instance = layer.getInstanceById('FACILITY_001');
|
|
261
|
+
|
|
262
|
+
if (instance) {
|
|
263
|
+
instance.position.set(10, 0, 5);
|
|
264
|
+
viewer.render();
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
如果应用为静态大场景关闭了根 Scene 的 `matrixWorldAutoUpdate`,修改单个实例后还需显式调用 `instance.updateWorldMatrix(true, false)` 和 `viewer.invalidate()`;修改包含实例的父 Group 时调用 `group.updateWorldMatrix(true, true)`。这会执行 `InstanceObject` 的原生方法覆盖并触发 dirty callback,不需要直接调用 layer 内部同步方法。
|
|
248
269
|
|
|
249
|
-
|
|
270
|
+
删除 API 支持单个字符串 ID,不需要为了删除一个实例额外包一层数组。下面两个调用等价,都会按完整 ID 删除实例:
|
|
250
271
|
|
|
251
|
-
|
|
272
|
+
```typescript
|
|
273
|
+
layer.removeInstance('FACILITY_001');
|
|
274
|
+
layer.removeInstances('FACILITY_001');
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
渲染前,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`,并从实例对象字段附加 `instanceObject`、`instanceObjectId`、`instanceKind`、`instanceName` 和 `instanceIndex`,事件随后会按楼层引用对象继续冒泡。
|
|
252
278
|
|
|
253
279
|
```typescript
|
|
254
280
|
import { FacilityInstancedLayer, FacilityInstanceObject } from 'u-space/plugins/u-manager';
|
|
@@ -256,16 +282,21 @@ import { FacilityInstancedLayer, FacilityInstanceObject } from 'u-space/plugins/
|
|
|
256
282
|
const layer = new FacilityInstancedLayer();
|
|
257
283
|
const refs = facilityItems.map(() => new FacilityInstanceObject());
|
|
258
284
|
|
|
259
|
-
|
|
285
|
+
layer.reserveBatch('/models/camera.glb', templateObject, refs.length + 100);
|
|
286
|
+
|
|
287
|
+
if (layer.addInstances('/models/camera.glb', templateObject, refs)) {
|
|
260
288
|
semanticGroup.setFacilityLayer(layer);
|
|
261
289
|
}
|
|
290
|
+
|
|
291
|
+
// 运行时按完整字符串 ID 删除一个实例;返回实际删除数量。
|
|
292
|
+
layer.removeInstances('FACILITY_001');
|
|
262
293
|
```
|
|
263
294
|
|
|
264
|
-
如果只想整体隐藏或禁用 scene-level batch,可以设置 `semanticGroup.facilityLayer.visible = false`;如果只想控制单个设备,推荐从 `floor.getFacilityById(id)` 或 `viewer.objectManager.getById(id)` 取回 `FacilityInstanceObject` 后调用 `
|
|
295
|
+
如果只想整体隐藏或禁用 scene-level batch,可以设置 `semanticGroup.facilityLayer.visible = false`;如果只想控制单个设备,推荐从 `floor.getFacilityById(id)` 或 `viewer.objectManager.getById(id)` 取回 `FacilityInstanceObject` 后调用 `setInstanceVisible()` / `setInstanceOpacity()`。
|
|
265
296
|
|
|
266
|
-
### `
|
|
297
|
+
### `ModelInstancedLayer`
|
|
267
298
|
|
|
268
|
-
`
|
|
299
|
+
`ModelInstancedLayer` 是核心 `src/batches` 模块导出的公共类,也是 `SceneInstancedLayer` 和 `FacilityInstancedLayer` 的共享实现。它负责模板 mesh 收集、材质克隆、`InstancedMesh` 创建、运行时实例新增/删除、capacity 扩容、instance matrix/color/opacity buffer 同步、raycast hit remap、fallback 判定和可选相机实例裁剪。模板 mesh 会保持独立,避免跨楼壳合并后破坏透明排序和局部包围体;射线通过实例包围体后,三角形数量较高的静态模板几何会按需建立并复用 `three-mesh-bvh`,小几何或不受支持的几何布局自动保留 Three.js 默认路径。普通静态模型可以由调用方对明确的热点 root 使用 `enableMeshBVHRaycast(root)`,避免对整个大型场景自动建树造成首击卡顿。layer 内部只依赖 `InstanceObject` 的 `instanceId`、`instanceKind`、`instanceName` 独立字段,不再扫描 `instance.userData` 的 `facilityId`、`id` 或 `sid`。`getInstanceById()` / `removeInstances(id)` 会按需刷新 id 索引;新增实例时如果发现重复 `instanceId` 会拒绝加入,避免同一个 layer 内出现不确定查询结果。通用 API 见 [Batches API](./api-batches#modelinstancedlayer)。
|
|
269
300
|
|
|
270
301
|
### `FloorMesh`
|
|
271
302
|
|
|
@@ -291,7 +322,7 @@ if (layer.addSemanticBatch('/models/camera.glb', templateObject, refs)) {
|
|
|
291
322
|
| `getSemanticObjectAt(index)` | 获取合并几何实例对应的 `FloorSemanticInstanceObject` 轻量对象。 |
|
|
292
323
|
| `showAllFacilities()` | 显示当前楼层的全部设备对象。 |
|
|
293
324
|
| `hideAllFacilities()` | 隐藏当前楼层的全部设备对象。 |
|
|
294
|
-
| `addFacility(object, ids?)` |
|
|
325
|
+
| `addFacility(object, ids?)` | 把设备对象挂到楼层,并按对象 `instanceId` 和显式 `ids` 建立检索别名。不会从 `object.userData` 自动扫描 ID。 |
|
|
295
326
|
| `getFacilityById(id)` | 根据 `Facility.ID` 或别名获取设备对象。 |
|
|
296
327
|
| `getFacilityAt(index)` | 按楼层设备数组下标获取设备对象。 |
|
|
297
328
|
| `getFacilities()` | 获取当前楼层全部设备对象。 |
|
|
@@ -323,17 +354,23 @@ floor.addEventListener('click', async ({ event }) => {
|
|
|
323
354
|
|
|
324
355
|
if (intersect.semanticKind === 'Facilities') {
|
|
325
356
|
const facility = intersect.semanticObject ?? event.target;
|
|
326
|
-
const facilityBox = facility.getSemanticBoundingBox(box, true);
|
|
327
357
|
|
|
328
|
-
await viewer.controls.
|
|
358
|
+
await viewer.controls.flyToObject(facility, {
|
|
359
|
+
viewpoint: 'rightFrontTop',
|
|
360
|
+
padding: 0.2,
|
|
361
|
+
});
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (intersect.semanticObject) {
|
|
366
|
+
await viewer.controls.flyToObject(intersect.semanticObject, {
|
|
329
367
|
viewpoint: 'rightFrontTop',
|
|
330
368
|
padding: 0.2,
|
|
331
369
|
});
|
|
332
370
|
return;
|
|
333
371
|
}
|
|
334
372
|
|
|
335
|
-
const semanticBox = intersect.
|
|
336
|
-
?? currentTarget.getBoundingBoxAt(intersect.semanticIndex, box, true);
|
|
373
|
+
const semanticBox = currentTarget.getBoundingBoxAt(intersect.semanticIndex, box, true);
|
|
337
374
|
|
|
338
375
|
if (semanticBox) {
|
|
339
376
|
await viewer.controls.flyToBox(semanticBox, {
|
|
@@ -365,7 +402,11 @@ if (index !== undefined) {
|
|
|
365
402
|
|
|
366
403
|
从服务端场景包加载完整场景树(模型、组、基础形状),并自动将所有已加载对象按 `id` 和 `sid` 注册到 `viewer.objectManager`。
|
|
367
404
|
默认会尝试读取同一路径下的 `db/semantic_model.json`,如果场景树节点的 `id` 已存在于语义文件中,则跳过该节点,方便和 `SemanticLoader` 同时加载而不重复创建建筑或设备。
|
|
368
|
-
对语义去重后仍保留的 3D 节点,`SceneLoader` 默认会按模型 `path` 自动合并重复静态模型:同一 `path` 的多个实例只加载一次模板,并由 `SceneInstancedLayer` 通过共享的 `
|
|
405
|
+
对语义去重后仍保留的 3D 节点,`SceneLoader` 默认会按模型 `path` 自动合并重复静态模型:同一 `path` 的多个实例只加载一次模板,并由 `SceneInstancedLayer` 通过共享的 `ModelInstancedLayer` 批量渲染。原场景树中仍会保留每个 `id` / `sid` 对应的 `SceneInstanceObject` 逻辑对象,用于业务检索、显隐、颜色、透明度、包围盒和事件派发。
|
|
406
|
+
|
|
407
|
+
如果场景主要由大量普通 Mesh 组成,并且比默认的同 URL instancing 更重视 draw call 和可渲染节点数量,可以显式调用 `setEditableBatching()`。该模式会把兼容 Mesh 的变换烘焙到顶点,再按材质和 Geometry 布局合并为少量 `SceneEditableBatchLayer` Mesh;逻辑 `SceneInstanceObject` 仍留在原场景树中。它是静态 Geometry batching,不是 `InstancedMesh`、WebGPU indirect draw 或 compute rasterizer。
|
|
408
|
+
|
|
409
|
+
两种 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 仍然存在。
|
|
369
410
|
|
|
370
411
|
模板包含动画、骨骼、morph target、非法多材质 groups 或没有可实例化网格时,会自动回退为普通 `Model` 渲染;fallback `Model` 会挂在同一个 `SceneInstanceObject` 下,因此调用侧不需要额外配置。
|
|
371
412
|
|
|
@@ -375,8 +416,15 @@ import { SceneLoader } from 'u-space/plugins/u-manager';
|
|
|
375
416
|
const sceneLoader = new SceneLoader(viewer);
|
|
376
417
|
sceneLoader.setPath('./scenes/my-scene');
|
|
377
418
|
sceneLoader.setKey('YOUR_LICENSE_KEY'); // 官方授权场景必需
|
|
378
|
-
|
|
419
|
+
sceneLoader.setEditableBatching({
|
|
420
|
+
maxVerticesPerBatch: 1_500_000,
|
|
421
|
+
maxIndicesPerBatch: 4_500_000,
|
|
422
|
+
freezeAnimations: true,
|
|
423
|
+
});
|
|
424
|
+
const group = await sceneLoader.loadAsync(); // SceneGroup
|
|
379
425
|
viewer.scene.add(group);
|
|
426
|
+
|
|
427
|
+
const sceneLayer = group.getDefaultSceneLayer();
|
|
380
428
|
```
|
|
381
429
|
|
|
382
430
|
**方法:**
|
|
@@ -384,64 +432,157 @@ viewer.scene.add(group);
|
|
|
384
432
|
| 方法 | 说明 |
|
|
385
433
|
| :------------------ | :------------------------------------------------------------------- |
|
|
386
434
|
| `setKey(key)` | 设置授权场景的 RSA 解密密钥。 |
|
|
387
|
-
| `
|
|
435
|
+
| `setEditableBatching(options?)` | 开启可编辑静态 Geometry batching。传 `true` 使用默认选项,传 `false` 关闭;默认关闭。 |
|
|
436
|
+
| `loadAsync()` | 加载并解析场景,返回去除语义重复节点后的 `SceneGroup`。 |
|
|
388
437
|
| `clearCache()` | 从 `objectManager` 中移除此加载器注册的所有 ID。 |
|
|
389
438
|
| `dispose()` | 清除缓存并释放水印叠加层。 |
|
|
390
439
|
|
|
440
|
+
### 可编辑静态合批:`setEditableBatching()`
|
|
441
|
+
|
|
442
|
+
`setEditableBatching(options)` 是面向大型、主要静态但仍需按对象操作的场景的 opt-in 路径。开启后,`SceneLoader` 会把所有保留的 3D 节点创建为 `SceneInstanceObject`,不再只处理重复 `path`;每个唯一模型 URL 仍只加载一次模板。
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
import {
|
|
446
|
+
SceneLoader,
|
|
447
|
+
type SceneEditableBatchOptions,
|
|
448
|
+
type SceneEditableBatchStats,
|
|
449
|
+
} from 'u-space/plugins/u-manager';
|
|
450
|
+
|
|
451
|
+
const options: SceneEditableBatchOptions = {
|
|
452
|
+
maxVerticesPerBatch: 1_500_000,
|
|
453
|
+
maxIndicesPerBatch: 4_500_000,
|
|
454
|
+
freezeAnimations: true,
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const sceneLoader = new SceneLoader(viewer).setEditableBatching(options);
|
|
458
|
+
const sceneGroup = await sceneLoader.loadAsync();
|
|
459
|
+
viewer.scene.add(sceneGroup);
|
|
460
|
+
|
|
461
|
+
const stats = sceneGroup.userData.editableBatch as SceneEditableBatchStats | undefined;
|
|
462
|
+
console.table(stats);
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
**配置:**
|
|
466
|
+
|
|
467
|
+
| 字段 | 默认值 | 说明 |
|
|
468
|
+
| :--- | :--- | :--- |
|
|
469
|
+
| `maxVerticesPerBatch` | `1_500_000` | 单个 merged Geometry 的最大顶点数;超过后开始新 chunk。 |
|
|
470
|
+
| `maxIndicesPerBatch` | `4_500_000` | 单个 merged Geometry 的最大索引数;超过后开始新 chunk。 |
|
|
471
|
+
| `freezeAnimations` | `false` | `false` 时含 animation clip 或 mixer 的模板整体 fallback;`true` 时烘焙当前姿态,后续播放动画前必须先 materialize。SkinnedMesh 和 morph target 始终 fallback。 |
|
|
472
|
+
|
|
473
|
+
#### 合并规则
|
|
474
|
+
|
|
475
|
+
每个候选 Mesh 会生成稳定的 material key 和 geometry-layout key。只有以下状态完全兼容的 Mesh 才进入同一组:
|
|
476
|
+
|
|
477
|
+
- 材质类型、颜色/粗糙度等序列化参数,以及贴图源和 sampler/transform 配置一致。
|
|
478
|
+
- Geometry 的 indexed/non-indexed 类型、index 数组类型、attribute 名称、`itemSize`、`normalized`、数组类型和 `gpuType` 一致。Geometry 内容和顶点数量可以不同。
|
|
479
|
+
- `castShadow`、`receiveShadow` 和 `renderOrder` 一致。
|
|
480
|
+
|
|
481
|
+
合并前会为每个源 Mesh 克隆 Geometry,并应用以下矩阵:
|
|
482
|
+
|
|
483
|
+
```text
|
|
484
|
+
inverse(batchLayer.matrixWorld) × instance.matrixWorld × meshLocalMatrix
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
随后为全部顶点写入 `batchObjectIndex` attribute,再通过 `mergeGeometries()` 生成 merged Mesh。批次达到顶点或索引上限时自动切分,因此每个兼容组可能生成多个 draw call。
|
|
488
|
+
|
|
489
|
+
该方案会把重复模板的顶点展开到 merged Geometry,通常以更高的 GPU 顶点内存换取更少的 draw calls 和可渲染 Mesh 遍历;重复率极高、几何较大的同模板对象仍可能更适合原生 `SceneInstancedLayer`。
|
|
490
|
+
|
|
491
|
+
#### 对象级状态与 materialize
|
|
492
|
+
|
|
493
|
+
合批后,每个逻辑对象在 Float `DataTexture` 中占一个 RGBA texel,batch NodeMaterial 通过 TSL `colorNode` 和独立布尔 `maskNode` 读取 `batchObjectIndex` 对应的颜色与显隐状态;不透明 batch 保留源材质原有的 opacity 路径。因此以下操作只会把对象标记为 dirty,并立即更新 CPU 状态纹理数据,GPU 在下一帧上传,不会重新合并 Geometry:
|
|
494
|
+
|
|
495
|
+
- `setInstanceVisible()` 和父级显隐变化。
|
|
496
|
+
- `setInstanceColor()` / `resetInstanceColor()`。
|
|
497
|
+
- 不引入半透明的 `setInstanceHighlight()` / `clearInstanceHighlight()`。
|
|
498
|
+
|
|
499
|
+
如果对象的世界 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,需要重新加载场景。也可以提前显式调用:
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
const object = viewer.objectManager.getById<SceneInstanceObject>('SCENE_NODE_ID');
|
|
503
|
+
const model = object?.materialize();
|
|
504
|
+
|
|
505
|
+
if (model) {
|
|
506
|
+
object.position.x += 2;
|
|
507
|
+
object.setInstanceOpacity(0.5);
|
|
508
|
+
await viewer.render();
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
#### Fallback 与透明材质
|
|
513
|
+
|
|
514
|
+
透明材质、`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`。
|
|
515
|
+
|
|
516
|
+
这条边界用于保留透明排序、蒙皮/变形和自定义 shader 行为。不要通过强制改成不透明材质来提高合批率。
|
|
517
|
+
|
|
518
|
+
#### Raycast 与统计
|
|
519
|
+
|
|
520
|
+
merged Mesh 首次实际拾取时会按需建立 `three-mesh-bvh`。命中结果通过 `faceIndex → vertexIndex → batchObjectIndex` 映射回原始 `SceneInstanceObject`,因此 `intersect.object` 仍是业务逻辑对象;已经 materialize 的对象不会在旧烘焙位置继续产生命中。
|
|
521
|
+
|
|
522
|
+
`SceneEditableBatchStats` 写入 `sceneGroup.userData.editableBatch`:
|
|
523
|
+
|
|
524
|
+
| 字段 | 说明 |
|
|
525
|
+
| :--- | :--- |
|
|
526
|
+
| `instances` | 进入 editable batch 状态表的逻辑对象数。 |
|
|
527
|
+
| `sourceMeshes` | 合并前的兼容源 Mesh 数。 |
|
|
528
|
+
| `batches` | 最终 merged Mesh 数。 |
|
|
529
|
+
| `drawCallsSaved` | `sourceMeshes - batches` 的理论 draw-call 减少量,不包含多 pass 放大。 |
|
|
530
|
+
| `vertices` / `indices` | 烘焙后的总顶点数和索引数,用于评估显存代价。 |
|
|
531
|
+
| `unsupportedInstances` | 需要完整或部分 fallback 的逻辑对象数。 |
|
|
532
|
+
| `unsupportedByReason` | 按 `transparent`、`animation`、`skinned`、`morph`、`material-array`、`geometry-layout`、`negative-scale` 等原因统计。 |
|
|
533
|
+
|
|
534
|
+
WebGPU 首次创建大量 batch material pipeline 时可能出现短暂的 shader 编译开销。如果产品不希望首个可见帧展示未完成的材质,可在需求渲染模式下暂时停止相机控制产生的 render invalidation,完成 `compileAsync()` 后再提交首帧;`examples/test_umanager2.html` 展示了这一流程。
|
|
535
|
+
|
|
536
|
+
### `SceneGroup`
|
|
537
|
+
|
|
538
|
+
`SceneGroup` 继承自 `BaseGroup`,是 `SceneLoader.loadAsync()` 的返回根组。它保留 `tree_models.json` 解析后的原始父子层级,并把重复静态模型的默认合批渲染层挂在 `sceneLayer`,方便外部直接访问,而不需要遍历 `children`。
|
|
539
|
+
|
|
540
|
+
| 属性 / 方法 | 说明 |
|
|
541
|
+
| :---------- | :--- |
|
|
542
|
+
| `isSceneGroup` | 固定为 `true`,用于判断对象是否为 `SceneLoader` 根组。 |
|
|
543
|
+
| `sceneLayer` | 默认 path instancing 下为 `SceneInstancedLayer`;启用 editable batching 后为 `SceneEditableBatchLayer`;没有可合批模型时为 `null`。 |
|
|
544
|
+
| `setSceneLayer(layer)` | 设置 `SceneInstancedLayer` 或 `SceneEditableBatchLayer`;传入 `null` 会移除已有 layer。 |
|
|
545
|
+
| `getDefaultSceneLayer()` | 返回当前默认场景渲染层;没有可合批模型时返回 `null`。 |
|
|
546
|
+
|
|
391
547
|
### `SceneInstanceObject`
|
|
392
548
|
|
|
393
|
-
`SceneInstanceObject` 继承自统一的 `
|
|
549
|
+
`SceneInstanceObject` 继承自统一的 `InstanceObject`,表示一个由 `SceneInstancedLayer`、`SceneEditableBatchLayer` 或 fallback 普通模型渲染的场景模型引用。它会保留在原场景树层级中,并注册到 `viewer.objectManager`,因此 `getById()`、`show()` / `hide()`、`isolate()`、`showAll()`、`setOpacity()` 和 `getBoundingBox()` 可以继续按单个模型 ID 使用。
|
|
550
|
+
|
|
551
|
+
完整的继承字段、方法、颜色模式、包围盒和 dirty 同步语义见 [核心 `InstanceObject` API](./api-objects.md#instanceobject)。
|
|
394
552
|
|
|
395
|
-
`SceneInstanceObject`
|
|
553
|
+
`SceneInstanceObject` 本身只增加场景模型标记:`isSceneInstanceObject = true`、`type = 'SceneInstanceObject'`、`instanceKind = 'SceneInstances'`。加载器会把场景树节点的 `id` 和 `sid` 都注册到 `viewer.objectManager`,因此同一个对象可以通过任一 ID 取回。对重复模型的 instanced 路径,`instanceId` 默认等于场景树节点 `id`,`instanceName` 默认等于节点名称;`userData.instanced = true`,并保留 `modelPath` 和 `modelUrl` 等原始加载元数据。如果模板不支持 instancing,会退回普通 `Model`,但仍挂在同一个 `SceneInstanceObject` 下,对外 API 不变。
|
|
396
554
|
|
|
397
555
|
**识别字段:**
|
|
398
556
|
|
|
399
557
|
| 字段 | 说明 |
|
|
400
558
|
| :--- | :--- |
|
|
401
559
|
| `isSceneInstanceObject` | 固定为 `true`,用于判断对象是否来自 `SceneLoader` 的场景实例。 |
|
|
402
|
-
| `isSemanticInstanceObject` | 固定为 `true`,表示它支持统一语义实例 API。 |
|
|
403
560
|
| `type` | 固定为 `SceneInstanceObject`。 |
|
|
561
|
+
| `instanceId` | 实例独立 ID,默认等于场景树节点 `id`;layer 的按 ID 查询、删除和 raycast remap 都使用该字段。 |
|
|
562
|
+
| `instanceKind` | 实例类型,场景实例为 `'SceneInstances'`。 |
|
|
563
|
+
| `instanceName` | 实例显示名称,默认等于场景树节点名称。 |
|
|
404
564
|
| `userData.id` / `userData.sid` | 原始场景树节点 ID;两个 ID 都会注册到 `viewer.objectManager`。 |
|
|
405
|
-
| `userData.
|
|
406
|
-
| `userData.semanticKind` | 固定为 `SceneInstances`。 |
|
|
407
|
-
| `userData.semanticName` | 原始场景树节点名称。 |
|
|
408
|
-
| `userData.instanced` | `true` 表示实际渲染来自 `SceneInstancedLayer`;`false` 表示 fallback `Model` 挂在当前对象下。 |
|
|
565
|
+
| `userData.instanced` | 加载时选择 batch 路径的元数据。editable batch 对象 materialize 后该字段不作为实时状态;使用 `getInstanceRenderObject()` 是否非空判断当前是否已有普通渲染对象。 |
|
|
409
566
|
| `userData.modelPath` / `userData.modelUrl` | instanced 场景实例对应的模型相对路径和完整 URL。 |
|
|
410
567
|
|
|
411
568
|
**方法:**
|
|
412
569
|
|
|
413
570
|
| 方法 | 说明 |
|
|
414
571
|
| :--- | :--- |
|
|
415
|
-
|
|
|
416
|
-
| `
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
| `resetSemanticColor()` | 清除颜色 override,恢复使用源模型颜色;不会清除透明度 override。 |
|
|
422
|
-
| `setSemanticOpacity(opacity)` | 设置单个实例透明度,值会限制在 `0..1`。instanced 路径写入 per-instance opacity;fallback 路径会按原始材质透明度相乘。 |
|
|
423
|
-
| `getSemanticColor(target?)` | 获取当前生效颜色。存在颜色或高亮 override 时返回该颜色,否则返回白色。传入 `Color` 可复用对象。 |
|
|
424
|
-
| `hasSemanticColor()` | 返回当前是否存在颜色或高亮颜色 override。 |
|
|
425
|
-
| `getSemanticOpacity()` | 获取当前生效透明度;高亮存在时返回高亮透明度,否则返回基础透明度。 |
|
|
426
|
-
| `setSemanticHighlight(color, opacity, overwrite?)` | 设置临时高亮样式。`overwrite = false` 时按 tint 模式与原色混合,`true` 时直接替换颜色;高亮会覆盖基础颜色/透明度直到调用 `clearSemanticHighlight()`。 |
|
|
427
|
-
| `clearSemanticHighlight()` | 清除临时高亮,并恢复到基础颜色/透明度状态。 |
|
|
428
|
-
| `getSemanticColorMode()` | 返回当前颜色模式:`SEMANTIC_COLOR_MODE_NONE`、`SEMANTIC_COLOR_MODE_OVERRIDE` 或 `SEMANTIC_COLOR_MODE_TINT`。 |
|
|
429
|
-
| `onSemanticRenderDirty(callback)` | 监听实例渲染状态变化,返回取消监听函数。实例显隐、颜色、透明度或世界矩阵变化时会触发。 |
|
|
430
|
-
| `markSemanticRenderDirty()` | 手动标记实例渲染状态已变化,通知所属 batch 在下一帧同步 instance buffer。 |
|
|
431
|
-
|
|
432
|
-
`MaterialEffects.highlightColor()` / `removeHighlightColor()` 会识别 `SceneInstanceObject` 并调用 `setSemanticHighlight()` / `clearSemanticHighlight()`,不会直接修改共享 batch 材质。`ObjectManager.setOpacity()`、`ObjectManager.hide()`、`ObjectManager.show()` 也会通过统一 API 作用到单个实例。
|
|
433
|
-
|
|
434
|
-
加载完成后可以按场景树节点 `id` 从 `objectManager` 取回 `SceneInstanceObject`,再用 `getSemanticBoundingBox(box, true)` 取得世界包围盒并传给 `viewer.controls.flyToBox()`。`SceneInstanceObject` 内部带有不可见的本地 bounds,因此不需要加载真实 Mesh 副本也能计算飞行包围盒。
|
|
572
|
+
| 继承的实例 API | 完整定义见 [核心 `InstanceObject` API](./api-objects.md#instanceobject);包含身份、bounds、fallback render object、显隐、颜色、透明度、高亮和 dirty callback。 |
|
|
573
|
+
| `materialize()` | 如果对象由 `SceneEditableBatchLayer` 渲染,clone 原始完整模板并将当前对象从 merged/fallback batch 中移出;默认 path instancing 或普通 fallback 下返回已有 render object 或 `null`。 |
|
|
574
|
+
|
|
575
|
+
`MaterialEffects.highlightColor()` / `removeHighlightColor()` 会识别 `SceneInstanceObject` 并调用 `setInstanceHighlight()` / `clearInstanceHighlight()`,不会直接修改共享 batch 材质。`ObjectManager.setOpacity()`、`ObjectManager.hide()`、`ObjectManager.show()` 也会通过统一 API 作用到单个实例。
|
|
576
|
+
|
|
577
|
+
加载完成后可以按场景树节点 `id` 从 `objectManager` 取回 `SceneInstanceObject`,再直接传给 `viewer.controls.flyToObject()`。`SceneInstanceObject` 内部带有本地 `boundingBox` 缓存能力,因此不需要加载真实 Mesh 副本也能计算飞行包围盒。
|
|
435
578
|
|
|
436
579
|
```typescript
|
|
437
|
-
import { Box3 } from 'three/webgpu';
|
|
438
580
|
import { SceneInstanceObject } from 'u-space/plugins/u-manager';
|
|
439
581
|
|
|
440
582
|
const object = viewer.objectManager.getById<SceneInstanceObject>('SCENE_NODE_ID');
|
|
441
583
|
|
|
442
584
|
if (object?.isSceneInstanceObject) {
|
|
443
|
-
|
|
444
|
-
await viewer.controls.flyToBox(box, {
|
|
585
|
+
await viewer.controls.flyToObject(object, {
|
|
445
586
|
viewpoint: 'current',
|
|
446
587
|
padding: 0.2,
|
|
447
588
|
enableTransition: true,
|
|
@@ -453,13 +594,13 @@ if (object?.isSceneInstanceObject) {
|
|
|
453
594
|
|
|
454
595
|
```typescript
|
|
455
596
|
object
|
|
456
|
-
?.
|
|
457
|
-
.
|
|
458
|
-
.
|
|
597
|
+
?.setInstanceColor('#29ccff')
|
|
598
|
+
.setInstanceOpacity(0.55)
|
|
599
|
+
.setInstanceVisible(true);
|
|
459
600
|
|
|
460
|
-
object?.
|
|
461
|
-
object?.
|
|
462
|
-
object?.
|
|
601
|
+
object?.setInstanceHighlight('#ffb703', 0.75, true);
|
|
602
|
+
object?.clearInstanceHighlight();
|
|
603
|
+
object?.resetInstanceColor();
|
|
463
604
|
```
|
|
464
605
|
|
|
465
606
|
如果需要自己控制包围盒,也可以取世界包围盒后调用 `flyToBox()`:
|
|
@@ -467,15 +608,23 @@ object?.resetSemanticColor();
|
|
|
467
608
|
```typescript
|
|
468
609
|
import { Box3 } from 'three/webgpu';
|
|
469
610
|
|
|
470
|
-
const box = object.
|
|
611
|
+
const box = object.getInstanceBoundingBox(new Box3(), true);
|
|
471
612
|
await viewer.controls.flyToBox(box, { viewpoint: 'frontTop', padding: 0.2 });
|
|
472
613
|
```
|
|
473
614
|
|
|
474
|
-
射线检测命中批处理实例时,`intersect.object` 会被改写为对应的 `SceneInstanceObject`,并附加 `
|
|
615
|
+
射线检测命中批处理实例时,`intersect.object` 会被改写为对应的 `SceneInstanceObject`,并附加 `instanceObject`、`instanceObjectId`、`instanceKind: 'SceneInstances'`、`instanceName` 和 `instanceIndex`。Three.js 原生的 `intersect.instanceId` 仍保留为内部 `InstancedMesh` 的数字实例下标,事件会按逻辑对象所在的原场景树继续冒泡。
|
|
475
616
|
|
|
476
617
|
### `SceneInstancedLayer`
|
|
477
618
|
|
|
478
|
-
`SceneInstancedLayer` 是 `SceneLoader` 内部使用的批量渲染层,默认 `name = 'SceneInstances'`。它继承自 `
|
|
619
|
+
`SceneInstancedLayer` 是 `SceneLoader` 内部使用的批量渲染层,默认 `name = 'SceneInstances'`。它继承自 `ModelInstancedLayer`,每个重复模型 URL 会生成一个 batch,batch 内按模板的可实例化 Mesh 创建 `InstancedMesh`。渲染前只在实例矩阵、显隐、颜色或透明度变化时同步 instance buffer;普通相机移动不会重新上传所有实例数据。
|
|
620
|
+
|
|
621
|
+
### `SceneEditableBatchLayer`
|
|
622
|
+
|
|
623
|
+
`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` 覆盖。
|
|
624
|
+
|
|
625
|
+
它不继承 `SceneInstancedLayer`。开启 editable batching 且生成 merged Mesh 时,`SceneEditableBatchLayer` 是 `SceneGroup.sceneLayer` 的主层;不兼容的重复 fallback 由同级、名称为 `SceneEditableBatchFallback` 的 `SceneInstancedLayer` 辅助渲染,单例或仍不支持 instancing 的对象使用挂在 `SceneInstanceObject` 下的普通 `Model`。没有 merged Mesh 时主 layer 为 `null`,辅助 fallback 不会被 `getDefaultSceneLayer()` 返回。
|
|
626
|
+
|
|
627
|
+
通常由 `SceneLoader` 管理,不需要业务代码直接调用 scene adapter 的 `addTemplate()` / `build()`,也不需要自行处理 materialize 后的 fallback 移除。公开的 `stats` 和 `options` 可用于诊断;materialize 通知使用继承的类型化 `materialize` 事件,释放独立创建的 layer 时应调用 `dispose()`。如果要在其他插件或应用中直接复用同一机制,应使用核心 [EditableGeometryBatchLayer](./api-batches#editablegeometrybatchlayer) 的 `addSource()` API。
|
|
479
628
|
|
|
480
629
|
## `TopologiesLoader` / `TopologyParser`
|
|
481
630
|
|