s3m-three 0.3.5 → 0.3.7

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 (69) hide show
  1. package/dist/browser/assets/s3mWorker-D7AV6O30.js +1 -0
  2. package/dist/browser/s3m-three.iife.ext-three.js +26 -20
  3. package/dist/browser/s3m-three.iife.js +26 -20
  4. package/dist/core/TextureDecode.d.ts +39 -0
  5. package/dist/core/TextureDecode.d.ts.map +1 -0
  6. package/dist/core/TextureDecode.js +174 -0
  7. package/dist/core/TextureDecode.js.map +1 -0
  8. package/dist/core/TextureFormat.d.ts +22 -2
  9. package/dist/core/TextureFormat.d.ts.map +1 -1
  10. package/dist/core/TextureFormat.js +22 -2
  11. package/dist/core/TextureFormat.js.map +1 -1
  12. package/dist/core/TileGeometry.d.ts +92 -0
  13. package/dist/core/TileGeometry.d.ts.map +1 -0
  14. package/dist/core/TileGeometry.js +607 -0
  15. package/dist/core/TileGeometry.js.map +1 -0
  16. package/dist/render/DatasetManager.d.ts +2 -0
  17. package/dist/render/DatasetManager.d.ts.map +1 -1
  18. package/dist/render/DatasetManager.js +1 -0
  19. package/dist/render/DatasetManager.js.map +1 -1
  20. package/dist/render/FeatureStyleManager.d.ts +4 -2
  21. package/dist/render/FeatureStyleManager.d.ts.map +1 -1
  22. package/dist/render/FeatureStyleManager.js +33 -1
  23. package/dist/render/FeatureStyleManager.js.map +1 -1
  24. package/dist/render/GeometryBuilder.d.ts +8 -10
  25. package/dist/render/GeometryBuilder.d.ts.map +1 -1
  26. package/dist/render/GeometryBuilder.js +77 -555
  27. package/dist/render/GeometryBuilder.js.map +1 -1
  28. package/dist/render/S3MViewer.d.ts +4 -0
  29. package/dist/render/S3MViewer.d.ts.map +1 -1
  30. package/dist/render/S3MViewer.js +13 -2
  31. package/dist/render/S3MViewer.js.map +1 -1
  32. package/dist/render/StyleMaterial.d.ts.map +1 -1
  33. package/dist/render/StyleMaterial.js +6 -0
  34. package/dist/render/StyleMaterial.js.map +1 -1
  35. package/dist/render/TextureFactory.d.ts +9 -19
  36. package/dist/render/TextureFactory.d.ts.map +1 -1
  37. package/dist/render/TextureFactory.js +62 -115
  38. package/dist/render/TextureFactory.js.map +1 -1
  39. package/dist/tileset/S3MTileset.d.ts +21 -0
  40. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  41. package/dist/tileset/S3MTileset.js +41 -2
  42. package/dist/tileset/S3MTileset.js.map +1 -1
  43. package/dist/worker/S3MWorkerClient.d.ts +48 -0
  44. package/dist/worker/S3MWorkerClient.d.ts.map +1 -0
  45. package/dist/worker/S3MWorkerClient.js +124 -0
  46. package/dist/worker/S3MWorkerClient.js.map +1 -0
  47. package/dist/worker/s3mWorker.d.ts +2 -0
  48. package/dist/worker/s3mWorker.d.ts.map +1 -0
  49. package/dist/worker/s3mWorker.js +56 -0
  50. package/dist/worker/s3mWorker.js.map +1 -0
  51. package/dist/worker/transferables.d.ts +14 -0
  52. package/dist/worker/transferables.d.ts.map +1 -0
  53. package/dist/worker/transferables.js +61 -0
  54. package/dist/worker/transferables.js.map +1 -0
  55. package/docs/API.md +6 -0
  56. package/package.json +1 -1
  57. package/src/core/TextureDecode.ts +224 -0
  58. package/src/core/TextureFormat.ts +23 -2
  59. package/src/core/TileGeometry.ts +745 -0
  60. package/src/render/DatasetManager.ts +3 -0
  61. package/src/render/FeatureStyleManager.ts +31 -2
  62. package/src/render/GeometryBuilder.ts +91 -617
  63. package/src/render/S3MViewer.ts +18 -4
  64. package/src/render/StyleMaterial.ts +6 -0
  65. package/src/render/TextureFactory.ts +78 -123
  66. package/src/tileset/S3MTileset.ts +55 -3
  67. package/src/worker/S3MWorkerClient.ts +159 -0
  68. package/src/worker/s3mWorker.ts +76 -0
  69. package/src/worker/transferables.ts +58 -0
