s3m-three 0.3.6 → 0.3.8

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 (92) hide show
  1. package/dist/anim/Animator.d.ts +111 -0
  2. package/dist/anim/Animator.d.ts.map +1 -0
  3. package/dist/anim/Animator.js +382 -0
  4. package/dist/anim/Animator.js.map +1 -0
  5. package/dist/anim/Path.d.ts +30 -0
  6. package/dist/anim/Path.d.ts.map +1 -0
  7. package/dist/anim/Path.js +100 -0
  8. package/dist/anim/Path.js.map +1 -0
  9. package/dist/anim/easing.d.ts +70 -0
  10. package/dist/anim/easing.d.ts.map +1 -0
  11. package/dist/anim/easing.js +170 -0
  12. package/dist/anim/easing.js.map +1 -0
  13. package/dist/anim/index.d.ts +6 -0
  14. package/dist/anim/index.d.ts.map +1 -0
  15. package/dist/anim/index.js +4 -0
  16. package/dist/anim/index.js.map +1 -0
  17. package/dist/browser/assets/{s3mWorker-hZY5KqWF.js → s3mWorker-D7AV6O30.js} +1 -1
  18. package/dist/browser/s3m-three.iife.ext-three.js +60 -22
  19. package/dist/browser/s3m-three.iife.js +60 -22
  20. package/dist/core/TextureDecode.d.ts +22 -1
  21. package/dist/core/TextureDecode.d.ts.map +1 -1
  22. package/dist/core/TextureDecode.js +47 -3
  23. package/dist/core/TextureDecode.js.map +1 -1
  24. package/dist/core/TileGeometry.d.ts +92 -0
  25. package/dist/core/TileGeometry.d.ts.map +1 -0
  26. package/dist/core/TileGeometry.js +607 -0
  27. package/dist/core/TileGeometry.js.map +1 -0
  28. package/dist/core/types.d.ts +16 -0
  29. package/dist/core/types.d.ts.map +1 -1
  30. package/dist/index.d.ts +3 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/render/EdgeLines.d.ts +62 -14
  35. package/dist/render/EdgeLines.d.ts.map +1 -1
  36. package/dist/render/EdgeLines.js +97 -8
  37. package/dist/render/EdgeLines.js.map +1 -1
  38. package/dist/render/FeatureStyleManager.d.ts +17 -1
  39. package/dist/render/FeatureStyleManager.d.ts.map +1 -1
  40. package/dist/render/FeatureStyleManager.js +14 -4
  41. package/dist/render/FeatureStyleManager.js.map +1 -1
  42. package/dist/render/GeometryBuilder.d.ts +8 -10
  43. package/dist/render/GeometryBuilder.d.ts.map +1 -1
  44. package/dist/render/GeometryBuilder.js +77 -555
  45. package/dist/render/GeometryBuilder.js.map +1 -1
  46. package/dist/render/MergeVertices.d.ts +7 -0
  47. package/dist/render/MergeVertices.d.ts.map +1 -1
  48. package/dist/render/MergeVertices.js +10 -1
  49. package/dist/render/MergeVertices.js.map +1 -1
  50. package/dist/render/S3MViewer.d.ts +218 -9
  51. package/dist/render/S3MViewer.d.ts.map +1 -1
  52. package/dist/render/S3MViewer.js +772 -68
  53. package/dist/render/S3MViewer.js.map +1 -1
  54. package/dist/render/StyleMaterial.d.ts +19 -0
  55. package/dist/render/StyleMaterial.d.ts.map +1 -1
  56. package/dist/render/StyleMaterial.js +37 -0
  57. package/dist/render/StyleMaterial.js.map +1 -1
  58. package/dist/render/TextureFactory.d.ts +2 -1
  59. package/dist/render/TextureFactory.d.ts.map +1 -1
  60. package/dist/render/TextureFactory.js +30 -6
  61. package/dist/render/TextureFactory.js.map +1 -1
  62. package/dist/tileset/S3MTileset.d.ts +6 -2
  63. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  64. package/dist/tileset/S3MTileset.js +17 -7
  65. package/dist/tileset/S3MTileset.js.map +1 -1
  66. package/dist/worker/S3MWorkerClient.d.ts +12 -4
  67. package/dist/worker/S3MWorkerClient.d.ts.map +1 -1
  68. package/dist/worker/S3MWorkerClient.js +12 -8
  69. package/dist/worker/S3MWorkerClient.js.map +1 -1
  70. package/dist/worker/s3mWorker.js +11 -3
  71. package/dist/worker/s3mWorker.js.map +1 -1
  72. package/docs/ANIMATION.md +534 -0
  73. package/docs/API.md +4 -0
  74. package/package.json +1 -1
  75. package/src/anim/Animator.ts +460 -0
  76. package/src/anim/Path.ts +107 -0
  77. package/src/anim/easing.ts +195 -0
  78. package/src/anim/index.ts +10 -0
  79. package/src/core/TextureDecode.ts +69 -3
  80. package/src/core/TileGeometry.ts +745 -0
  81. package/src/core/types.ts +8 -0
  82. package/src/index.ts +10 -0
  83. package/src/render/EdgeLines.ts +131 -15
  84. package/src/render/FeatureStyleManager.ts +12 -4
  85. package/src/render/GeometryBuilder.ts +91 -617
  86. package/src/render/MergeVertices.ts +11 -1
  87. package/src/render/S3MViewer.ts +866 -67
  88. package/src/render/StyleMaterial.ts +54 -1
  89. package/src/render/TextureFactory.ts +42 -8
  90. package/src/tileset/S3MTileset.ts +25 -9
  91. package/src/worker/S3MWorkerClient.ts +18 -9
  92. package/src/worker/s3mWorker.ts +24 -6
