three-base 2.0.172 → 2.0.180

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.
Files changed (127) hide show
  1. package/FPScontrol.d.ts +15 -24
  2. package/FPScontrol.js +63 -87
  3. package/FPScontrol.ts +64 -88
  4. package/Ground1/Ground1.d.ts +4 -2
  5. package/Ground1/Ground1.js +77 -57
  6. package/Ground1/Ground1.ts +77 -57
  7. package/Mirror.js +0 -4
  8. package/Mirror.ts +0 -4
  9. package/MktMesh/MktMesh.d.ts +3 -2
  10. package/MktMesh/MktMesh.js +27 -8
  11. package/MktMesh/MktMesh.ts +24 -8
  12. package/MktMesh/boxLine2.d.ts +42 -0
  13. package/MktMesh/boxLine2.js +185 -0
  14. package/MktMesh/boxLine2.ts +222 -0
  15. package/MktMesh/imagePlane.d.ts +60 -0
  16. package/MktMesh/imagePlane.js +158 -0
  17. package/MktMesh/imagePlane.ts +203 -0
  18. package/MktMesh/imageTileLoader.d.ts +19 -0
  19. package/MktMesh/imageTileLoader.js +55 -0
  20. package/MktMesh/imageTileLoader.ts +64 -0
  21. package/MktMesh/mktMappingShader.d.ts +6 -5
  22. package/MktMesh/mktMappingShader.js +26 -19
  23. package/MktMesh/mktMappingShader.ts +28 -19
  24. package/MktMesh/mktShader.d.ts +127 -58
  25. package/MktMesh/mktShader.js +192 -117
  26. package/MktMesh/mktShader.ts +251 -127
  27. package/MktMesh/modelSlot.d.ts +123 -0
  28. package/MktMesh/modelSlot.js +339 -0
  29. package/MktMesh/modelSlot.ts +383 -0
  30. package/MktMesh/panoSlotRoster.d.ts +2 -2
  31. package/MktMesh/panoSlotRoster.js +9 -7
  32. package/MktMesh/panoSlotRoster.ts +9 -7
  33. package/MktMesh/panoTileStitcher.d.ts +20 -13
  34. package/MktMesh/panoTileStitcher.js +97 -76
  35. package/MktMesh/panoTileStitcher.ts +119 -90
  36. package/PipeMesh/PipeJoint.d.ts +22 -0
  37. package/PipeMesh/PipeJoint.js +33 -0
  38. package/PipeMesh/PipeJoint.ts +38 -0
  39. package/PipeMesh/PipeMesh.d.ts +94 -22
  40. package/PipeMesh/PipeMesh.js +192 -53
  41. package/PipeMesh/PipeMesh.ts +223 -76
  42. package/PipeMesh/PipeStraight.d.ts +19 -0
  43. package/PipeMesh/PipeStraight.js +34 -0
  44. package/PipeMesh/PipeStraight.ts +45 -0
  45. package/PipeMesh/axisSnap.d.ts +52 -0
  46. package/PipeMesh/axisSnap.js +52 -0
  47. package/PipeMesh/axisSnap.ts +93 -0
  48. package/PipeMesh/directRoute.d.ts +85 -0
  49. package/PipeMesh/directRoute.js +126 -0
  50. package/PipeMesh/directRoute.ts +190 -0
  51. package/PipeMesh/index.d.ts +6 -0
  52. package/PipeMesh/index.js +6 -0
  53. package/PipeMesh/index.ts +7 -1
  54. package/PipeMesh/pipeMaterial.d.ts +16 -0
  55. package/PipeMesh/pipeMaterial.js +23 -0
  56. package/PipeMesh/pipeMaterial.ts +26 -0
  57. package/ThreeBase.d.ts +1 -39
  58. package/ThreeBase.js +38 -147
  59. package/ThreeBase.ts +1176 -1302
  60. package/instanced/InstancedBatch.d.ts +14 -1
  61. package/instanced/InstancedBatch.js +53 -3
  62. package/instanced/InstancedBatch.ts +62 -3
  63. package/instanced/InstancedChild.d.ts +24 -0
  64. package/instanced/InstancedChild.js +40 -0
  65. package/instanced/InstancedChild.ts +41 -0
  66. package/instanced/InstancedGroup.d.ts +48 -0
  67. package/instanced/InstancedGroup.js +45 -1
  68. package/instanced/InstancedGroup.ts +85 -0
  69. package/package.json +22 -7
  70. package/pass/debugDepthState.d.ts +16 -8
  71. package/pass/debugDepthState.js +4 -0
  72. package/pass/debugDepthState.ts +27 -9
  73. package/pass/glow_pass.js +11 -94
  74. package/pass/glow_pass.ts +11 -97
  75. package/pass/ssao_pass.d.ts +9 -23
  76. package/pass/ssao_pass.js +41 -128
  77. package/pass/ssao_pass.ts +36 -166
  78. package/pass/stroke_pass.js +3 -0
  79. package/pass/stroke_pass.ts +4 -0
  80. package/shaders/debugDepthCompareShader.d.ts +1 -1
  81. package/shaders/debugDepthCompareShader.js +1 -1
  82. package/shaders/debugDepthCompareShader.ts +1 -1
  83. package/shaders/debugDepthJudgeShader.d.ts +1 -1
  84. package/shaders/debugDepthJudgeShader.js +1 -1
  85. package/shaders/debugDepthJudgeShader.ts +1 -1
  86. package/PipeMesh/PathGeometry.d.ts +0 -44
  87. package/PipeMesh/PathGeometry.js +0 -307
  88. package/PipeMesh/PathGeometry.ts +0 -343
  89. package/PipeMesh/PathPoint.d.ts +0 -17
  90. package/PipeMesh/PathPoint.js +0 -31
  91. package/PipeMesh/PathPoint.ts +0 -36
  92. package/PipeMesh/PathPointList.d.ts +0 -31
  93. package/PipeMesh/PathPointList.js +0 -209
  94. package/PipeMesh/PathPointList.ts +0 -227
  95. package/PipeMesh/PathTubeGeometry.d.ts +0 -25
  96. package/PipeMesh/PathTubeGeometry.js +0 -130
  97. package/PipeMesh/PathTubeGeometry.ts +0 -146
  98. package/WaterFlow/FloatingDuck.d.ts +0 -88
  99. package/WaterFlow/FloatingDuck.js +0 -307
  100. package/WaterFlow/FloatingDuck.ts +0 -445
  101. package/WaterFlow/GPUFluidSimulation.d.ts +0 -56
  102. package/WaterFlow/GPUFluidSimulation.js +0 -785
  103. package/WaterFlow/GPUFluidSimulation.ts +0 -827
  104. package/WaterFlow/ReservoirSystem.d.ts +0 -106
  105. package/WaterFlow/ReservoirSystem.js +0 -621
  106. package/WaterFlow/ReservoirSystem.ts +0 -754
  107. package/WaterFlow/SPHField.d.ts +0 -35
  108. package/WaterFlow/SPHField.js +0 -204
  109. package/WaterFlow/SPHField.ts +0 -228
  110. package/WaterFlow/ScreenSpaceFluidPass.d.ts +0 -69
  111. package/WaterFlow/ScreenSpaceFluidPass.js +0 -736
  112. package/WaterFlow/ScreenSpaceFluidPass.ts +0 -789
  113. package/WaterFlow/ShallowWaterSurface.d.ts +0 -45
  114. package/WaterFlow/ShallowWaterSurface.js +0 -441
  115. package/WaterFlow/ShallowWaterSurface.ts +0 -510
  116. package/WaterFlow/WaterFlow.d.ts +0 -87
  117. package/WaterFlow/WaterFlow.js +0 -171
  118. package/WaterFlow/WaterFlow.ts +0 -219
  119. package/WaterFlow/types.d.ts +0 -161
  120. package/WaterFlow/types.js +0 -1
  121. package/WaterFlow/types.ts +0 -189
  122. package/WaterFlow/waterFlowPass.d.ts +0 -3
  123. package/WaterFlow/waterFlowPass.js +0 -20
  124. package/WaterFlow/waterFlowPass.ts +0 -40
  125. package/shaders/debugExcludedShader.d.ts +0 -15
  126. package/shaders/debugExcludedShader.js +0 -48
  127. package/shaders/debugExcludedShader.ts +0 -50