@@ -0,0 +1,745 @@
1
+ /**
2
+ * S3M 解析结果 → 纯数据几何组装(three 无关,主线程 / Worker 共用)
3
+ *
4
+ * 从 GeometryBuilder 拆出来的 CPU 密集段:顶点流解交错、法线计算、
5
+ * 同材质合批合并(含 geode 矩阵变换)。产出是「PrimitiveData 列表」——
6
+ * 每个 primitive 就是一份能直接包成 BufferGeometry 的 attribute 字典 + 索引,
7
+ * three 对象(Mesh / 材质 / InstancedMesh)仍由渲染层的 GeometryBuilder 包装。
8
+ *
9
+ * 语义必须与原实现逐位一致(下游拾取 / 单体化 / 图集都依赖):
10
+ * - attribute 省缺补默认:aColor 补白、aBatchId 补 -1、无 index 走 drawRange;
11
+ * - 点云骨架(无索引包)补一个 primitiveType=1 的伪包;
12
+ * - 合批只在 primitiveType=4 且 batching 开启时做,棱线包与实例化网格单独摆放;
13
+ * - 合并时矩阵变换用列主序 mat4(S3M 与 three elements 布局一致),
14
+ * 法线用上 3x3 的逆转置并归一化。
15
+ */
16
+ import type {
17
+ AttrDType,
18
+ GeoPackageEntry,
19
+ IndexPackage,
20
+ ParsedTile,
21
+ VertexAttribute,
22
+ } from './types.js';
23
+
24
+ /**
25
+ * S3M 规范里 `DrawPrimitiveType` 的两个特殊值(原住 GeometryBuilder,随组装层
26
+ * 下沉到 core —— 渲染层从这里 re-export,对外 API 不变):
27
+ * 13 = 数据自带的「棱线包」—— 索引是线段对,不是三角形(把 13 当三角形画,
28
+ * 每条棱线都会被连成一根几十上百米的尖刺);
29
+ * 1 = 点云包。索引是点序号,画成三角面就是满屏彩色尖刺。
30
+ */
31
+ export const LINE_PRIMITIVE = 13;
32
+ export const POINT_PRIMITIVE = 1;
33
+
34
+ /* ------------------------------------------------------------------ 顶点流工具 */
35
+
36
+ function bytesPerElement(dtype: AttrDType): number {
37
+ return dtype === 'u8' ? 1 : dtype === 'u16' ? 2 : 4;
38
+ }
39
+
40
+ /** 解交错后的属性流:数组 + 分量数 + 是否归一化(对应 THREE.BufferAttribute 三参) */
41
+ export interface PlainAttribute {
42
+ array: Float32Array | Uint8Array | Uint16Array | Uint32Array;
43
+ components: number;
44
+ normalized: boolean;
45
+ }
46
+
47
+ function typedArray(dtype: AttrDType, buffer: ArrayBufferLike): Float32Array | Uint8Array | Uint16Array | Uint32Array {
48
+ switch (dtype) {
49
+ case 'u8':
50
+ return new Uint8Array(buffer);
51
+ case 'u16':
52
+ return new Uint16Array(buffer);
53
+ case 'u32':
54
+ return new Uint32Array(buffer);
55
+ default:
56
+ return new Float32Array(buffer);
57
+ }
58
+ }
59
+
60
+ function typedArrayOf(dtype: AttrDType, length: number): Float32Array | Uint8Array | Uint16Array | Uint32Array {
61
+ switch (dtype) {
62
+ case 'u8':
63
+ return new Uint8Array(length);
64
+ case 'u16':
65
+ return new Uint16Array(length);
66
+ case 'u32':
67
+ return new Uint32Array(length);
68
+ default:
69
+ return new Float32Array(length);
70
+ }
71
+ }
72
+
73
+ /** 非交错整段铺开 / 交错逐元素抽取(原 toBufferAttribute 的数据部分) */
74
+ export function deinterleave(attr: VertexAttribute): PlainAttribute {
75
+ const { data, stride, offsetInBytes, dtype = 'f32' } = attr;
76
+ const components = attr.components ?? (attr as unknown as { comp?: number }).comp ?? 3;
77
+ const bpe = bytesPerElement(dtype);
78
+ const itemBytes = components * bpe;
79
+ const actualStride = stride ?? itemBytes;
80
+ const actualOffset = offsetInBytes ?? 0;
81
+ const normalized = dtype === 'u8';
82
+
83
+ if (actualStride === itemBytes && actualOffset === 0) {
84
+ const view = typedArray(dtype, data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength));
85
+ return { array: view, components, normalized };
86
+ }
87
+
88
+ const count = Math.floor(data.byteLength / actualStride);
89
+ const out = typedArrayOf(dtype, count * components);
90
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
91
+ for (let i = 0; i < count; i++) {
92
+ const base = i * actualStride + actualOffset;
93
+ for (let c = 0; c < components; c++) {
94
+ const o = base + c * bpe;
95
+ out[i * components + c] =
96
+ dtype === 'u8'
97
+ ? dv.getUint8(o)
98
+ : dtype === 'u16'
99
+ ? dv.getUint16(o, true)
100
+ : dtype === 'u32'
101
+ ? dv.getUint32(o, true)
102
+ : dv.getFloat32(o, true);
103
+ }
104
+ }
105
+ return { array: out, components, normalized };
106
+ }
107
+
108
+ /** 把 u8 顶点色转成 float vec3,供着色器直接相乘(原 toColorAttribute) */
109
+ function colorData(attr: VertexAttribute | null, vertexCount: number): Float32Array {
110
+ const out = new Float32Array(vertexCount * 3);
111
+ if (!attr) {
112
+ out.fill(1);
113
+ return out;
114
+ }
115
+ const src = attr.data;
116
+ const comp = attr.components ?? 4;
117
+ const scale = attr.dtype === 'u8' ? 1 / 255 : 1;
118
+ for (let i = 0; i < vertexCount; i++) {
119
+ const o = i * comp;
120
+ out[i * 3] = (src[o] ?? 255) * scale;
121
+ out[i * 3 + 1] = (src[o + 1] ?? 255) * scale;
122
+ out[i * 3 + 2] = (src[o + 2] ?? 255) * scale;
123
+ }
124
+ return out;
125
+ }
126
+
127
+ /** 解出索引(原 toIndex 的数据部分),返回 null = 无索引走 drawRange */
128
+ function indexData(pack: IndexPackage): Uint16Array | Uint32Array | null {
129
+ const { indices, indexType, indexCount } = pack;
130
+ if (!indices || indexCount === 0) return null;
131
+ // 以字节宽度为准:S3M 的 indexType 在 MeshOpt 包里恒为 0,靠它会读错一半索引。
132
+ const width = Math.floor(indices.byteLength / indexCount);
133
+ const wide = width === 4 || (width !== 2 && (indexType === 1 || indexType === 3));
134
+ const n = indexCount * (wide ? 4 : 2);
135
+ if (n > indices.byteLength) return null;
136
+ const buf = indices.buffer.slice(indices.byteOffset, indices.byteOffset + n);
137
+ return wide ? new Uint32Array(buf) : new Uint16Array(buf);
138
+ }
139
+
140
+ /**
141
+ * 顶点法线计算(等价 BufferGeometry.computeVertexNormals:索引几何按面累积、
142
+ * 非索引逐面复制,最后归一化)。仅三角面在数据没带 normal 时调用。
143
+ */
144
+ function computeVertexNormals(
145
+ position: ArrayLike<number>,
146
+ index: Uint16Array | Uint32Array | null,
147
+ vertexCount: number
148
+ ): Float32Array {
149
+ const normals = new Float32Array(vertexCount * 3);
150
+ const addFace = (a: number, b: number, c: number): void => {
151
+ const ax = position[a * 3], ay = position[a * 3 + 1], az = position[a * 3 + 2];
152
+ const bx = position[b * 3], by = position[b * 3 + 1], bz = position[b * 3 + 2];
153
+ const cx = position[c * 3], cy = position[c * 3 + 1], cz = position[c * 3 + 2];
154
+ const e1x = bx - ax, e1y = by - ay, e1z = bz - az;
155
+ const e2x = cx - ax, e2y = cy - ay, e2z = cz - az;
156
+ const nx = e1y * e2z - e1z * e2y;
157
+ const ny = e1z * e2x - e1x * e2z;
158
+ const nz = e1x * e2y - e1y * e2x;
159
+ for (const v of [a, b, c]) {
160
+ normals[v * 3] += nx;
161
+ normals[v * 3 + 1] += ny;
162
+ normals[v * 3 + 2] += nz;
163
+ }
164
+ };
165
+ if (index) {
166
+ for (let i = 0; i + 2 < index.length; i += 3) {
167
+ addFace(index[i], index[i + 1], index[i + 2]);
168
+ }
169
+ } else {
170
+ for (let i = 0; i + 2 < vertexCount; i += 3) {
171
+ addFace(i, i + 1, i + 2);
172
+ }
173
+ }
174
+ for (let i = 0; i < vertexCount; i++) {
175
+ const o = i * 3;
176
+ const len = Math.hypot(normals[o], normals[o + 1], normals[o + 2]);
177
+ if (len > 0) {
178
+ normals[o] /= len;
179
+ normals[o + 1] /= len;
180
+ normals[o + 2] /= len;
181
+ }
182
+ }
183
+ return normals;
184
+ }
185
+
186
+ /* ------------------------------------------------------------------ 包围盒 / 包围球 */
187
+
188
+ export interface BoundsData {
189
+ boxMin: [number, number, number];
190
+ boxMax: [number, number, number];
191
+ sphereCenter: [number, number, number];
192
+ sphereRadius: number;
193
+ }
194
+
195
+ /** 等价 BufferGeometry.computeBoundingBox / computeBoundingSphere 的数学部分 */
196
+ export function computeBounds(position: ArrayLike<number>, vertexCount: number): BoundsData {
197
+ const min = [Infinity, Infinity, Infinity];
198
+ const max = [-Infinity, -Infinity, -Infinity];
199
+ for (let i = 0; i < vertexCount; i++) {
200
+ for (let c = 0; c < 3; c++) {
201
+ const v = position[i * 3 + c];
202
+ if (v < min[c]) min[c] = v;
203
+ if (v > max[c]) max[c] = v;
204
+ }
205
+ }
206
+ const center: [number, number, number] = [
207
+ (min[0] + max[0]) / 2,
208
+ (min[1] + max[1]) / 2,
209
+ (min[2] + max[2]) / 2,
210
+ ];
211
+ let radius = 0;
212
+ for (let i = 0; i < vertexCount; i++) {
213
+ const dx = position[i * 3] - center[0];
214
+ const dy = position[i * 3 + 1] - center[1];
215
+ const dz = position[i * 3 + 2] - center[2];
216
+ const d = dx * dx + dy * dy + dz * dz;
217
+ if (d > radius) radius = d;
218
+ }
219
+ return { boxMin: min as [number, number, number], boxMax: max as [number, number, number], sphereCenter: center, sphereRadius: Math.sqrt(radius) };
220
+ }
221
+
222
+ /* ------------------------------------------------------------------ 组装产物 */
223
+
224
+ /** 一个 primitive 的属性字典:键即 geometry attribute 名,数组/分量/归一化齐备 */
225
+ export interface PrimitiveAttributes {
226
+ position: PlainAttribute;
227
+ normal?: PlainAttribute;
228
+ uv?: PlainAttribute;
229
+ uv1?: PlainAttribute;
230
+ aColor: PlainAttribute;
231
+ aBatchId: PlainAttribute;
232
+ aAtlasRect?: PlainAttribute;
233
+ aAtlasRect1?: PlainAttribute;
234
+ aSecUv?: PlainAttribute;
235
+ }
236
+
237
+ export interface PrimitiveData {
238
+ /** 首个候选骨架名(userData.skeletonName) */
239
+ skeletonName: string;
240
+ /** 参与合并的 geoPackage 键(主线程重建 geoRef / geoByBatchId) */
241
+ geoNames: string[];
242
+ primitiveType: number;
243
+ passNames: string[];
244
+ /** geode 矩阵(列主序 16 个数)或 null */
245
+ matrix: number[] | null;
246
+ vertexCount: number;
247
+ indexCount: number;
248
+ attributes: PrimitiveAttributes;
249
+ index: Uint16Array | Uint32Array | null;
250
+ bounds: BoundsData;
251
+ /** 合批前候选数(>1 表示是合并产物) */
252
+ batchedFrom?: number;
253
+ }
254
+
255
+ export interface TileGeometryStats {
256
+ vertices: number;
257
+ instanced: number;
258
+ edgeLines: number;
259
+ /** 合批前独立网格数(batching 生效时 > meshes) */
260
+ originalMeshes?: number;
261
+ skipped: string[];
262
+ }
263
+
264
+ export interface TileGeometryData {
265
+ primitives: PrimitiveData[];
266
+ stats: TileGeometryStats;
267
+ }
268
+
269
+ export interface AssembleOptions {
270
+ /** 是否对 primitiveType=4 的三角面做瓦片内同材质合批(默认 true) */
271
+ batching?: boolean;
272
+ }
273
+
274
+ /* ------------------------------------------------------------------ 合批合并 */
275
+
276
+ interface Candidate {
277
+ attrs: PrimitiveAttributes;
278
+ index: Uint16Array | Uint32Array | null;
279
+ vertexCount: number;
280
+ indexCount: number;
281
+ name: string;
282
+ geo: GeoPackageEntry;
283
+ matrix: number[] | null;
284
+ passNames: string[];
285
+ primitiveType: number;
286
+ }
287
+
288
+ const LINE_PRIMITIVES = [LINE_PRIMITIVE, 2, 3];
289
+ const POINT_PRIMITIVE_TYPE = POINT_PRIMITIVE;
290
+
291
+ function isLinePrimitive(primitiveType: number): boolean {
292
+ return LINE_PRIMITIVES.includes(primitiveType);
293
+ }
294
+
295
+ /** 列主序 mat4 是否为单位阵(等价 THREE.Matrix4.equals(IDENTITY)) */
296
+ function isIdentity(m: number[] | null): boolean {
297
+ if (!m) return true;
298
+ return (
299
+ m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 0 &&
300
+ m[4] === 0 && m[5] === 1 && m[6] === 0 && m[7] === 0 &&
301
+ m[8] === 0 && m[9] === 0 && m[10] === 1 && m[11] === 0 &&
302
+ m[12] === 0 && m[13] === 0 && m[14] === 0 && m[15] === 1
303
+ );
304
+ }
305
+
306
+ /** 列主序 mat4 变换点(仿射,忽略投影行——与 applyMatrix4 对点一致) */
307
+ function xformPoint(m: number[], x: number, y: number, z: number, out: number[]): void {
308
+ out[0] = m[0] * x + m[4] * y + m[8] * z + m[12];
309
+ out[1] = m[1] * x + m[5] * y + m[9] * z + m[13];
310
+ out[2] = m[2] * x + m[6] * y + m[10] * z + m[14];
311
+ }
312
+
313
+ /**
314
+ * 上 3x3 的逆转置(normal matrix),列主序展开。
315
+ * 等价 THREE.Matrix3.getNormalMatrix,纯标量实现,不依赖 three。
316
+ */
317
+ function normalMatrixOf(m: number[]): Float32Array {
318
+ // 上 3x3(列主序):a=m0 b=m4 c=m8 / d=m1 e=m5 f=m9 / g=m2 h=m6 i=m10
319
+ const a = m[0], b = m[4], c = m[8];
320
+ const d = m[1], e = m[5], f = m[9];
321
+ const g = m[2], h = m[6], i = m[10];
322
+ const A = e * i - f * h, B = -(d * i - f * g), C = d * h - e * g;
323
+ const D = -(b * i - c * h), E = a * i - c * g, F = -(a * h - b * g);
324
+ const G = b * f - c * e, H = -(a * f - c * d), I = a * e - b * d;
325
+ const det = a * A + b * B + c * C || 1e-12;
326
+ // 逆转置 = inv 上 3x3 的转置;inv 的元素 = cofactor / det(行主序),转置后回列主序
327
+ const inv = [A / det, B / det, C / det, D / det, E / det, F / det, G / det, H / det, I / det];
328
+ // 转置回列主序布局:n[0..2] 是第一列
329
+ const n = new Float32Array(9);
330
+ n[0] = inv[0]; n[1] = inv[3]; n[2] = inv[6];
331
+ n[3] = inv[1]; n[4] = inv[4]; n[5] = inv[7];
332
+ n[6] = inv[2]; n[7] = inv[5]; n[8] = inv[8];
333
+ return n;
334
+ }
335
+
336
+ /** 逆转置 3x3 变换法线并归一化 */
337
+ function xformNormal(n: Float32Array, x: number, y: number, z: number, out: number[]): void {
338
+ out[0] = n[0] * x + n[3] * y + n[6] * z;
339
+ out[1] = n[1] * x + n[4] * y + n[7] * z;
340
+ out[2] = n[2] * x + n[5] * y + n[8] * z;
341
+ const len = Math.hypot(out[0], out[1], out[2]) || 1;
342
+ out[0] /= len;
343
+ out[1] /= len;
344
+ out[2] /= len;
345
+ }
346
+
347
+ /**
348
+ * 合并同材质候选(等价 GeometryBuilder.mergeBatchGeometries 的数据部分)。
349
+ * 矩阵变换烘进顶点;合出的 primitive 自身 matrix 为 null。
350
+ */
351
+ function mergeCandidates(candidates: Candidate[]): PrimitiveData {
352
+ let totalVertices = 0;
353
+ let totalIndices = 0;
354
+ let hasNormal = false;
355
+ let hasUv = false;
356
+ let hasUv1 = false;
357
+ let hasAtlas = false;
358
+
359
+ for (const c of candidates) {
360
+ totalVertices += c.vertexCount;
361
+ totalIndices += c.index ? c.indexCount : c.vertexCount;
362
+ if (c.attrs.normal) hasNormal = true;
363
+ if (c.attrs.uv) hasUv = true;
364
+ if (c.attrs.uv1) hasUv1 = true;
365
+ if (c.attrs.aAtlasRect) hasAtlas = true;
366
+ }
367
+
368
+ const mergedPos: PlainAttribute = { array: new Float32Array(totalVertices * 3), components: 3, normalized: false };
369
+ const mergedNorm = hasNormal ? { array: new Float32Array(totalVertices * 3), components: 3, normalized: false } : null;
370
+ const mergedUv = hasUv ? { array: new Float32Array(totalVertices * 2), components: 2, normalized: false } : null;
371
+ const mergedUv1 = hasUv1 ? { array: new Float32Array(totalVertices * 2), components: 2, normalized: false } : null;
372
+ const mergedColor: PlainAttribute = { array: new Float32Array(totalVertices * 3), components: 3, normalized: false };
373
+ const mergedBatchId: PlainAttribute = { array: new Float32Array(totalVertices), components: 1, normalized: false };
374
+ const mergedRect0 = hasAtlas ? { array: new Float32Array(totalVertices * 4), components: 4, normalized: false } : null;
375
+ const mergedRect1 = hasAtlas ? { array: new Float32Array(totalVertices * 4), components: 4, normalized: false } : null;
376
+ const mergedSecUv = hasAtlas ? { array: new Float32Array(totalVertices * 2), components: 2, normalized: false } : null;
377
+ const mergedIndices = (totalVertices > 65535 ? new Uint32Array(totalIndices) : new Uint16Array(totalIndices)) as Uint16Array | Uint32Array;
378
+
379
+ const tmp = [0, 0, 0];
380
+ let vertexOffset = 0;
381
+ let indexOffset = 0;
382
+
383
+ for (const c of candidates) {
384
+ const { attrs } = c;
385
+ const vCount = c.vertexCount;
386
+ const m = isIdentity(c.matrix) ? null : c.matrix;
387
+ const nrm = m && mergedNorm ? normalMatrixOf(m) : null;
388
+ const posArr = attrs.position.array;
389
+ const normArr = attrs.normal?.array;
390
+ const uvArr = attrs.uv?.array;
391
+ const uv1Arr = attrs.uv1?.array;
392
+ const colArr = attrs.aColor.array;
393
+ const bArr = attrs.aBatchId.array;
394
+ const posOut = mergedPos.array as Float32Array;
395
+ const normOut = mergedNorm?.array as Float32Array | undefined;
396
+ const uvOut = mergedUv?.array as Float32Array | undefined;
397
+ const uv1Out = mergedUv1?.array as Float32Array | undefined;
398
+ const colOut = mergedColor.array as Float32Array;
399
+ const bOut = mergedBatchId.array as Float32Array;
400
+
401
+ for (let i = 0; i < vCount; i++) {
402
+ const dst = vertexOffset + i;
403
+ // position
404
+ if (m) {
405
+ xformPoint(m, posArr[i * 3], posArr[i * 3 + 1], posArr[i * 3 + 2], tmp);
406
+ posOut[dst * 3] = tmp[0];
407
+ posOut[dst * 3 + 1] = tmp[1];
408
+ posOut[dst * 3 + 2] = tmp[2];
409
+ } else {
410
+ posOut[dst * 3] = posArr[i * 3];
411
+ posOut[dst * 3 + 1] = posArr[i * 3 + 1];
412
+ posOut[dst * 3 + 2] = posArr[i * 3 + 2];
413
+ }
414
+ // normal
415
+ if (normOut) {
416
+ if (normArr && nrm) {
417
+ xformNormal(nrm, normArr[i * 3], normArr[i * 3 + 1], normArr[i * 3 + 2], tmp);
418
+ normOut[dst * 3] = tmp[0];
419
+ normOut[dst * 3 + 1] = tmp[1];
420
+ normOut[dst * 3 + 2] = tmp[2];
421
+ } else if (normArr) {
422
+ normOut[dst * 3] = normArr[i * 3];
423
+ normOut[dst * 3 + 1] = normArr[i * 3 + 1];
424
+ normOut[dst * 3 + 2] = normArr[i * 3 + 2];
425
+ }
426
+ }
427
+ // uv / uv1
428
+ if (uvOut && uvArr) {
429
+ uvOut[dst * 2] = uvArr[i * 2];
430
+ uvOut[dst * 2 + 1] = uvArr[i * 2 + 1];
431
+ }
432
+ if (uv1Out && uv1Arr) {
433
+ uv1Out[dst * 2] = uv1Arr[i * 2];
434
+ uv1Out[dst * 2 + 1] = uv1Arr[i * 2 + 1];
435
+ }
436
+ // aColor
437
+ if (colArr) {
438
+ colOut[dst * 3] = colArr[i * 3];
439
+ colOut[dst * 3 + 1] = colArr[i * 3 + 1];
440
+ colOut[dst * 3 + 2] = colArr[i * 3 + 2];
441
+ } else {
442
+ colOut[dst * 3] = 1;
443
+ colOut[dst * 3 + 1] = 1;
444
+ colOut[dst * 3 + 2] = 1;
445
+ }
446
+ // aBatchId
447
+ bOut[dst] = bArr ? bArr[i] : -1;
448
+ }
449
+
450
+ // 图集矩形(缺失补 0,等价原 copyAttr 的 fallback)
451
+ if (mergedRect0) {
452
+ const r0 = attrs.aAtlasRect?.array;
453
+ const r1 = attrs.aAtlasRect1?.array;
454
+ const su = attrs.aSecUv?.array;
455
+ const r0Out = mergedRect0.array as Float32Array;
456
+ const r1Out = mergedRect1!.array as Float32Array;
457
+ const suOut = mergedSecUv!.array as Float32Array;
458
+ for (let i = 0; i < vCount; i++) {
459
+ const dst = (vertexOffset + i) * 4;
460
+ for (let k = 0; k < 4; k++) {
461
+ r0Out[dst + k] = r0 ? r0[i * 4 + k] ?? 0 : 0;
462
+ r1Out[dst + k] = r1 ? r1[i * 4 + k] ?? 0 : 0;
463
+ }
464
+ suOut[(vertexOffset + i) * 2] = su ? su[i * 2] ?? 0 : 0;
465
+ suOut[(vertexOffset + i) * 2 + 1] = su ? su[i * 2 + 1] ?? 0 : 0;
466
+ }
467
+ }
468
+
469
+ // index(偏移累加;无索引按序号)
470
+ if (c.index) {
471
+ for (let k = 0; k < c.indexCount; k++) {
472
+ mergedIndices[indexOffset + k] = c.index[k] + vertexOffset;
473
+ }
474
+ indexOffset += c.indexCount;
475
+ } else {
476
+ for (let k = 0; k < vCount; k++) {
477
+ mergedIndices[indexOffset + k] = k + vertexOffset;
478
+ }
479
+ indexOffset += vCount;
480
+ }
481
+
482
+ vertexOffset += vCount;
483
+ }
484
+
485
+ const bounds = computeBounds(mergedPos.array, totalVertices);
486
+ return {
487
+ skeletonName: candidates[0].name,
488
+ geoNames: candidates.map((c) => c.name),
489
+ primitiveType: 4,
490
+ passNames: candidates[0].passNames,
491
+ matrix: null,
492
+ vertexCount: totalVertices,
493
+ indexCount: totalIndices,
494
+ attributes: {
495
+ position: mergedPos,
496
+ normal: mergedNorm ?? undefined,
497
+ uv: mergedUv ?? undefined,
498
+ uv1: mergedUv1 ?? undefined,
499
+ aColor: mergedColor,
500
+ aBatchId: mergedBatchId,
501
+ aAtlasRect: mergedRect0 ?? undefined,
502
+ aAtlasRect1: mergedRect1 ?? undefined,
503
+ aSecUv: mergedSecUv ?? undefined,
504
+ },
505
+ index: mergedIndices,
506
+ bounds,
507
+ batchedFrom: candidates.length,
508
+ };
509
+ }
510
+
511
+ /* ------------------------------------------------------------------ 入口 */
512
+
513
+ /** 单个骨架解交错后的属性与索引包(按 pack 分) */
514
+ interface SkeletonAsm {
515
+ attrs: PrimitiveAttributes;
516
+ packs: IndexPackage[];
517
+ instanced: boolean;
518
+ }
519
+
520
+ /** 每骨架只解一次交错流(同一骨架被多个 pack / geode 引用时共享同一份数据) */
521
+ function assembleSkeleton(name: string, geo: GeoPackageEntry, tile: ParsedTile, skipped: string[]): SkeletonAsm | null {
522
+ const vp = geo.vertexPackage;
523
+ const positionAttr = vp.vertexAttributes.find((a) => a.semantic === 'position');
524
+ if (!positionAttr) {
525
+ skipped.push(`${name} 无 position`);
526
+ return null;
527
+ }
528
+
529
+ const vertexCount = vp.verticesCount || 0;
530
+ const attrs: PrimitiveAttributes = {
531
+ position: deinterleave(positionAttr),
532
+ aColor: { array: new Float32Array(0), components: 3, normalized: false },
533
+ aBatchId: { array: new Float32Array(0), components: 1, normalized: false },
534
+ };
535
+ let colorAttr: VertexAttribute | null = null;
536
+ let batchAttr: VertexAttribute | null = null;
537
+ let normalData: PlainAttribute | null = null;
538
+ let uvData: PlainAttribute | null = null;
539
+ let uv1Data: PlainAttribute | null = null;
540
+
541
+ for (const attr of vp.vertexAttributes) {
542
+ if (attr.divisor !== undefined) continue; // 实例属性不进顶点流
543
+ if (attr.semantic === 'position') continue;
544
+ if (attr.semantic === 'color') colorAttr = attr;
545
+ else if (attr.semantic === 'batchId') batchAttr = attr;
546
+ else if (attr.semantic === 'normal') normalData = deinterleave(attr);
547
+ else if (attr.semantic === 'uv') uvData = deinterleave(attr);
548
+ else if (attr.semantic === 'uv2') uv1Data = deinterleave(attr);
549
+ }
550
+
551
+ attrs.aColor = { array: colorData(colorAttr, vertexCount), components: 3, normalized: false };
552
+ attrs.aBatchId = batchAttr
553
+ ? { array: deinterleave(batchAttr).array, components: 1, normalized: false }
554
+ : { array: new Float32Array(vertexCount).fill(-1), components: 1, normalized: false };
555
+ if (normalData) attrs.normal = normalData;
556
+ if (uvData) attrs.uv = uvData;
557
+ if (uv1Data) attrs.uv1 = uv1Data;
558
+
559
+ // S3MBLOCK 图集:逐顶点带子区域矩形;烘焙图用第二套 UV(aSecUv)
560
+ const rects = tile.atlasRects?.[name];
561
+ if (rects) {
562
+ attrs.aAtlasRect = { array: rects.uv ?? new Float32Array(vertexCount * 4), components: 4, normalized: false };
563
+ if (rects.uv2 && uv1Data) {
564
+ attrs.aAtlasRect1 = { array: rects.uv2, components: 4, normalized: false };
565
+ attrs.aSecUv = { array: uv1Data.array, components: uv1Data.components, normalized: uv1Data.normalized };
566
+ } else {
567
+ attrs.aAtlasRect1 = { array: new Float32Array(vertexCount * 4), components: 4, normalized: false };
568
+ attrs.aSecUv = attrs.uv ?? { array: new Float32Array(vertexCount * 2), components: 2, normalized: false };
569
+ }
570
+ }
571
+
572
+ // 一个索引包都没有的骨架 = 纯点集(高斯泼溅 / 无索引点云):补「顶点即点」伪包
573
+ let packs = geo.indexPackages ?? [];
574
+ if (packs.length === 0 && vertexCount > 0) {
575
+ packs = [
576
+ {
577
+ indexCount: 0,
578
+ indexType: 0,
579
+ useIndex: 0,
580
+ primitiveType: POINT_PRIMITIVE_TYPE,
581
+ indices: null,
582
+ passNames: [],
583
+ },
584
+ ];
585
+ }
586
+
587
+ return {
588
+ attrs,
589
+ packs,
590
+ instanced: vp.instanceIndex !== -1 && (vp.instanceMode === 16 || vp.instanceMode === 17),
591
+ };
592
+ }
593
+
594
+ function primitiveOf(
595
+ skeletonName: string,
596
+ geoName: string,
597
+ geo: GeoPackageEntry,
598
+ asm: SkeletonAsm,
599
+ pack: IndexPackage,
600
+ matrix: number[] | null
601
+ ): PrimitiveData {
602
+ const vertexCount = geo.vertexPackage.verticesCount || 0;
603
+ const index = indexData(pack);
604
+ const attrs: PrimitiveAttributes = { ...asm.attrs };
605
+ // 数据没带法线的三角网格:必须按索引后的连续顶点算,否则法线朝向错乱甚至 NaN 死黑面
606
+ if (!isLinePrimitive(pack.primitiveType) && pack.primitiveType !== POINT_PRIMITIVE_TYPE && !attrs.normal) {
607
+ attrs.normal = {
608
+ array: computeVertexNormals(attrs.position.array, index, vertexCount),
609
+ components: 3,
610
+ normalized: false,
611
+ };
612
+ }
613
+ return {
614
+ skeletonName,
615
+ geoNames: [geoName],
616
+ primitiveType: pack.primitiveType,
617
+ passNames: pack.passNames ?? [],
618
+ matrix,
619
+ vertexCount,
620
+ indexCount: pack.indexCount || 0,
621
+ attributes: attrs,
622
+ index,
623
+ bounds: computeBounds(attrs.position.array, vertexCount),
624
+ };
625
+ }
626
+
627
+ /**
628
+ * 瓦片几何组装入口。与 GeometryBuilder 的摆放/分组逻辑逐位对齐,
629
+ * 但只产出纯数据 —— three 包装在渲染层。
630
+ */
631
+ export function assembleTileGeometry(tile: ParsedTile, options: AssembleOptions = {}): TileGeometryData {
632
+ const batching = options.batching !== false;
633
+ const skipped: string[] = [];
634
+ const stats: TileGeometryStats = { vertices: 0, instanced: 0, edgeLines: 0, skipped };
635
+
636
+ const skeletons = new Map<string, SkeletonAsm>();
637
+ for (const [name, geo] of Object.entries(tile.geoPackage ?? {})) {
638
+ const asm = assembleSkeleton(name, geo, tile, skipped);
639
+ if (asm) {
640
+ skeletons.set(name, asm);
641
+ stats.vertices += geo.vertexPackage.verticesCount || 0;
642
+ }
643
+ }
644
+
645
+ const primitives: PrimitiveData[] = [];
646
+ const batchGroups = new Map<string, Candidate[]>();
647
+ let placed = 0;
648
+
649
+ const emit = (p: PrimitiveData): void => {
650
+ if (isLinePrimitive(p.primitiveType)) stats.edgeLines++;
651
+ primitives.push(p);
652
+ };
653
+ const group = (candidate: Candidate): void => {
654
+ const matKey = candidate.passNames[0] ?? '__default__';
655
+ let list = batchGroups.get(matKey);
656
+ if (!list) {
657
+ list = [];
658
+ batchGroups.set(matKey, list);
659
+ }
660
+ list.push(candidate);
661
+ };
662
+
663
+ const placeOrGroup = (
664
+ name: string,
665
+ geo: GeoPackageEntry,
666
+ asm: SkeletonAsm,
667
+ pi: number,
668
+ matrix: number[] | null
669
+ ): void => {
670
+ const pack = asm.packs[pi];
671
+ const isInstanced = asm.instanced && !!geo.vertexPackage.instanceBuffer && (geo.vertexPackage.instanceCount ?? 0) > 0;
672
+ if (isLinePrimitive(pack.primitiveType) || isInstanced) {
673
+ if (isInstanced) stats.instanced++;
674
+ emit(primitiveOf(name, name, geo, asm, pack, matrix));
675
+ placed++;
676
+ return;
677
+ }
678
+ if (batching && pack.primitiveType === 4) {
679
+ group({
680
+ attrs: asm.attrs,
681
+ index: indexData(pack),
682
+ vertexCount: geo.vertexPackage.verticesCount || 0,
683
+ indexCount: pack.indexCount || 0,
684
+ name,
685
+ geo,
686
+ matrix,
687
+ passNames: pack.passNames ?? [],
688
+ primitiveType: pack.primitiveType,
689
+ });
690
+ placed++;
691
+ return;
692
+ }
693
+ emit(primitiveOf(name, name, geo, asm, pack, matrix));
694
+ placed++;
695
+ };
696
+
697
+ // 按 Geode 摆放:一个骨架可能被多个 geode 以不同矩阵引用
698
+ for (const pageLod of tile.groupNode?.pageLods ?? []) {
699
+ for (const geode of pageLod.geodes ?? []) {
700
+ const matrix = geode.matrix && geode.matrix.length === 16 ? geode.matrix : null;
701
+ for (const name of geode.skeletonNames ?? []) {
702
+ const asm = skeletons.get(name);
703
+ if (!asm) continue;
704
+ const geo = tile.geoPackage[name];
705
+ if (!geo) continue;
706
+ for (let pi = 0; pi < asm.packs.length; pi++) {
707
+ placeOrGroup(name, geo, asm, pi, matrix);
708
+ }
709
+ }
710
+ }
711
+ }
712
+
713
+ // 兜底:没有被任何 geode 引用的骨架,按单位矩阵渲染,避免整块消失
714
+ if (placed === 0) {
715
+ for (const [name, asm] of skeletons) {
716
+ const geo = tile.geoPackage[name];
717
+ if (!geo) continue;
718
+ for (let pi = 0; pi < asm.packs.length; pi++) {
719
+ placeOrGroup(name, geo, asm, pi, null);
720
+ }
721
+ }
722
+ }
723
+
724
+ // 执行同材质几何合批
725
+ if (batchGroups.size > 0) {
726
+ for (const [, list] of batchGroups) {
727
+ stats.originalMeshes = (stats.originalMeshes ?? 0) + list.length;
728
+ if (list.length === 1) {
729
+ const c = list[0];
730
+ emit(primitiveOf(c.name, c.name, c.geo, { attrs: c.attrs, packs: [], instanced: false }, {
731
+ indexCount: c.indexCount,
732
+ indexType: 0,
733
+ useIndex: 1,
734
+ primitiveType: c.primitiveType,
735
+ indices: c.index ? new Uint8Array(c.index.buffer, c.index.byteOffset, c.index.byteLength) : null,
736
+ passNames: c.passNames,
737
+ }, c.matrix));
738
+ } else {
739
+ emit(mergeCandidates(list));
740
+ }
741
+ }
742
+ }
743
+
744
+ return { primitives, stats };
745
+ }