@@ -14,17 +14,9 @@
14
14
  import * as THREE from 'three';
15
15
  import { createTexture } from './TextureFactory.js';
16
16
  import { createSharedUniforms, createStyledMaterial } from './StyleMaterial.js';
17
- /* ------------------------------------------------------------------ 索引包类型 */
18
- /**
19
- * S3M 规范里 `DrawPrimitiveType` 只到 10(多边形),但 iServer 实际发布的 v3 瓦片里
20
- * 会出现 **13**:它是数据自带的「棱线包」—— 索引是线段对,不是三角形。
21
- *
22
- * 这条是踩过坑才认出来的:把 13 当三角形画,每条棱线都会被连成一根几十上百米的尖刺,
23
- * 满屏都是放射状的三角面。按 LineSegments 画出来,正好是整片建筑轮廓线。
24
- */
25
- export const LINE_PRIMITIVE = 13;
26
- /** primitiveType = 1:点云包。索引是点序号,画成三角面就是满屏彩色尖刺。 */
27
- export const POINT_PRIMITIVE = 1;
17
+ import { assembleTileGeometry, LINE_PRIMITIVE, POINT_PRIMITIVE } from '../core/TileGeometry.js';
18
+ // 组装层下沉 core 后的对外 API 兼容:常量原住本文件
19
+ export { LINE_PRIMITIVE, POINT_PRIMITIVE };
28
20
  function isLinePrimitive(primitiveType) {
29
21
  // 1 = 点,2 = 线段,3 = 折线,13 = 棱线包
30
22
  return primitiveType === LINE_PRIMITIVE || primitiveType === 2 || primitiveType === 3;
@@ -66,98 +58,7 @@ function getPointCloudMaterial() {
66
58
  return pointCloudMaterial;
67
59
  }
68
60
  /* ------------------------------------------------------------------ 顶点流 */
69
- function bytesPerElement(dtype) {
70
- return dtype === 'u8' ? 1 : dtype === 'u16' ? 2 : 4;
71
- }
72
- function typedArray(dtype, buffer) {
73
- switch (dtype) {
74
- case 'u8':
75
- return new Uint8Array(buffer);
76
- case 'u16':
77
- return new Uint16Array(buffer);
78
- case 'u32':
79
- return new Uint32Array(buffer);
80
- default:
81
- return new Float32Array(buffer);
82
- }
83
- }
84
- function typedArrayOf(dtype, length) {
85
- switch (dtype) {
86
- case 'u8':
87
- return new Uint8Array(length);
88
- case 'u16':
89
- return new Uint16Array(length);
90
- case 'u32':
91
- return new Uint32Array(length);
92
- default:
93
- return new Float32Array(length);
94
- }
95
- }
96
- function toBufferAttribute(attr) {
97
- const { data, stride, offsetInBytes, dtype = 'f32' } = attr;
98
- const components = attr.components ?? attr.comp ?? 3;
99
- const bpe = bytesPerElement(dtype);
100
- const itemBytes = components * bpe;
101
- const actualStride = stride ?? itemBytes;
102
- const actualOffset = offsetInBytes ?? 0;
103
- const normalized = dtype === 'u8';
104
- // 非交错:整段直接铺开
105
- if (actualStride === itemBytes && actualOffset === 0) {
106
- const view = typedArray(dtype, data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength));
107
- return new THREE.BufferAttribute(view, components, normalized);
108
- }
109
- // 交错:逐元素抽取
110
- const count = Math.floor(data.byteLength / actualStride);
111
- const out = typedArrayOf(dtype, count * components);
112
- const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
113
- for (let i = 0; i < count; i++) {
114
- const base = i * actualStride + actualOffset;
115
- for (let c = 0; c < components; c++) {
116
- const o = base + c * bpe;
117
- out[i * components + c] =
118
- dtype === 'u8'
119
- ? dv.getUint8(o)
120
- : dtype === 'u16'
121
- ? dv.getUint16(o, true)
122
- : dtype === 'u32'
123
- ? dv.getUint32(o, true)
124
- : dv.getFloat32(o, true);
125
- }
126
- }
127
- return new THREE.BufferAttribute(out, components, normalized);
128
- }
129
- function toIndex(indexPack) {
130
- const { indices, indexType, indexCount } = indexPack;
131
- if (!indices || indexCount === 0)
132
- return null;
133
- // 以字节宽度为准:S3M 的 indexType 在 MeshOpt 包里恒为 0,靠它会读错一半索引。
134
- const width = Math.floor(indices.byteLength / indexCount);
135
- const wide = width === 4 || (width !== 2 && (indexType === 1 || indexType === 3));
136
- const n = indexCount * (wide ? 4 : 2);
137
- if (n > indices.byteLength)
138
- return null;
139
- const buf = indices.buffer.slice(indices.byteOffset, indices.byteOffset + n);
140
- const view = wide ? new Uint32Array(buf) : new Uint16Array(buf);
141
- return new THREE.BufferAttribute(view, 1);
142
- }
143
- /** 把 u8 顶点色转成 float vec3,供着色器直接相乘 */
144
- function toColorAttribute(attr, vertexCount) {
145
- const out = new Float32Array(vertexCount * 3);
146
- if (!attr) {
147
- out.fill(1);
148
- return new THREE.BufferAttribute(out, 3);
149
- }
150
- const src = attr.data;
151
- const comp = attr.components ?? 4;
152
- const scale = attr.dtype === 'u8' ? 1 / 255 : 1;
153
- for (let i = 0; i < vertexCount; i++) {
154
- const o = i * comp;
155
- out[i * 3] = (src[o] ?? 255) * scale;
156
- out[i * 3 + 1] = (src[o + 1] ?? 255) * scale;
157
- out[i * 3 + 2] = (src[o + 2] ?? 255) * scale;
158
- }
159
- return new THREE.BufferAttribute(out, 3);
160
- }
61
+ /** 顶点流解交错 / 法线 / 合批已下沉 core/TileGeometry.ts(Worker / 主线程共用) */
161
62
  /** SuperMap 内置水面(`103.water`)的基色:偏深的青蓝,配合低粗糙度出一点天光反射 */