@@ -27,6 +27,13 @@ export default class InstancedBatch {
27
27
  private _currentClip?;
28
28
  /** 首例实例是否声明碰撞(构造时快照,buildMeshes 用它给 InstancedMesh 打标记) */
29
29
  private _sampleFpsCollision;
30
+ /**
31
+ * 首例实例所在类是否声明 `static panoIndexAttribute`(构造时快照,与 _sampleFpsCollision 同一套路):
32
+ * true ⇒ buildMeshes 给本 batch 克隆一份渲染用几何并挂上逐实例 `aPanoIndex`(全景序号直取着色器读它)。
33
+ */
34
+ private _samplePanoIndexAttr;
35
+ /** 本 batch 自己挂上去的逐实例 `aPanoIndex` 缓冲(与 _meshes 顺序对齐;未启用则为空数组) */
36
+ private _panoAttrs;
30
37
  /** 当前正在播放的 AnimationAction(切换时先 stop 旧的) */
31
38
  private _currentAction?;
32
39
  /** 非骨骼动画:需每帧烘焙节点变换的几何列表(与 _meshes 顺序对齐) */
@@ -43,7 +50,7 @@ export default class InstancedBatch {
43
50
  /** 根据当前首批实例的 fpsCollision 标记,把真实 InstancedMesh 的 userData.fpsCollision 同步过去,
44
51
  * 并在开启时通知所有 FPScontrol 重建碰撞列表。碰撞以整个 batch 为单位开关。 */
45
52
  private _applyCollisionFlag;
46
- /** 把单个实例的 matrix + color 写入所有 submesh 的对应槽位 */
53
+ /** 把单个实例的 matrix + color +(若启用)全景序号写入所有 submesh 的对应槽位 */
47
54
  private writeInstance;
48
55
  /** 事件路由:给定 InstancedMesh 与 instanceId,返回对应 InstancedChild */
49
56
  resolveInstance(mesh: InstancedMesh, instanceId: number): InstancedChild | null;
@@ -53,5 +60,11 @@ export default class InstancedBatch {
53
60
  hasCursor(): boolean;
54
61
  /** 拾取用 InstancedMesh 列表 */
55
62
  get pickables(): InstancedMesh[];
63
+ /** 只读快照(拓扑图面板用):本 batch 当前持有的逻辑实例数 */
64
+ get childCount(): number;
65
+ /** 只读快照:实例缓冲容量(不够时按 step 扩容,从不缩容) */
66
+ get capacity(): number;
67
+ /** 只读快照:本 batch 的逻辑实例列表(只给引用,调用方不得增删) */
68
+ get childrenView(): readonly InstancedChild[];
56
69
  dispose(): void;
57
70
  }
@@ -111,6 +111,13 @@ export default class InstancedBatch {
111
111
  _currentClip;
112
112
  /** 首例实例是否声明碰撞(构造时快照,buildMeshes 用它给 InstancedMesh 打标记) */
113
113
  _sampleFpsCollision = false;
114
+ /**
115
+ * 首例实例所在类是否声明 `static panoIndexAttribute`(构造时快照,与 _sampleFpsCollision 同一套路):
116
+ * true ⇒ buildMeshes 给本 batch 克隆一份渲染用几何并挂上逐实例 `aPanoIndex`(全景序号直取着色器读它)。
117
+ */
118
+ _samplePanoIndexAttr = false;
119
+ /** 本 batch 自己挂上去的逐实例 `aPanoIndex` 缓冲(与 _meshes 顺序对齐;未启用则为空数组) */
120
+ _panoAttrs = [];
114
121
  /** 当前正在播放的 AnimationAction(切换时先 stop 旧的) */
115
122
  _currentAction;
116
123
  /** 非骨骼动画:需每帧烘焙节点变换的几何列表(与 _meshes 顺序对齐) */
@@ -123,6 +130,7 @@ export default class InstancedBatch {
123
130
  // 因 key 已含动画名,相同 glb+动画的实例都进同一个常驻 batch
124
131
  this._clipName = sample.getEffectiveAnimation();
125
132
  this._sampleFpsCollision = !!sample.fpsCollision;
133
+ this._samplePanoIndexAttr = !!sample.constructor.panoIndexAttribute;
126
134
  this._loading = loadTemplate(sample).then((t) => {
127
135
  const ctor = sample.constructor;
128
136
  // GLB 模板就绪后,交给子类按需修正材质(如瓶子 metalness 修正)
@@ -140,9 +148,22 @@ export default class InstancedBatch {
140
148
  return;
141
149
  }
142
150
  for (const sub of this.template.submeshes) {
143
- const geom = sub.skinned && this.template.sharedGeometry
151
+ let geom = sub.skinned && this.template.sharedGeometry
144
152
  ? this.template.sharedGeometry
145
153
  : sub.geometry;
154
+ // 逐实例全景序号:本 batch 用**自己克隆的一份几何**挂 `aPanoIndex`(InstancedBufferAttribute
155
+ // 只能挂在 geometry 上,而 static geometry 跨 batch 共享 ⇒ 挂原几何会被别的 batch 覆盖,
156
+ // 详见 InstancedChild.panoIndexAttribute 的注释)。
157
+ // ⚠️ 骨骼蒙皮那条路不能克隆:sharedGeometry 每帧由 bakeSkinning 从 sourceGeometry 烤出来,
158
+ // 克隆一份就再也收不到烘焙结果(实例全部定格在绑定姿态)⇒ 蒙皮批次不加此属性。
159
+ if (this._samplePanoIndexAttr && !sub.skinned) {
160
+ const instGeom = geom.clone();
161
+ const panoAttr = new InstancedBufferAttribute(new Float32Array(this._capacity).fill(-1), 1);
162
+ panoAttr.setUsage(DynamicDrawUsage);
163
+ instGeom.setAttribute("aPanoIndex", panoAttr);
164
+ this._panoAttrs.push(panoAttr);
165
+ geom = instGeom;
166
+ }
146
167
  const im = new InstancedMesh(geom, sub.material, this._capacity);
147
168
  im.frustumCulled = false;
148
169
  im.instanceMatrix.setUsage(DynamicDrawUsage);
@@ -250,7 +271,8 @@ export default class InstancedBatch {
250
271
  // 容量按 100 为一阶阶梯增长、只增不降:1 个对象→100 矩阵,101 个→200 矩阵,201 个→300 ...
251
272
  // 避免每超出即整段 realloc,也避免从 200 回降到 100 造成反复分配/收缩。
252
273
  const newCap = Math.ceil(n / 100) * 100;
253
- for (const im of this._meshes) {
274
+ for (let k = 0; k < this._meshes.length; k++) {
275
+ const im = this._meshes[k];
254
276
  const imMat = new InstancedBufferAttribute(new Float32Array(newCap * 16), 16);
255
277
  const imCol = new InstancedBufferAttribute(new Float32Array(newCap * 3).fill(1), 3);
256
278
  for (let i = 0; i < im.count; i++) {
@@ -261,6 +283,14 @@ export default class InstancedBatch {
261
283
  }
262
284
  im.instanceMatrix = imMat;
263
285
  im.instanceColor = imCol;
286
+ // 逐实例全景序号缓冲跟着一起扩容(与上面两份实例缓冲同步换)。
287
+ // 不必搬旧值:唯一的调用点 ensureCapacity → compactWrite() 紧接着全量重写每个实例。
288
+ if (this._panoAttrs[k]) {
289
+ const pano = new InstancedBufferAttribute(new Float32Array(newCap).fill(-1), 1);
290
+ pano.setUsage(DynamicDrawUsage);
291
+ im.geometry.setAttribute("aPanoIndex", pano);
292
+ this._panoAttrs[k] = pano;
293
+ }
264
294
  }
265
295
  this._capacity = newCap;
266
296
  }
@@ -282,7 +312,7 @@ export default class InstancedBatch {
282
312
  FPScontrol.markAllCollidersDirty();
283
313
  }
284
314
  }
285
- /** 把单个实例的 matrix + color 写入所有 submesh 的对应槽位 */
315
+ /** 把单个实例的 matrix + color +(若启用)全景序号写入所有 submesh 的对应槽位 */
286
316
  writeInstance(i, child) {
287
317
  for (let k = 0; k < this._meshes.length; k++) {
288
318
  const im = this._meshes[k];
@@ -299,6 +329,11 @@ export default class InstancedBatch {
299
329
  im.setColorAt(i, child.color);
300
330
  im.instanceColor.needsUpdate = true;
301
331
  }
332
+ const pano = this._panoAttrs[k];
333
+ if (pano) {
334
+ pano.setX(i, child.panoIndex);
335
+ pano.needsUpdate = true;
336
+ }
302
337
  }
303
338
  }
304
339
  /** 事件路由:给定 InstancedMesh 与 instanceId,返回对应 InstancedChild */
@@ -331,6 +366,18 @@ export default class InstancedBatch {
331
366
  get pickables() {
332
367
  return this._meshes;
333
368
  }
369
+ /** 只读快照(拓扑图面板用):本 batch 当前持有的逻辑实例数 */
370
+ get childCount() {
371
+ return this._children.length;
372
+ }
373
+ /** 只读快照:实例缓冲容量(不够时按 step 扩容,从不缩容) */
374
+ get capacity() {
375
+ return this._capacity;
376
+ }
377
+ /** 只读快照:本 batch 的逻辑实例列表(只给引用,调用方不得增删) */
378
+ get childrenView() {
379
+ return this._children;
380
+ }
334
381
  dispose() {
335
382
  for (const im of this._meshes) {
336
383
  // 只释放 InstancedMesh 自带的实例缓冲(instanceMatrix / instanceColor),
@@ -341,6 +388,9 @@ export default class InstancedBatch {
341
388
  }
342
389
  this._meshes = [];
343
390
  this._nodeBake = [];
391
+ // 逐实例全景序号缓冲随 batch 一起散手(克隆的那份几何按本类既有规矩不 dispose:
392
+ // 它的 position/normal 数组与共享 static 几何同源,dispose 会把那份共享缓冲的 GPU 绑定一并摘掉)。
393
+ this._panoAttrs = [];
344
394
  if (this._mixer) {
345
395
  this._mixer.stopAllAction();
346
396
  }
@@ -140,6 +140,13 @@ export default class InstancedBatch {
140
140
  private _currentClip?: string;
141
141
  /** 首例实例是否声明碰撞(构造时快照,buildMeshes 用它给 InstancedMesh 打标记) */
142
142
  private _sampleFpsCollision: boolean = false;
143
+ /**
144
+ * 首例实例所在类是否声明 `static panoIndexAttribute`(构造时快照,与 _sampleFpsCollision 同一套路):
145
+ * true ⇒ buildMeshes 给本 batch 克隆一份渲染用几何并挂上逐实例 `aPanoIndex`(全景序号直取着色器读它)。
146
+ */
147
+ private _samplePanoIndexAttr: boolean = false;
148
+ /** 本 batch 自己挂上去的逐实例 `aPanoIndex` 缓冲(与 _meshes 顺序对齐;未启用则为空数组) */
149
+ private _panoAttrs: InstancedBufferAttribute[] = [];
143
150
  /** 当前正在播放的 AnimationAction(切换时先 stop 旧的) */
144
151
  private _currentAction?: AnimationAction;
145
152
  /** 非骨骼动画:需每帧烘焙节点变换的几何列表(与 _meshes 顺序对齐) */
@@ -153,6 +160,7 @@ export default class InstancedBatch {
153
160
  // 因 key 已含动画名,相同 glb+动画的实例都进同一个常驻 batch
154
161
  this._clipName = sample.getEffectiveAnimation();
155
162
  this._sampleFpsCollision = !!sample.fpsCollision;
163
+ this._samplePanoIndexAttr = !!(sample.constructor as typeof InstancedChild).panoIndexAttribute;
156
164
  this._loading = loadTemplate(sample).then((t) => {
157
165
  const ctor = sample.constructor as typeof InstancedChild;
158
166
  // GLB 模板就绪后,交给子类按需修正材质(如瓶子 metalness 修正)
@@ -171,10 +179,26 @@ export default class InstancedBatch {
171
179
  return;
172
180
  }
173
181
  for (const sub of this.template.submeshes) {
174
- const geom =
182
+ let geom =
175
183
  sub.skinned && this.template.sharedGeometry
176
184
  ? this.template.sharedGeometry
177
185
  : sub.geometry;
186
+ // 逐实例全景序号:本 batch 用**自己克隆的一份几何**挂 `aPanoIndex`(InstancedBufferAttribute
187
+ // 只能挂在 geometry 上,而 static geometry 跨 batch 共享 ⇒ 挂原几何会被别的 batch 覆盖,
188
+ // 详见 InstancedChild.panoIndexAttribute 的注释)。
189
+ // ⚠️ 骨骼蒙皮那条路不能克隆:sharedGeometry 每帧由 bakeSkinning 从 sourceGeometry 烤出来,
190
+ // 克隆一份就再也收不到烘焙结果(实例全部定格在绑定姿态)⇒ 蒙皮批次不加此属性。
191
+ if (this._samplePanoIndexAttr && !sub.skinned) {
192
+ const instGeom = geom.clone();
193
+ const panoAttr = new InstancedBufferAttribute(
194
+ new Float32Array(this._capacity).fill(-1),
195
+ 1
196
+ );
197
+ panoAttr.setUsage(DynamicDrawUsage);
198
+ instGeom.setAttribute("aPanoIndex", panoAttr);
199
+ this._panoAttrs.push(panoAttr);
200
+ geom = instGeom;
201
+ }
178
202
  const im = new InstancedMesh(geom, sub.material, this._capacity);
179
203
  im.frustumCulled = false;
180
204
  im.instanceMatrix.setUsage(DynamicDrawUsage);
@@ -295,7 +319,8 @@ export default class InstancedBatch {
295
319
  // 容量按 100 为一阶阶梯增长、只增不降:1 个对象→100 矩阵,101 个→200 矩阵,201 个→300 ...
296
320
  // 避免每超出即整段 realloc,也避免从 200 回降到 100 造成反复分配/收缩。
297
321
  const newCap = Math.ceil(n / 100) * 100;
298
- for (const im of this._meshes) {
322
+ for (let k = 0; k < this._meshes.length; k++) {
323
+ const im = this._meshes[k];
299
324
  const imMat = new InstancedBufferAttribute(new Float32Array(newCap * 16), 16);
300
325
  const imCol = new InstancedBufferAttribute(
301
326
  new Float32Array(newCap * 3).fill(1),
@@ -309,6 +334,17 @@ export default class InstancedBatch {
309
334
  }
310
335
  im.instanceMatrix = imMat;
311
336
  im.instanceColor = imCol;
337
+ // 逐实例全景序号缓冲跟着一起扩容(与上面两份实例缓冲同步换)。
338
+ // 不必搬旧值:唯一的调用点 ensureCapacity → compactWrite() 紧接着全量重写每个实例。
339
+ if (this._panoAttrs[k]) {
340
+ const pano = new InstancedBufferAttribute(
341
+ new Float32Array(newCap).fill(-1),
342
+ 1
343
+ );
344
+ pano.setUsage(DynamicDrawUsage);
345
+ im.geometry.setAttribute("aPanoIndex", pano);
346
+ this._panoAttrs[k] = pano;
347
+ }
312
348
  }
313
349
  this._capacity = newCap;
314
350
  }
@@ -333,7 +369,7 @@ export default class InstancedBatch {
333
369
  }
334
370
  }
335
371
 
336
- /** 把单个实例的 matrix + color 写入所有 submesh 的对应槽位 */
372
+ /** 把单个实例的 matrix + color +(若启用)全景序号写入所有 submesh 的对应槽位 */
337
373
  private writeInstance(i: number, child: InstancedChild): void {
338
374
  for (let k = 0; k < this._meshes.length; k++) {
339
375
  const im = this._meshes[k];
@@ -350,6 +386,11 @@ export default class InstancedBatch {
350
386
  im.setColorAt(i, child.color);
351
387
  im.instanceColor.needsUpdate = true;
352
388
  }
389
+ const pano = this._panoAttrs[k];
390
+ if (pano) {
391
+ pano.setX(i, child.panoIndex);
392
+ pano.needsUpdate = true;
393
+ }
353
394
  }
354
395
  }
355
396
 
@@ -389,6 +430,21 @@ export default class InstancedBatch {
389
430
  return this._meshes;
390
431
  }
391
432
 
433
+ /** 只读快照(拓扑图面板用):本 batch 当前持有的逻辑实例数 */
434
+ get childCount(): number {
435
+ return this._children.length;
436
+ }
437
+
438
+ /** 只读快照:实例缓冲容量(不够时按 step 扩容,从不缩容) */
439
+ get capacity(): number {
440
+ return this._capacity;
441
+ }
442
+
443
+ /** 只读快照:本 batch 的逻辑实例列表(只给引用,调用方不得增删) */
444
+ get childrenView(): readonly InstancedChild[] {
445
+ return this._children;
446
+ }
447
+
392
448
  dispose(): void {
393
449
  for (const im of this._meshes) {
394
450
  // 只释放 InstancedMesh 自带的实例缓冲(instanceMatrix / instanceColor),
@@ -399,6 +455,9 @@ export default class InstancedBatch {
399
455
  }
400
456
  this._meshes = [];
401
457
  this._nodeBake = [];
458
+ // 逐实例全景序号缓冲随 batch 一起散手(克隆的那份几何按本类既有规矩不 dispose:
459
+ // 它的 position/normal 数组与共享 static 几何同源,dispose 会把那份共享缓冲的 GPU 绑定一并摘掉)。
460
+ this._panoAttrs = [];
402
461
  if (this._mixer) {
403
462
  this._mixer.stopAllAction();
404
463
  }
@@ -66,10 +66,26 @@ export default class InstancedChild extends Object3D {
66
66
  static instanceUrl?: string;
67
67
  /** GLB 模式:要播放的动画剪辑名;不同动画 = 不同 batch(彼此独立) */
68
68
  static animation?: string;
69
+ /**
70
+ * 逐实例全景序号开关(内置几何模式):声明 true 的类,其 batch 会给渲染用的几何挂上一份
71
+ * 名为 `aPanoIndex` 的 InstancedBufferAttribute,每帧把每个实例的 `panoIndex` 写进去 ——
72
+ * 「全景序号直取」投射着色器(MktMesh/mktMappingShader)就按它逐实例挑那张全景。
73
+ *
74
+ * ⚠️ 为什么属性挂在**克隆出来的一份几何**上而不是 `static geometry` 本身(见 InstancedBatch):
75
+ * three 的逐实例自定义属性只能挂在 geometry 上,而 `static geometry` 是**跨 batch 共享**的
76
+ * (主编辑器与预览弹窗各有一份 InstancedMesh、实例序号各自独立排)⇒ 挂在原几何上等于两边
77
+ * 往同一块缓冲写各自的表,后写的把先写的覆盖掉,两边显示的图都会错。
78
+ */
79
+ static panoIndexAttribute: boolean;
69
80
  /** GLB 模式:模板(含材质)加载完成后回调,用于按需修正材质(如瓷器瓶 metalness=1 改 0) */
70
81
  static prepareTemplate?: (template: InstancedTemplate) => void;
71
82
  /** 实例颜色,写入 InstancedMesh.instanceColor */
72
83
  color: Color;
84
+ /**
85
+ * 逐实例全景序号(映射着色器的 `aPanoIndex`):-1 = 未指派 ⇒ 着色器回落 `uFaceMissingColor` 深灰。
86
+ * 仅当本类声明 `static panoIndexAttribute = true` 时才会被写进实例缓冲(见该静态字段的注释)。
87
+ */
88
+ panoIndex: number;
73
89
  /**
74
90
  * 脏标记:true 时本帧由 InstancedGroup 把本实例的 matrix/color 写回实例缓冲,
75
91
  * 写完后立即置 false —— 避免没有改动时重复写入真正参与渲染的 InstancedMesh。
@@ -96,6 +112,12 @@ export default class InstancedChild extends Object3D {
96
112
  * 可在运行时于控制台切换:InstancedChild.AUTO_MARK_DIRTY = true / false。
97
113
  */
98
114
  static AUTO_MARK_DIRTY: boolean;
115
+ /**
116
+ * 累计创建过的逻辑实例数(只增不减):构造函数里 +1,`dispose()` 不回退。
117
+ * 与 `instancedChildRegistry.size`(当前存活)配对看:已销毁 = 本值 - 存活数。
118
+ * 注册表是 Set,实例销毁就掉出去,看不到历史 ⇒ 「一共创建了多少个」只能靠这个计数器统计。
119
+ */
120
+ static createdTotal: number;
99
121
  /**
100
122
  * 请求切换到的动画剪辑名(由所属 batch 每帧应用)。
101
123
  * 实例化是"整批共享一个 mixer",动画是 batch 级而非实例级;
@@ -140,6 +162,8 @@ export default class InstancedChild extends Object3D {
140
162
  static syncSelectProxy(proxy: Group): void;
141
163
  /** 设置实例颜色(自动置 needUpdate) */
142
164
  setColor(c: Color | number | string): this;
165
+ /** 设置逐实例全景序号(自动置 needUpdate,与 setColor 同一条脏标记路) */
166
+ setPanoIndex(v: number): this;
143
167
  /**
144
168
  * 请求切换本实例的动画剪辑。batch 按「glb + 当前生效动画」分桶(每个动画一个常驻 InstancedBatch),
145
169
  * 调用 playClip 只改变本实例的生效动画,下一帧 getBatchKey 随之变化,实例便自动从旧动画 batch
@@ -48,10 +48,26 @@ export default class InstancedChild extends Object3D {
48
48
  static instanceUrl;
49
49
  /** GLB 模式:要播放的动画剪辑名;不同动画 = 不同 batch(彼此独立) */
50
50
  static animation;
51
+ /**
52
+ * 逐实例全景序号开关(内置几何模式):声明 true 的类,其 batch 会给渲染用的几何挂上一份
53
+ * 名为 `aPanoIndex` 的 InstancedBufferAttribute,每帧把每个实例的 `panoIndex` 写进去 ——
54
+ * 「全景序号直取」投射着色器(MktMesh/mktMappingShader)就按它逐实例挑那张全景。
55
+ *
56
+ * ⚠️ 为什么属性挂在**克隆出来的一份几何**上而不是 `static geometry` 本身(见 InstancedBatch):
57
+ * three 的逐实例自定义属性只能挂在 geometry 上,而 `static geometry` 是**跨 batch 共享**的
58
+ * (主编辑器与预览弹窗各有一份 InstancedMesh、实例序号各自独立排)⇒ 挂在原几何上等于两边
59
+ * 往同一块缓冲写各自的表,后写的把先写的覆盖掉,两边显示的图都会错。
60
+ */
61
+ static panoIndexAttribute = false;
51
62
  /** GLB 模式:模板(含材质)加载完成后回调,用于按需修正材质(如瓷器瓶 metalness=1 改 0) */
52
63
  static prepareTemplate;
53
64
  /** 实例颜色,写入 InstancedMesh.instanceColor */
54
65
  color = new Color(0xffffff);
66
+ /**
67
+ * 逐实例全景序号(映射着色器的 `aPanoIndex`):-1 = 未指派 ⇒ 着色器回落 `uFaceMissingColor` 深灰。
68
+ * 仅当本类声明 `static panoIndexAttribute = true` 时才会被写进实例缓冲(见该静态字段的注释)。
69
+ */
70
+ panoIndex = -1;
55
71
  /**
56
72
  * 脏标记:true 时本帧由 InstancedGroup 把本实例的 matrix/color 写回实例缓冲,
57
73
  * 写完后立即置 false —— 避免没有改动时重复写入真正参与渲染的 InstancedMesh。
@@ -78,6 +94,12 @@ export default class InstancedChild extends Object3D {
78
94
  * 可在运行时于控制台切换:InstancedChild.AUTO_MARK_DIRTY = true / false。
79
95
  */
80
96
  static AUTO_MARK_DIRTY = true;
97
+ /**
98
+ * 累计创建过的逻辑实例数(只增不减):构造函数里 +1,`dispose()` 不回退。
99
+ * 与 `instancedChildRegistry.size`(当前存活)配对看:已销毁 = 本值 - 存活数。
100
+ * 注册表是 Set,实例销毁就掉出去,看不到历史 ⇒ 「一共创建了多少个」只能靠这个计数器统计。
101
+ */
102
+ static createdTotal = 0;
81
103
  /**
82
104
  * 请求切换到的动画剪辑名(由所属 batch 每帧应用)。
83
105
  * 实例化是"整批共享一个 mixer",动画是 batch 级而非实例级;
@@ -87,10 +109,22 @@ export default class InstancedChild extends Object3D {
87
109
  _cachedWorld = new Matrix4();
88
110
  constructor(fpsCollision = false) {
89
111
  super();
112
+ // 控制台取证用:three 给的默认值是 `type = "Object3D"` + `name = ""`,一整棵场景树里根本看不出哪个是逻辑实例、
113
+ // 是哪个模型来的。⇒ name = `InstancedChild-来源`(皇上旨 2026-10-12),来源 GLB 模式**只取 url 的文件名**
114
+ // (`./models/bottle.glb` ⇒ `bottle.glb`,整条路径在控制台里太长且没信息量),内置几何模式没有 url ⇒ 取几何类型(BoxGeometry)。
115
+ // ⚠️ @types/three 把 Object3D.type 声明成 readonly(只允许声明类内部赋值),故这里走一次显式类型转换。
116
+ this.type = "InstancedChild";
117
+ const ctor = this.constructor;
118
+ // 文件名:切掉目录(最后一截)、再丢掉可能挂着的 ?查询 / #片段
119
+ const urlBase = ctor.instanceUrl ? ctor.instanceUrl.split("?")[0].split("#")[0].split("/").pop() : "";
120
+ const src = urlBase || (ctor.geometry ? ctor.geometry.type : "noSource");
121
+ this.name = `${this.type}-${src}`;
90
122
  this.fpsCollision = fpsCollision;
91
123
  // 标记:逻辑实例节点,ThreeMouse 收集候选时跳过(不渲染、无几何,靠 InstancedMesh 命中后路由回来)
92
124
  this.userData.__instancedChild = true;
93
125
  instancedChildRegistry.add(this);
126
+ // 累计统计走基类静态字段本身(⛔ 别用 this.constructor:子类各自有一份静态槽会漏计)
127
+ InstancedChild.createdTotal++;
94
128
  }
95
129
  /** 该实例归属哪个 batch 的 key:相同 geometry+material 或相同 instanceUrl+当前生效动画 合并为一类。
96
130
  * 因 key 含「当前生效动画」,切换动画 = 实例在「glb+旧动画」与「glb+新动画」两个常驻 batch 间迁移,而非 batch 内切 mixer。 */
@@ -207,6 +241,12 @@ export default class InstancedChild extends Object3D {
207
241
  this.needUpdate = true;
208
242
  return this;
209
243
  }
244
+ /** 设置逐实例全景序号(自动置 needUpdate,与 setColor 同一条脏标记路) */
245
+ setPanoIndex(v) {
246
+ this.panoIndex = v;
247
+ this.needUpdate = true;
248
+ return this;
249
+ }
210
250
  /**
211
251
  * 请求切换本实例的动画剪辑。batch 按「glb + 当前生效动画」分桶(每个动画一个常驻 InstancedBatch),
212
252
  * 调用 playClip 只改变本实例的生效动画,下一帧 getBatchKey 随之变化,实例便自动从旧动画 batch
@@ -78,11 +78,27 @@ export default class InstancedChild extends Object3D {
78
78
  static instanceUrl?: string;
79
79
  /** GLB 模式:要播放的动画剪辑名;不同动画 = 不同 batch(彼此独立) */
80
80
  static animation?: string;
81
+ /**
82
+ * 逐实例全景序号开关(内置几何模式):声明 true 的类,其 batch 会给渲染用的几何挂上一份
83
+ * 名为 `aPanoIndex` 的 InstancedBufferAttribute,每帧把每个实例的 `panoIndex` 写进去 ——
84
+ * 「全景序号直取」投射着色器(MktMesh/mktMappingShader)就按它逐实例挑那张全景。
85
+ *
86
+ * ⚠️ 为什么属性挂在**克隆出来的一份几何**上而不是 `static geometry` 本身(见 InstancedBatch):
87
+ * three 的逐实例自定义属性只能挂在 geometry 上,而 `static geometry` 是**跨 batch 共享**的
88
+ * (主编辑器与预览弹窗各有一份 InstancedMesh、实例序号各自独立排)⇒ 挂在原几何上等于两边
89
+ * 往同一块缓冲写各自的表,后写的把先写的覆盖掉,两边显示的图都会错。
90
+ */
91
+ static panoIndexAttribute: boolean = false;
81
92
  /** GLB 模式:模板(含材质)加载完成后回调,用于按需修正材质(如瓷器瓶 metalness=1 改 0) */
82
93
  static prepareTemplate?: (template: InstancedTemplate) => void;
83
94
 
84
95
  /** 实例颜色,写入 InstancedMesh.instanceColor */
85
96
  color: Color = new Color(0xffffff);
97
+ /**
98
+ * 逐实例全景序号(映射着色器的 `aPanoIndex`):-1 = 未指派 ⇒ 着色器回落 `uFaceMissingColor` 深灰。
99
+ * 仅当本类声明 `static panoIndexAttribute = true` 时才会被写进实例缓冲(见该静态字段的注释)。
100
+ */
101
+ panoIndex: number = -1;
86
102
  /**
87
103
  * 脏标记:true 时本帧由 InstancedGroup 把本实例的 matrix/color 写回实例缓冲,
88
104
  * 写完后立即置 false —— 避免没有改动时重复写入真正参与渲染的 InstancedMesh。
@@ -109,6 +125,12 @@ export default class InstancedChild extends Object3D {
109
125
  * 可在运行时于控制台切换:InstancedChild.AUTO_MARK_DIRTY = true / false。
110
126
  */
111
127
  static AUTO_MARK_DIRTY: boolean = true;
128
+ /**
129
+ * 累计创建过的逻辑实例数(只增不减):构造函数里 +1,`dispose()` 不回退。
130
+ * 与 `instancedChildRegistry.size`(当前存活)配对看:已销毁 = 本值 - 存活数。
131
+ * 注册表是 Set,实例销毁就掉出去,看不到历史 ⇒ 「一共创建了多少个」只能靠这个计数器统计。
132
+ */
133
+ static createdTotal: number = 0;
112
134
  /**
113
135
  * 请求切换到的动画剪辑名(由所属 batch 每帧应用)。
114
136
  * 实例化是"整批共享一个 mixer",动画是 batch 级而非实例级;
@@ -120,10 +142,22 @@ export default class InstancedChild extends Object3D {
120
142
 
121
143
  constructor(fpsCollision: boolean = false) {
122
144
  super();
145
+ // 控制台取证用:three 给的默认值是 `type = "Object3D"` + `name = ""`,一整棵场景树里根本看不出哪个是逻辑实例、
146
+ // 是哪个模型来的。⇒ name = `InstancedChild-来源`(皇上旨 2026-10-12),来源 GLB 模式**只取 url 的文件名**
147
+ // (`./models/bottle.glb` ⇒ `bottle.glb`,整条路径在控制台里太长且没信息量),内置几何模式没有 url ⇒ 取几何类型(BoxGeometry)。
148
+ // ⚠️ @types/three 把 Object3D.type 声明成 readonly(只允许声明类内部赋值),故这里走一次显式类型转换。
149
+ (this as { type: string }).type = "InstancedChild";
150
+ const ctor = this.constructor as typeof InstancedChild;
151
+ // 文件名:切掉目录(最后一截)、再丢掉可能挂着的 ?查询 / #片段
152
+ const urlBase = ctor.instanceUrl ? ctor.instanceUrl.split("?")[0].split("#")[0].split("/").pop() : "";
153
+ const src = urlBase || (ctor.geometry ? ctor.geometry.type : "noSource");
154
+ this.name = `${this.type}-${src}`;
123
155
  this.fpsCollision = fpsCollision;
124
156
  // 标记:逻辑实例节点,ThreeMouse 收集候选时跳过(不渲染、无几何,靠 InstancedMesh 命中后路由回来)
125
157
  this.userData.__instancedChild = true;
126
158
  instancedChildRegistry.add(this);
159
+ // 累计统计走基类静态字段本身(⛔ 别用 this.constructor:子类各自有一份静态槽会漏计)
160
+ InstancedChild.createdTotal++;
127
161
  }
128
162
 
129
163
  /** 该实例归属哪个 batch 的 key:相同 geometry+material 或相同 instanceUrl+当前生效动画 合并为一类。
@@ -265,6 +299,13 @@ export default class InstancedChild extends Object3D {
265
299
  return this;
266
300
  }
267
301
 
302
+ /** 设置逐实例全景序号(自动置 needUpdate,与 setColor 同一条脏标记路) */
303
+ setPanoIndex(v: number): this {
304
+ this.panoIndex = v;
305
+ this.needUpdate = true;
306
+ return this;
307
+ }
308
+
268
309
  /**
269
310
  * 请求切换本实例的动画剪辑。batch 按「glb + 当前生效动画」分桶(每个动画一个常驻 InstancedBatch),
270
311
  * 调用 playClip 只改变本实例的生效动画,下一帧 getBatchKey 随之变化,实例便自动从旧动画 batch
@@ -1,6 +1,44 @@
1
1
  import { Object3D } from "three";
2
2
  import InstancedChild from "./InstancedChild";
3
3
  import InstancedBatch from "./InstancedBatch";
4
+ /** 一个 batch 的合并情况快照(详见 `InstancedGroup.snapshot`) */
5
+ export interface InstancedBatchSnapshot {
6
+ /** 分桶键:`geo:<geometryUuid>/<materialUuid>` 或 `url:<glb>:anim:<剪辑名>` */
7
+ key: string;
8
+ /** 本桶里的逻辑实例数(InstancedChild) */
9
+ childCount: number;
10
+ /** 真实 InstancedMesh 个数 = 本桶一帧的 draw call 数(GLB 多 submesh ⇒ 大于 1) */
11
+ drawCalls: number;
12
+ /** 实例缓冲容量(只增不减) */
13
+ capacity: number;
14
+ /** GLB 模板是否已就绪;false ⇒ 实例已登记但还没画出来 */
15
+ ready: boolean;
16
+ /** 每个 InstancedMesh 实际写入的实例数(与 drawCalls 一一对应) */
17
+ counts: number[];
18
+ /** 子实例的可读标签(name 优先,其次构造器名) */
19
+ children: string[];
20
+ /** 首个 InstancedMesh 的几何类型名(如 `BoxGeometry`);模板未就绪时为空串 */
21
+ geomType: string;
22
+ /** 首个 InstancedMesh 的材质类型名(多材质为 `多材质`);模板未就绪时为空串 */
23
+ matType: string;
24
+ }
25
+ /** 一个 InstancedGroup 的合并总览 */
26
+ export interface InstancedGroupSnapshot {
27
+ /** 本组归属 tag;undefined = ThreeBase 默认组(只收未声明归属的实例) */
28
+ owner?: string;
29
+ batches: InstancedBatchSnapshot[];
30
+ /** 已收进各 batch 的实例总数 */
31
+ merged: number;
32
+ /** 全局注册表里的 InstancedChild 总数(含归属其他组的) */
33
+ registryTotal: number;
34
+ /**
35
+ * 累计创建过的 InstancedChild 总数(只增不减,dispose 不回退)。
36
+ * 与 `registryTotal` 配对即得「已销毁 = createdTotal - registryTotal」。
37
+ */
38
+ createdTotal: number;
39
+ /** 归属本组却没进任何 batch 的实例数:正常恒为 0,非 0 说明分桶/收集有漏 */
40
+ leaked: number;
41
+ }
4
42
  /**
5
43
  * 固定在 ThreeBase.scene 里的分组器。每帧收集所有 InstancedChild,按 batchKey(glb+当前生效动画)分组,
6
44
  * 为每个 key 创建/复用 InstancedBatch,把实例的世界矩阵 / 颜色同步进真实 InstancedMesh。
@@ -29,6 +67,16 @@ export default class InstancedGroup extends Object3D {
29
67
  resolveInstance(mesh: Object3D, instanceId: number | undefined): InstancedChild | null;
30
68
  /** 收集可拾取的 InstancedMesh(已就绪 batch,且注册了监听或设置了 cursor 的),供 ThreeMouse 候选 */
31
69
  collectPickable(out: Object3D[]): void;
70
+ /**
71
+ * 只读快照:本组当前的合并情况,供调试面板画拓扑图。
72
+ *
73
+ * 归属判定(哪些注册表实例该由本组收集)刻意留在这里而不是让调用方自己判 ——
74
+ * 规则与 `_accepts` 必须同源,否则面板报的「未合并数」会与实际渲染不符。
75
+ *
76
+ * ⚠️ 子实例标签取 `name`,其次取构造器名:业务子类若没设 `name`,生产构建(压缩后)
77
+ * 构造器名会变成短标识符。要在拓扑图里认出实例,请给实例设 `name`(同 [[InstancedChild]] 的控制台可读命名约定)。
78
+ */
79
+ snapshot(): InstancedGroupSnapshot;
32
80
  /**
33
81
  * 释放本组的全部 batch(实例缓冲 + 从组内摘除 mesh)。
34
82
  *
@@ -1,5 +1,5 @@
1
1
  import { Clock, Object3D } from "three";
2
- import { instancedChildRegistry, instancedChildOwner } from "./InstancedChild";
2
+ import InstancedChild, { instancedChildRegistry, instancedChildOwner } from "./InstancedChild";
3
3
  import InstancedBatch from "./InstancedBatch";
4
4
  const PICK_EVENT_TYPES = [
5
5
  "mouseenter",
@@ -107,6 +107,50 @@ export default class InstancedGroup extends Object3D {
107
107
  }
108
108
  }
109
109
  }
110
+ /**
111
+ * 只读快照:本组当前的合并情况,供调试面板画拓扑图。
112
+ *
113
+ * 归属判定(哪些注册表实例该由本组收集)刻意留在这里而不是让调用方自己判 ——
114
+ * 规则与 `_accepts` 必须同源,否则面板报的「未合并数」会与实际渲染不符。
115
+ *
116
+ * ⚠️ 子实例标签取 `name`,其次取构造器名:业务子类若没设 `name`,生产构建(压缩后)
117
+ * 构造器名会变成短标识符。要在拓扑图里认出实例,请给实例设 `name`(同 [[InstancedChild]] 的控制台可读命名约定)。
118
+ */
119
+ snapshot() {
120
+ const batches = [];
121
+ let merged = 0;
122
+ for (const batch of this.batches.values()) {
123
+ const childCount = batch.childCount;
124
+ merged += childCount;
125
+ const first = batch.pickables[0];
126
+ const mat = first?.material;
127
+ batches.push({
128
+ key: batch.key,
129
+ childCount,
130
+ drawCalls: batch.pickables.length,
131
+ capacity: batch.capacity,
132
+ ready: batch.ready,
133
+ counts: batch.pickables.map((im) => im.count),
134
+ children: batch.childrenView.map((c) => c.name || c.constructor.name),
135
+ geomType: first?.geometry.type ?? "",
136
+ matType: Array.isArray(mat) ? "多材质" : mat?.type ?? "",
137
+ });
138
+ }
139
+ let accepted = 0;
140
+ for (const child of instancedChildRegistry) {
141
+ if (this._accepts(child)) {
142
+ accepted++;
143
+ }
144
+ }
145
+ return {
146
+ owner: this.owner,
147
+ batches,
148
+ merged,
149
+ registryTotal: instancedChildRegistry.size,
150
+ createdTotal: InstancedChild.createdTotal,
151
+ leaked: accepted - merged,
152
+ };
153
+ }
110
154
  /**
111
155
  * 释放本组的全部 batch(实例缓冲 + 从组内摘除 mesh)。
112
156
  *