162
63
  const WATER_COLOR = new THREE.Color(0x1d4e63);
163
64
  /** 3.x 是 `{materials:[...]}`,1.x/2.x 是 `{material:[...]}`,桌面导出偶尔直接给数组 */
@@ -327,354 +228,54 @@ export async function buildTileObjects(tile, options = {}) {
327
228
  });
328
229
  return mat;
329
230
  };
330
- // 3) 骨架 → 几何。顶点是「骨架局部坐标」,真正位置由引用它的 Geode 矩阵决定。
331
- const geoCache = new Map();
332
- const skeletonInfo = new Map();
333
- for (const [name, geo] of Object.entries(tile.geoPackage ?? {})) {
334
- const vp = geo.vertexPackage;
335
- const positionAttr = vp.vertexAttributes.find((a) => a.semantic === 'position');
336
- if (!positionAttr) {
337
- stats.skipped.push(`${name} 无 position`);
338
- continue;
339
- }
340
- const vertexCount = vp.verticesCount || 0;
341
- const attributes = { position: toBufferAttribute(positionAttr) };
342
- let colorAttr = null;
343
- let batchAttr = null;
344
- for (const attr of vp.vertexAttributes) {
345
- if (attr.divisor !== undefined)
346
- continue; // 实例属性不进顶点流
347
- if (attr.semantic === 'position')
348
- continue;
349
- if (attr.semantic === 'color')
350
- colorAttr = attr;
351
- else if (attr.semantic === 'batchId')
352
- batchAttr = attr;
353
- else if (attr.semantic === 'normal')
354
- attributes.normal = toBufferAttribute(attr);
355
- else if (attr.semantic === 'uv')
356
- attributes.uv = toBufferAttribute(attr);
357
- else if (attr.semantic === 'uv2')
358
- attributes.uv1 = toBufferAttribute(attr);
359
- }
360
- // 着色器强依赖这两个 attribute,缺了就补默认值
361
- attributes.aColor = toColorAttribute(colorAttr, vertexCount);
362
- attributes.aBatchId = batchAttr
363
- ? toBufferAttribute(batchAttr)
364
- : new THREE.BufferAttribute(new Float32Array(vertexCount).fill(-1), 1);
365
- // S3MBLOCK 图集:逐顶点带子区域矩形,平铺的 fract 交给片元做(顶点上做会画反)。
366
- // 烘焙图(材质 unit1)用第二套原始 UV,这里另开一个 aSecUv 槽位,不跟 three 的 uv1 语义纠缠。
367
- const rects = tile.atlasRects?.[name];
368
- if (rects) {
369
- attributes.aAtlasRect = new THREE.BufferAttribute(rects.uv ?? new Float32Array(vertexCount * 4), 4);
370
- // 没有第二套 UV 的骨架不做烘焙:给一个退化矩形,片元里据此跳过
371
- const uv1 = attributes.uv1;
372
- if (rects.uv2 && uv1) {
373
- attributes.aAtlasRect1 = new THREE.BufferAttribute(rects.uv2, 4);
374
- attributes.aSecUv = uv1;
375
- }
376
- else {
377
- attributes.aAtlasRect1 = new THREE.BufferAttribute(new Float32Array(vertexCount * 4), 4);
378
- attributes.aSecUv = attributes.uv ?? new THREE.BufferAttribute(new Float32Array(vertexCount * 2), 2);
379
- }
380
- }
381
- const instanced = vp.instanceIndex !== -1 && (vp.instanceMode === 16 || vp.instanceMode === 17);
382
- // 注意:indexPackages 属于 Skeleton(geoPackage[name]),不在 vertexPackage 里
383
- //
384
- // 一个索引包都没有的骨架 = 纯点集:高斯泼溅(3DGaussianSplatting)和无索引点云
385
- // 都是这种——顶点就是点,没有任何三角形可画。不补一个「顶点即点」的伪包,
386
- // 下面就不会产出任何对象,整块瓦片最后落到「tile 无可用几何」被丢掉。
387
- let packs = geo.indexPackages ?? [];
388
- if (packs.length === 0 && vertexCount > 0) {
389
- packs = [
390
- {
391
- indexCount: 0,
392
- indexType: 0,
393
- useIndex: 0,
394
- primitiveType: POINT_PRIMITIVE,
395
- indices: null,
396
- passNames: [],
397
- },
398
- ];
399
- }
400
- skeletonInfo.set(name, { indexPackages: packs, instanced });
401
- stats.vertices += vertexCount;
402
- packs.forEach((pack, pi) => {
403
- let geometry = new THREE.BufferGeometry();
404
- for (const [k, v] of Object.entries(attributes))
405
- geometry.setAttribute(k, v);
406
- const idx = toIndex(pack);
407
- if (idx)
408
- geometry.setIndex(idx);
409
- else
410
- geometry.setDrawRange(0, vertexCount);
411
- // 如果数据本身没有法线且是三角网格,必须在绑定了 index 之后计算法线
412
- // 绝不能在未索引的 attributes.position 上算,否则非连续顶点会导致法线朝向错乱甚至越界 NaN 导致死黑面
413
- // 线 / 点包不需要法线
414
- if (!isLinePrimitive(pack.primitiveType) &&
415
- !isPointPrimitive(pack.primitiveType) &&
416
- !geometry.getAttribute('normal')) {
417
- geometry.computeVertexNormals();
418
- }
419
- geometry.computeBoundingBox();
420
- geometry.computeBoundingSphere();
421
- geoCache.set(`${name}|${pi}`, { geometry, pack });
422
- });
423
- }
231
+ // 3) 纯数据几何组装:Worker 解析时已顺带产出就直接用,否则主线程现场组装
232
+ // (core/TileGeometry 的同一份实现,两条路径产物一致)
233
+ const assembled = options.assembled ?? assembleTileGeometry(tile, { batching: options.batching !== false });
234
+ stats.vertices = assembled.stats.vertices;
235
+ stats.edgeLines = assembled.stats.edgeLines;
236
+ stats.instanced = assembled.stats.instanced;
237
+ if (assembled.stats.originalMeshes != null)
238
+ stats.originalMeshes = assembled.stats.originalMeshes;
239
+ stats.skipped.push(...assembled.stats.skipped);
424
240
  const IDENTITY = new THREE.Matrix4();
425
- const batchGroups = new Map();
426
- const mergeBatchGeometries = (candidates, material, tileRef) => {
427
- let totalVertices = 0;
428
- let totalIndices = 0;
429
- let hasNormal = false;
430
- let hasUv = false;
431
- let hasUv1 = false;
432
- let hasAColor = false;
433
- let hasABatchId = false;
434
- let hasAtlas = false;
435
- for (const c of candidates) {
436
- const g = c.cached.geometry;
437
- const vCount = g.attributes.position?.count ?? 0;
438
- totalVertices += vCount;
439
- totalIndices += g.index ? g.index.count : vCount;
440
- if (g.attributes.normal)
441
- hasNormal = true;
442
- if (g.attributes.uv)
443
- hasUv = true;
444
- if (g.attributes.uv1)
445
- hasUv1 = true;
446
- if (g.attributes.aColor)
447
- hasAColor = true;
448
- if (g.attributes.aBatchId)
449
- hasABatchId = true;
450
- if (g.attributes.aAtlasRect)
451
- hasAtlas = true;
241
+ const packPrimitive = (prim) => {
242
+ const geometry = new THREE.BufferGeometry();
243
+ for (const [k, a] of Object.entries(prim.attributes)) {
244
+ if (a)
245
+ geometry.setAttribute(k, new THREE.BufferAttribute(a.array, a.components, a.normalized));
452
246
  }
453
- const mergedPos = new Float32Array(totalVertices * 3);
454
- const mergedNorm = hasNormal ? new Float32Array(totalVertices * 3) : null;
455
- const mergedUv = hasUv ? new Float32Array(totalVertices * 2) : null;
456
- const mergedUv1 = hasUv1 ? new Float32Array(totalVertices * 2) : null;
457
- const mergedColor = hasAColor ? new Float32Array(totalVertices * 3) : null;
458
- const mergedBatchId = hasABatchId ? new Float32Array(totalVertices) : null;
459
- const mergedRect0 = hasAtlas ? new Float32Array(totalVertices * 4) : null;
460
- const mergedRect1 = hasAtlas ? new Float32Array(totalVertices * 4) : null;
461
- const mergedSecUv = hasAtlas ? new Float32Array(totalVertices * 2) : null;
462
- /** 逐顶点拷一个 n 分量的浮点属性;缺失时用 fallbackRect / 0 兜底 */
463
- const copyAttr = (dst, attr, comp, vCount, vertexOffset, fallback) => {
464
- if (attr) {
465
- const src = attr.array;
466
- for (let i = 0; i < vCount; i++)
467
- for (let k = 0; k < comp; k++)
468
- dst[(vertexOffset + i) * comp + k] = src[i * comp + k] ?? 0;
469
- return;
470
- }
471
- for (let i = 0; i < vCount; i++) {
472
- const out = [];
473
- fallback ? fallback(i, out) : out.push(...new Array(comp).fill(0));
474
- for (let k = 0; k < comp; k++)
475
- dst[(vertexOffset + i) * comp + k] = out[k] ?? 0;
476
- }
477
- };
478
- const IndexArrayType = totalVertices > 65535 ? Uint32Array : Uint16Array;
479
- const mergedIndices = new IndexArrayType(totalIndices);
480
- let vertexOffset = 0;
481
- let indexOffset = 0;
482
- const tmpVec = new THREE.Vector3();
483
- const normalMatrix = new THREE.Matrix3();
484
- const geoByBatchId = new Map();
485
- for (const c of candidates) {
486
- const g = c.cached.geometry;
487
- const vCount = g.attributes.position.count;
488
- const m = c.matrix;
489
- const hasTransform = m && !m.equals(IDENTITY);
490
- if (hasTransform && hasNormal) {
491
- normalMatrix.getNormalMatrix(m);
492
- }
493
- if (c.geo.pickInfo) {
494
- for (const entry of Object.values(c.geo.pickInfo)) {
495
- if (typeof entry.batchId === 'number' && entry.batchId >= 0) {
496
- geoByBatchId.set(entry.batchId, c.geo);
497
- }
498
- }
499
- }
500
- // 1. Position
501
- const posArr = g.attributes.position.array;
502
- for (let i = 0; i < vCount; i++) {
503
- const srcIdx = i * 3;
504
- const dstIdx = (vertexOffset + i) * 3;
505
- if (hasTransform) {
506
- tmpVec.set(posArr[srcIdx], posArr[srcIdx + 1], posArr[srcIdx + 2]).applyMatrix4(m);
507
- mergedPos[dstIdx] = tmpVec.x;
508
- mergedPos[dstIdx + 1] = tmpVec.y;
509
- mergedPos[dstIdx + 2] = tmpVec.z;
510
- }
511
- else {
512
- mergedPos[dstIdx] = posArr[srcIdx];
513
- mergedPos[dstIdx + 1] = posArr[srcIdx + 1];
514
- mergedPos[dstIdx + 2] = posArr[srcIdx + 2];
515
- }
516
- }
517
- // 2. Normal
518
- if (mergedNorm) {
519
- const normAttr = g.attributes.normal;
520
- if (normAttr) {
521
- const normArr = normAttr.array;
522
- for (let i = 0; i < vCount; i++) {
523
- const srcIdx = i * 3;
524
- const dstIdx = (vertexOffset + i) * 3;
525
- if (hasTransform) {
526
- tmpVec
527
- .set(normArr[srcIdx], normArr[srcIdx + 1], normArr[srcIdx + 2])
528
- .applyMatrix3(normalMatrix)
529
- .normalize();
530
- mergedNorm[dstIdx] = tmpVec.x;
531
- mergedNorm[dstIdx + 1] = tmpVec.y;
532
- mergedNorm[dstIdx + 2] = tmpVec.z;
533
- }
534
- else {
535
- mergedNorm[dstIdx] = normArr[srcIdx];
536
- mergedNorm[dstIdx + 1] = normArr[srcIdx + 1];
537
- mergedNorm[dstIdx + 2] = normArr[srcIdx + 2];
538
- }
539
- }
540
- }
541
- }
542
- // 3. UV
543
- if (mergedUv) {
544
- const uvAttr = g.attributes.uv;
545
- if (uvAttr) {
546
- const uvArr = uvAttr.array;
547
- for (let i = 0; i < vCount; i++) {
548
- mergedUv[(vertexOffset + i) * 2] = uvArr[i * 2];
549
- mergedUv[(vertexOffset + i) * 2 + 1] = uvArr[i * 2 + 1];
550
- }
551
- }
552
- }
553
- // 4. UV1
554
- if (mergedUv1) {
555
- const uv1Attr = g.attributes.uv1;
556
- if (uv1Attr) {
557
- const uvArr = uv1Attr.array;
558
- for (let i = 0; i < vCount; i++) {
559
- mergedUv1[(vertexOffset + i) * 2] = uvArr[i * 2];
560
- mergedUv1[(vertexOffset + i) * 2 + 1] = uvArr[i * 2 + 1];
561
- }
562
- }
563
- }
564
- // 5. aColor
565
- if (mergedColor) {
566
- const colAttr = g.attributes.aColor;
567
- if (colAttr) {
568
- const colArr = colAttr.array;
569
- for (let i = 0; i < vCount; i++) {
570
- mergedColor[(vertexOffset + i) * 3] = colArr[i * 3];
571
- mergedColor[(vertexOffset + i) * 3 + 1] = colArr[i * 3 + 1];
572
- mergedColor[(vertexOffset + i) * 3 + 2] = colArr[i * 3 + 2];
573
- }
574
- }
575
- else {
576
- for (let i = 0; i < vCount; i++) {
577
- mergedColor[(vertexOffset + i) * 3] = 1;
578
- mergedColor[(vertexOffset + i) * 3 + 1] = 1;
579
- mergedColor[(vertexOffset + i) * 3 + 2] = 1;
580
- }
581
- }
582
- }
583
- // 6. aBatchId
584
- if (mergedBatchId) {
585
- const bAttr = g.attributes.aBatchId;
586
- if (bAttr) {
587
- const bArr = bAttr.array;
588
- for (let i = 0; i < vCount; i++) {
589
- mergedBatchId[vertexOffset + i] = bArr[i];
590
- }
591
- }
592
- else {
593
- for (let i = 0; i < vCount; i++) {
594
- mergedBatchId[vertexOffset + i] = -1;
595
- }
596
- }
597
- }
598
- // 7. 图集矩形(S3MBLOCK)
599
- if (mergedRect0) {
600
- copyAttr(mergedRect0, g.attributes.aAtlasRect, 4, vCount, vertexOffset, (_i, out) => out.push(0, 0, 0, 0));
601
- copyAttr(mergedRect1, g.attributes.aAtlasRect1, 4, vCount, vertexOffset, (_i, out) => out.push(0, 0, 0, 0));
602
- copyAttr(mergedSecUv, g.attributes.aSecUv, 2, vCount, vertexOffset);
603
- }
604
- // 8. Indices
605
- if (g.index) {
606
- const idxArr = g.index.array;
607
- const iCount = g.index.count;
608
- for (let k = 0; k < iCount; k++) {
609
- mergedIndices[indexOffset + k] = idxArr[k] + vertexOffset;
610
- }
611
- indexOffset += iCount;
612
- }
613
- else {
614
- for (let k = 0; k < vCount; k++) {
615
- mergedIndices[indexOffset + k] = k + vertexOffset;
616
- }
617
- indexOffset += vCount;
618
- }
619
- vertexOffset += vCount;
620
- }
621
- const mergedGeo = new THREE.BufferGeometry();
622
- mergedGeo.setAttribute('position', new THREE.BufferAttribute(mergedPos, 3));
623
- if (mergedNorm)
624
- mergedGeo.setAttribute('normal', new THREE.BufferAttribute(mergedNorm, 3));
625
- if (mergedUv)
626
- mergedGeo.setAttribute('uv', new THREE.BufferAttribute(mergedUv, 2));
627
- if (mergedUv1)
628
- mergedGeo.setAttribute('uv1', new THREE.BufferAttribute(mergedUv1, 2));
629
- if (mergedColor)
630
- mergedGeo.setAttribute('aColor', new THREE.BufferAttribute(mergedColor, 3));
631
- if (mergedBatchId)
632
- mergedGeo.setAttribute('aBatchId', new THREE.BufferAttribute(mergedBatchId, 1));
633
- if (mergedRect0) {
634
- mergedGeo.setAttribute('aAtlasRect', new THREE.BufferAttribute(mergedRect0, 4));
635
- mergedGeo.setAttribute('aAtlasRect1', new THREE.BufferAttribute(mergedRect1, 4));
636
- mergedGeo.setAttribute('aSecUv', new THREE.BufferAttribute(mergedSecUv, 2));
637
- }
638
- mergedGeo.setIndex(new THREE.BufferAttribute(mergedIndices, 1));
639
- mergedGeo.computeBoundingBox();
640
- mergedGeo.computeBoundingSphere();
641
- const mesh = new THREE.Mesh(mergedGeo, material);
642
- mesh.userData.skeletonName = candidates[0].name;
643
- mesh.userData.geoRef = candidates[0].geo;
644
- mesh.userData.tileRef = tileRef;
645
- mesh.userData.primitiveType = 4;
646
- mesh.userData.isBatched = true;
647
- mesh.userData.batchedCount = candidates.length;
648
- if (geoByBatchId.size > 0) {
649
- mesh.userData.geoByBatchId = geoByBatchId;
650
- }
651
- return mesh;
247
+ if (prim.index)
248
+ geometry.setIndex(new THREE.BufferAttribute(prim.index, 1));
249
+ else
250
+ geometry.setDrawRange(0, prim.vertexCount);
251
+ geometry.boundingBox = new THREE.Box3(new THREE.Vector3(...prim.bounds.boxMin), new THREE.Vector3(...prim.bounds.boxMax));
252
+ geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(...prim.bounds.sphereCenter), prim.bounds.sphereRadius);
253
+ return geometry;
652
254
  };
653
- const placeObject = (cached, name, geo, matrix) => {
654
- const { geometry, pack } = cached;
655
- const material = makeMaterial(pack.passNames?.[0], !!geometry.getAttribute('aAtlasRect'));
656
- let object;
657
- const info = skeletonInfo.get(name);
658
- const vp = geo.vertexPackage;
255
+ for (const prim of assembled.primitives) {
256
+ const geometry = packPrimitive(prim);
257
+ const geoRef = tile.geoPackage[prim.geoNames[0]] ?? null;
258
+ const vp = geoRef?.vertexPackage;
259
+ const material = makeMaterial(prim.passNames[0], !!prim.attributes.aAtlasRect);
260
+ const matrix = prim.matrix && prim.matrix.length === 16 ? new THREE.Matrix4().fromArray(prim.matrix) : null;
659
261
  // 棱线包:线段,不是三角面。默认隐藏,等 setEdges(true) 再显示
660
- if (isLinePrimitive(pack.primitiveType)) {
262
+ if (isLinePrimitive(prim.primitiveType)) {
661
263
  const lines = new THREE.LineSegments(geometry, edgeLineMaterial);
662
264
  lines.visible = options.edges === true;
663
265
  lines.renderOrder = 2;
664
266
  lines.raycast = () => { };
665
267
  lines.userData.isEdgeLines = true;
666
- lines.userData.skeletonName = name;
268
+ lines.userData.skeletonName = prim.skeletonName;
667
269
  if (matrix && !matrix.equals(IDENTITY)) {
668
270
  matrix.decompose(lines.position, lines.quaternion, lines.scale);
669
271
  }
670
272
  group.add(lines);
671
273
  objects.push(lines);
672
274
  group.userData.hasNativeEdges = true;
673
- stats.edgeLines++;
674
- return lines;
275
+ continue;
675
276
  }
676
277
  // 点云包:顶点即点,按 THREE.Points 渲染。索引只是点序号,直接走 drawRange。
677
- if (isPointPrimitive(pack.primitiveType)) {
278
+ if (isPointPrimitive(prim.primitiveType)) {
678
279
  // PointsMaterial 的顶点色读内置 'color' 槽位,与 aColor 共享同一份 buffer
679
280
  if (!geometry.getAttribute('color')) {
680
281
  geometry.setAttribute('color', geometry.getAttribute('aColor'));
@@ -682,23 +283,28 @@ export async function buildTileObjects(tile, options = {}) {
682
283
  const points = new THREE.Points(geometry, getPointCloudMaterial());
683
284
  // 点云不是 Mesh:viewer 的样式 / 拾取 / 卸载链路都靠这个标记放行
684
285
  points.userData.isPointCloud = true;
685
- points.userData.skeletonName = name;
686
- points.userData.geoRef = geo;
286
+ points.userData.skeletonName = prim.skeletonName;
287
+ points.userData.geoRef = geoRef;
687
288
  points.userData.tileRef = tile;
688
- points.userData.primitiveType = pack.primitiveType;
289
+ points.userData.primitiveType = prim.primitiveType;
689
290
  if (matrix && !matrix.equals(IDENTITY)) {
690
291
  matrix.decompose(points.position, points.quaternion, points.scale);
691
292
  }
692
293
  group.add(points);
693
294
  objects.push(points);
694
295
  stats.meshes++;
695
- return points;
296
+ continue;
696
297
  }
697
- if (info?.instanced && vp.instanceBuffer && vp.instanceCount > 0) {
298
+ let object;
299
+ const instanced = !!vp &&
300
+ vp.instanceIndex !== -1 &&
301
+ (vp.instanceMode === 16 || vp.instanceMode === 17) &&
302
+ !!vp.instanceBuffer &&
303
+ (vp.instanceCount ?? 0) > 0;
304
+ if (instanced && vp) {
698
305
  // 逐实例的两条属性:
699
306
  // aInstanceBatchId —— 单体控制位,着色器靠它查状态纹理(改色 / 隐藏 / 抬高)
700
307
  // aInstanceIndex —— 实例序号,拾取时编码进颜色,CPU 侧再映射回 SMID
701
- // 逐顶点属性 aBatchId 在这里没用(顶点数 ≠ 实例数),gpu 上按实例取值只能靠 instanced attribute。
702
308
  if (!geometry.getAttribute('aInstanceBatchId')) {
703
309
  const ids = vp.instanceBatchIds;
704
310
  const batchIds = ids && ids.length === vp.instanceCount ? ids.slice() : new Float32Array(vp.instanceCount).fill(-1);
@@ -708,40 +314,35 @@ export async function buildTileObjects(tile, options = {}) {
708
314
  index[i] = i;
709
315
  geometry.setAttribute('aInstanceIndex', new THREE.InstancedBufferAttribute(index, 1));
710
316
  }
711
- const instanced = new THREE.InstancedMesh(geometry, material, vp.instanceCount);
712
- instanced.instanceMatrix.setUsage(THREE.StaticDrawUsage);
317
+ const instancedMesh = new THREE.InstancedMesh(geometry, material, vp.instanceCount);
318
+ instancedMesh.instanceMatrix.setUsage(THREE.StaticDrawUsage);
713
319
  const m4 = new THREE.Matrix4();
714
320
  const color = new THREE.Color();
715
321
  const stride = vp.instanceMode;
716
322
  const f32 = new Float32Array(vp.instanceBuffer.buffer, vp.instanceBuffer.byteOffset, vp.instanceCount * stride);
717
323
  for (let i = 0; i < vp.instanceCount; i++) {
718
324
  const o = i * stride;
719
- // S3M 实例矩阵按 3x4 仿射矩阵(行主序)连续存储 12 个 float:
720
- // Row 0: [r00, r01, r02, tx]
721
- // Row 1: [r10, r11, r12, ty]
722
- // Row 2: [r20, r21, r22, tz]
723
- // Row 3: [0, 0, 0, 1] 隐式补齐
325
+ // S3M 实例矩阵按 3x4 仿射矩阵(行主序)连续存储 12 个 float(Row3 隐式补齐),
724
326
  // 后续紧随 4 个 float 的 RGBA 颜色(stride >= 16)及可选 uint32 的 SMID(stride == 17)。
725
327
  // 若用 fromArray 直接按列主序填充 Matrix4,会将平移分量塞进底部的投影行 (w),
726
328
  // 造成严重的透视奇点畸变(所有顶点拉扯成放射状尖锥)。
727
329
  m4.set(f32[o + 0], f32[o + 1], f32[o + 2], f32[o + 3], f32[o + 4], f32[o + 5], f32[o + 6], f32[o + 7], f32[o + 8], f32[o + 9], f32[o + 10], f32[o + 11], 0, 0, 0, 1);
728
- instanced.setMatrixAt(i, m4);
330
+ instancedMesh.setMatrixAt(i, m4);
729
331
  if (stride >= 16) {
730
332
  const r = f32[o + 12];
731
333
  const g = f32[o + 13];
732
334
  const b = f32[o + 14];
733
335
  if (Number.isFinite(r) && Number.isFinite(g) && Number.isFinite(b)) {
734
336
  color.setRGB(r, g, b);
735
- instanced.setColorAt(i, color);
337
+ instancedMesh.setColorAt(i, color);
736
338
  }
737
339
  }
738
340
  }
739
- instanced.instanceMatrix.needsUpdate = true;
740
- if (instanced.instanceColor)
741
- instanced.instanceColor.needsUpdate = true;
742
- instanced.frustumCulled = false;
743
- stats.instanced++;
744
- object = instanced;
341
+ instancedMesh.instanceMatrix.needsUpdate = true;
342
+ if (instancedMesh.instanceColor)
343
+ instancedMesh.instanceColor.needsUpdate = true;
344
+ instancedMesh.frustumCulled = false;
345
+ object = instancedMesh;
745
346
  }
746
347
  else {
747
348
  object = new THREE.Mesh(geometry, material);
@@ -751,112 +352,33 @@ export async function buildTileObjects(tile, options = {}) {
751
352
  if (matrix && !matrix.equals(IDENTITY)) {
752
353
  matrix.decompose(object.position, object.quaternion, object.scale);
753
354
  }
754
- object.userData.skeletonName = name;
755
- object.userData.geoRef = geo;
355
+ object.userData.skeletonName = prim.skeletonName;
356
+ object.userData.geoRef = geoRef;
756
357
  object.userData.tileRef = tile;
757
- object.userData.primitiveType = pack.primitiveType;
758
- group.add(object);
759
- objects.push(object);
760
- stats.meshes++;
761
- if (pack.primitiveType === 4)
762
- stats.triangles += (pack.indexCount || 0) / 3;
763
- return object;
764
- };
765
- // 4) 按 Geode 摆放:一个骨架可能被多个 geode 以不同矩阵引用
766
- let placed = 0;
767
- for (const pageLod of tile.groupNode?.pageLods ?? []) {
768
- for (const geode of pageLod.geodes ?? []) {
769
- const m = geode.matrix;
770
- // S3M 的 matrix4d 是列主序(OpenGL 约定),平移在第 12/13/14 位,
771
- // 与 three 的 Matrix4.elements 布局一致 —— 必须用 fromArray 直接搬。
772
- // 用 set() 按行主序喂进去等于转置,平移会被塞进投影行,模型直接飞上天。
773
- const matrix = m && m.length === 16 ? new THREE.Matrix4().fromArray(m) : null;
774
- for (const name of geode.skeletonNames ?? []) {
775
- const info = skeletonInfo.get(name);
776
- if (!info)
777
- continue;
778
- const geo = tile.geoPackage[name];
779
- if (!geo)
780
- continue;
781
- const vp = geo.vertexPackage;
782
- for (let pi = 0; pi < info.indexPackages.length; pi++) {
783
- const cached = geoCache.get(`${name}|${pi}`);
784
- if (!cached)
785
- continue;
786
- // 棱线包或实例化网格:保持单独摆放
787
- if (isLinePrimitive(cached.pack.primitiveType) ||
788
- (info.instanced && vp.instanceBuffer && vp.instanceCount > 0)) {
789
- placeObject(cached, name, geo, matrix);
790
- placed++;
791
- continue;
792
- }
793
- // 普通三角面网格:若开启合批,按材质进行分组收集
794
- if (options.batching !== false && cached.pack.primitiveType === 4) {
795
- const matKey = cached.pack.passNames?.[0] ?? '__default__';
796
- let list = batchGroups.get(matKey);
797
- if (!list) {
798
- list = [];
799
- batchGroups.set(matKey, list);
800
- }
801
- list.push({ cached, name, geo, matrix });
802
- placed++;
803
- continue;
804
- }
805
- placeObject(cached, name, geo, matrix);
806
- placed++;
807
- }
808
- }
809
- }
810
- }
811
- // 兜底:没有被任何 geode 引用的骨架,按单位矩阵渲染,避免整块消失
812
- if (placed === 0) {
813
- for (const [name, info] of skeletonInfo) {
814
- const geo = tile.geoPackage[name];
815
- if (!geo)
816
- continue;
817
- const vp = geo.vertexPackage;
818
- for (let pi = 0; pi < info.indexPackages.length; pi++) {
819
- const cached = geoCache.get(`${name}|${pi}`);
820
- if (!cached)
821
- continue;
822
- if (isLinePrimitive(cached.pack.primitiveType) ||
823
- (info.instanced && vp.instanceBuffer && vp.instanceCount > 0)) {
824
- placeObject(cached, name, geo, null);
358
+ object.userData.primitiveType = prim.primitiveType;
359
+ if (prim.batchedFrom != null && prim.batchedFrom > 1) {
360
+ object.userData.isBatched = true;
361
+ object.userData.batchedCount = prim.batchedFrom;
362
+ // 合并产物按 batchId 反查 geoRef(拾取用):原实现在合并时顺手建,这里从 geoNames 重建
363
+ const geoByBatchId = new Map();
364
+ for (const gname of prim.geoNames) {
365
+ const g = tile.geoPackage[gname];
366
+ if (!g?.pickInfo)
825
367
  continue;
826
- }
827
- if (options.batching !== false && cached.pack.primitiveType === 4) {
828
- const matKey = cached.pack.passNames?.[0] ?? '__default__';
829
- let list = batchGroups.get(matKey);
830
- if (!list) {
831
- list = [];
832
- batchGroups.set(matKey, list);
368
+ for (const entry of Object.values(g.pickInfo)) {
369
+ if (typeof entry.batchId === 'number' && entry.batchId >= 0) {
370
+ geoByBatchId.set(entry.batchId, g);
833
371
  }
834
- list.push({ cached, name, geo, matrix: null });
835
- continue;
836
- }
837
- placeObject(cached, name, geo, null);
838
- }
839
- }
840
- }
841
- // 执行同材质几何合批
842
- if (batchGroups.size > 0) {
843
- for (const [matKey, list] of batchGroups) {
844
- stats.originalMeshes = (stats.originalMeshes ?? 0) + list.length;
845
- if (list.length === 1) {
846
- placeObject(list[0].cached, list[0].name, list[0].geo, list[0].matrix);
847
- }
848
- else {
849
- const hasAtlasRect = !!list[0].cached.geometry.getAttribute('aAtlasRect');
850
- const material = makeMaterial(matKey === '__default__' ? undefined : matKey, hasAtlasRect);
851
- const batchedMesh = mergeBatchGeometries(list, material, tile);
852
- group.add(batchedMesh);
853
- objects.push(batchedMesh);
854
- stats.meshes++;
855
- for (const item of list) {
856
- stats.triangles += (item.cached.pack.indexCount || 0) / 3;
857
372
  }
858
373
  }
374
+ if (geoByBatchId.size > 0)
375
+ object.userData.geoByBatchId = geoByBatchId;
859
376
  }
377
+ group.add(object);
378
+ objects.push(object);
379
+ stats.meshes++;
380
+ if (prim.primitiveType === 4)
381
+ stats.triangles += prim.indexCount / 3;
860
382
  }
861
383
  if (!group.children.length)
862
384
  stats.skipped.push('tile 无可用几何');