s3m-three 0.1.0 → 0.2.0

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 (44) hide show
  1. package/LICENSE +202 -202
  2. package/dist/render/DatasetManager.d.ts +10 -3
  3. package/dist/render/DatasetManager.d.ts.map +1 -1
  4. package/dist/render/DatasetManager.js +14 -5
  5. package/dist/render/DatasetManager.js.map +1 -1
  6. package/dist/render/EdgeLines.js +46 -46
  7. package/dist/render/GroundManager.d.ts +9 -0
  8. package/dist/render/GroundManager.d.ts.map +1 -1
  9. package/dist/render/GroundManager.js +13 -1
  10. package/dist/render/GroundManager.js.map +1 -1
  11. package/dist/render/Picker.js +66 -66
  12. package/dist/render/S3MViewer.d.ts +11 -3
  13. package/dist/render/S3MViewer.d.ts.map +1 -1
  14. package/dist/render/S3MViewer.js +18 -3
  15. package/dist/render/S3MViewer.js.map +1 -1
  16. package/dist/render/StyleMaterial.js +265 -265
  17. package/dist/vendor/meshopt_decoder.module.js +118 -118
  18. package/docs/API.md +20 -0
  19. package/package.json +1 -1
  20. package/src/core/BinaryReader.ts +116 -116
  21. package/src/core/DXTTextureDecode.ts +260 -260
  22. package/src/core/FeatureInfo.ts +150 -150
  23. package/src/core/MeshOpt.ts +401 -401
  24. package/src/core/S3MBParser.ts +590 -590
  25. package/src/core/types.ts +222 -222
  26. package/src/render/DatasetManager.ts +17 -6
  27. package/src/render/EdgeLines.ts +343 -343
  28. package/src/render/GeometryBuilder.ts +840 -840
  29. package/src/render/GroundManager.ts +14 -1
  30. package/src/render/Picker.ts +293 -293
  31. package/src/render/S3MViewer.ts +18 -3
  32. package/src/render/StyleMaterial.ts +602 -602
  33. package/src/render/TextureFactory.ts +134 -134
  34. package/src/tileset/S3MTileset.ts +552 -552
  35. package/src/vendor/meshopt_decoder.module.d.ts +31 -31
  36. package/src/vendor/meshopt_decoder.module.js +118 -118
  37. package/dist/render/CoordinateHelper.d.ts +0 -90
  38. package/dist/render/CoordinateHelper.d.ts.map +0 -1
  39. package/dist/render/CoordinateHelper.js +0 -85
  40. package/dist/render/CoordinateHelper.js.map +0 -1
  41. package/dist/render/S3MLayer.d.ts +0 -457
  42. package/dist/render/S3MLayer.d.ts.map +0 -1
  43. package/dist/render/S3MLayer.js +0 -1860
  44. package/dist/render/S3MLayer.js.map +0 -1
@@ -1,590 +1,590 @@
1
- /**
2
- * S3MB 瓦片解析器(S3M 1.0 / 2.0 / 3.0)
3
- *
4
- * 实现依据:SuperMap/s3m-spec 官方 S3M_SDK/S3M_JS/S3M_module/S3MParser/S3ModelParser.js
5
- * (Apache-2.0)+ Specification/S3MB/S3MBBinary.md。
6
- *
7
- * 与官方实现的差异:
8
- * 1. 不依赖 Cesium —— 输出纯数据对象,几何上传交给渲染层。
9
- * 2. 修复了官方 v3 非压缩分支把整个 ArrayBuffer(含文件头)当数据流的缺陷。
10
- * 3. Draco / MeshOpt 几何压缩只做识别,不做解码(需要额外的 wasm 解码器)。
11
- */
12
- import { BinaryReader } from './BinaryReader.js';
13
- import { parseMeshOptSkeleton, parseMeshOptIndexPackages } from './MeshOpt.js';
14
- import type {
15
- AttrDType,
16
- GeoPackageEntry,
17
- Geode,
18
- GroupNode,
19
- IndexPackage,
20
- PageLOD,
21
- ParsedTile,
22
- PickEntry,
23
- S3MTexture,
24
- Vec3,
25
- VertexAttribute,
26
- VertexPackage,
27
- } from './types.js';
28
-
29
- export const S3M_VERSION = { V1: 1.0, V2: 2.0, V3: 3.0 } as const;
30
-
31
- export const VertexTag = { UNKNOWN: 0, STANDARD: 1, COMPRESSED: 2, DRACO: 3 } as const;
32
- export const VertexTagV3 = { STANDARD: 0, DRACO: 1, MESHOPT: 2 } as const;
33
-
34
- const CompressOption = {
35
- VERTEX: 1,
36
- NORMAL: 2,
37
- VERTEX_COLOR: 4,
38
- SECOND_COLOR: 8,
39
- TEXCOORD: 16,
40
- TEXCOORD_IS_W: 32,
41
- } as const;
42
-
43
- const AttrTypeSize: Record<number, number> = { 0: 4, 1: 4, 2: 8, 3: 2 };
44
-
45
- export type InflateFn = (data: Uint8Array) => Promise<Uint8Array> | Uint8Array;
46
-
47
- export interface ParseOptions {
48
- /** 自定义解压实现,默认用 DecompressionStream('deflate');Node 下传 zlib.inflateSync */
49
- inflate?: InflateFn;
50
- /** 逐段追踪偏移,调试解析用 */
51
- trace?: (section: string, detail?: unknown, offset?: number) => void;
52
- }
53
-
54
- /** 默认解压:zlib(文件头 78 9C / 78 DA)。浏览器与 Node 18+ 均可用。 */
55
- async function defaultInflate(bytes: Uint8Array): Promise<Uint8Array> {
56
- if (typeof DecompressionStream === 'undefined') {
57
- throw new Error('[s3m] 当前环境无 DecompressionStream,请通过 options.inflate 注入解压实现');
58
- }
59
- const stream = new Blob([bytes as BlobPart]).stream().pipeThrough(new DecompressionStream('deflate'));
60
- return new Uint8Array(await new Response(stream).arrayBuffer());
61
- }
62
-
63
- function pushAttr(
64
- vp: VertexPackage,
65
- semantic: string,
66
- data: Uint8Array,
67
- components: number,
68
- stride: number,
69
- offset = 0,
70
- divisor?: number,
71
- dtype: AttrDType = 'f32'
72
- ): void {
73
- vp.attrLocation[semantic] = vp.vertexAttributes.length;
74
- vp.vertexAttributes.push({
75
- semantic,
76
- data,
77
- components,
78
- stride: stride || components * 4,
79
- offsetInBytes: offset,
80
- divisor,
81
- dtype,
82
- });
83
- }
84
-
85
- function newVertexPackage(): VertexPackage {
86
- return { vertexAttributes: [], attrLocation: {}, instanceCount: 0, instanceMode: 0, instanceIndex: -1, verticesCount: 0 };
87
- }
88
-
89
- /* ---------------------------------------------------------------- LOD 树 */
90
-
91
- function parseGeode(r: BinaryReader): Geode {
92
- const matrix: number[] = new Array(16);
93
- for (let i = 0; i < 16; i++) matrix[i] = r.f64();
94
- const n = r.u32();
95
- const skeletonNames: string[] = [];
96
- for (let i = 0; i < n; i++) skeletonNames.push(r.string());
97
- return { matrix, skeletonNames };
98
- }
99
-
100
- function parsePageLOD(r: BinaryReader, version: number): PageLOD {
101
- const rangeValue = r.f32();
102
- const rangeMode = r.u16();
103
- const boundingSphere = { center: r.vec3f64(), radius: r.f64() };
104
-
105
- const pageLod: PageLOD = { rangeValue, rangeMode, boundingSphere, childTile: '', geodes: [] };
106
-
107
- if (version >= 3) {
108
- pageLod.obb = {
109
- center: r.vec3f64(),
110
- xExtent: r.vec3f64(),
111
- yExtent: r.vec3f64(),
112
- zExtent: r.vec3f64(),
113
- };
114
- }
115
-
116
- // childTile 里可能带 "Geometry" 后缀,官方在切分时去掉
117
- let childTile = r.string();
118
- const gi = childTile.indexOf('Geometry');
119
- if (gi !== -1) childTile = childTile.substring(0, gi);
120
- pageLod.childTile = childTile;
121
-
122
- const geodeCount = r.u32();
123
- for (let i = 0; i < geodeCount; i++) pageLod.geodes.push(parseGeode(r));
124
-
125
- if (version >= 3) r.string(); // 动画 JSON,暂不解析
126
-
127
- return pageLod;
128
- }
129
-
130
- function parseGroupNode(r: BinaryReader, version: number): GroupNode {
131
- r.u32(); // streamSize
132
- const count = r.u32();
133
- const pageLods: PageLOD[] = [];
134
- for (let i = 0; i < count; i++) pageLods.push(parsePageLOD(r, version));
135
- r.align4();
136
- return { pageLods };
137
- }
138
-
139
- /* ------------------------------------------------------------ 顶点数据包 */
140
-
141
- function parseVertex(r: BinaryReader, vp: VertexPackage): void {
142
- const count = r.u32();
143
- vp.verticesCount = count;
144
- if (count === 0) return;
145
- const dim = r.u16();
146
- r.u16(); // stride 字段不可信,官方也忽略它
147
- pushAttr(vp, 'position', r.bytes(count * dim * 4), dim, dim * 4);
148
- }
149
-
150
- function parseNormal(r: BinaryReader, vp: VertexPackage): void {
151
- const count = r.u32();
152
- if (count <= 0) return;
153
- const dim = r.u16();
154
- const stride = r.u16();
155
- pushAttr(vp, 'normal', r.bytes(count * dim * 4), dim, stride || dim * 4);
156
- }
157
-
158
- function parseVertexColor(r: BinaryReader, vp: VertexPackage): void {
159
- const count = r.u32();
160
- let data: Uint8Array;
161
- if (count > 0) {
162
- r.u16(); // stride
163
- r.skip(2);
164
- data = r.bytes(count * 4);
165
- } else {
166
- // 缺省白色,保证 shader 拿得到 aColor
167
- data = new Uint8Array((vp.verticesCount || 0) * 4).fill(255);
168
- }
169
- pushAttr(vp, 'color', data, 4, 4, 0, undefined, 'u8');
170
- }
171
-
172
- function parseSecondColor(r: BinaryReader, vp: VertexPackage): void {
173
- const count = r.u32();
174
- if (count <= 0) return;
175
- r.u16();
176
- r.skip(2);
177
- pushAttr(vp, 'secondColor', r.bytes(count * 4), 4, 4, 0, undefined, 'u8');
178
- }
179
-
180
- function parseTexCoord(r: BinaryReader, vp: VertexPackage): void {
181
- const count = r.u16();
182
- r.skip(2);
183
- for (let i = 0; i < count; i++) {
184
- const n = r.u32();
185
- const dim = r.u16();
186
- const stride = r.u16();
187
- pushAttr(vp, i === 0 ? 'uv' : `uv${i + 1}`, r.bytes(n * dim * 4), dim, stride || dim * 4);
188
- }
189
- }
190
-
191
- /**
192
- * 实例化信息。官方按 dimension 分派:
193
- * 16 → 每实例一个 4x4 矩阵(另有 2/4 字节 per-instance 前缀,v3 与 v1/v2 不同)
194
- * 17/29 → 每实例一个矩阵 + 颜色
195
- * 其余情况按 dimension * count * 4 字节整体跳过。
196
- */
197
- function parseInstanceInfo(r: BinaryReader, vp: VertexPackage, version: number): void {
198
- const count = r.u16();
199
- r.skip(2);
200
-
201
- for (let i = 0; i < count; i++) {
202
- const instanceCount = r.u32();
203
- const dim = r.u16();
204
-
205
- if (dim === 16) {
206
- r.offset -= 2; // 官方此处回退 2 字节,perInstance 前缀直接计入行长
207
- const prefix = version === 3 ? 2 : 4;
208
- const rowLen = dim * 4 + prefix;
209
- const total = instanceCount * rowLen;
210
- const raw = r.bytes(total);
211
- const clean = new Uint8Array(instanceCount * dim * 4);
212
- for (let k = 0; k < instanceCount; k++) {
213
- clean.set(raw.subarray(k * rowLen + prefix, k * rowLen + rowLen), k * dim * 4);
214
- }
215
- vp.instanceCount = instanceCount;
216
- vp.instanceMode = dim;
217
- vp.instanceBuffer = clean;
218
- vp.instanceIndex = 1;
219
- const byteStride = 64;
220
- pushAttr(vp, 'instanceMatrix0', clean, 4, byteStride, 0, 1);
221
- pushAttr(vp, 'instanceMatrix1', clean, 4, byteStride, 16, 1);
222
- pushAttr(vp, 'instanceMatrix2', clean, 4, byteStride, 32, 1);
223
- pushAttr(vp, 'instanceColor', clean, 4, byteStride, 48, 1);
224
- } else {
225
- r.u16(); // stride
226
- const total = instanceCount * dim * 4;
227
- if (dim === 17 || dim === 29) {
228
- vp.instanceCount = instanceCount;
229
- vp.instanceMode = dim;
230
- vp.instanceBuffer = r.bytesCopy(total);
231
- vp.instanceIndex = 1;
232
- } else {
233
- r.skip(total);
234
- }
235
- }
236
- }
237
- }
238
-
239
- function parseVertexAttrExtension(r: BinaryReader, vp: VertexPackage): void {
240
- const n = r.u32();
241
- for (let i = 0; i < n; i++) {
242
- const count = r.u32();
243
- const dim = r.u16();
244
- const type = r.u16();
245
- const size = AttrTypeSize[type] ?? 4;
246
- pushAttr(vp, `custom${i}`, r.bytes(count * dim * size), dim, dim * size);
247
- }
248
- }
249
-
250
- function parseTangent(r: BinaryReader): void {
251
- const n = r.u32();
252
- if (n <= 0) return;
253
- const dim = r.u16();
254
- r.u16();
255
- r.skip(n * dim * 4);
256
- }
257
-
258
- function parseStandardSkeleton(r: BinaryReader, vp: VertexPackage, version: number): void {
259
- if (version >= 3) r.u32(); // streamSize
260
-
261
- parseVertex(r, vp);
262
- parseNormal(r, vp);
263
- parseVertexColor(r, vp);
264
- if (version < 3) parseSecondColor(r, vp);
265
- parseTexCoord(r, vp);
266
- parseInstanceInfo(r, vp, version);
267
-
268
- if (version >= 3) {
269
- parseVertexAttrExtension(r, vp);
270
- const descript = r.string();
271
- vp.customVertexAttribute = descript ? JSON.parse(descript) : undefined;
272
- r.align4();
273
- parseTangent(r);
274
- }
275
- }
276
-
277
- function parseCompressedSkeleton(r: BinaryReader, vp: VertexPackage, version: number): void {
278
- const flags = r.u32();
279
- vp.compressOptions = flags;
280
-
281
- if (flags & CompressOption.VERTEX) throw new Error('[s3m] 顶点量化压缩(v2 旧式)暂未实现');
282
- parseVertex(r, vp);
283
-
284
- if (flags & CompressOption.NORMAL) throw new Error('[s3m] 法线压缩(v2 旧式)暂未实现');
285
- parseNormal(r, vp);
286
-
287
- parseVertexColor(r, vp);
288
- parseSecondColor(r, vp);
289
-
290
- if (flags & CompressOption.TEXCOORD) throw new Error('[s3m] 纹理坐标压缩(v2 旧式)暂未实现');
291
- parseTexCoord(r, vp);
292
-
293
- if (flags & CompressOption.TEXCOORD_IS_W) vp.textureCoordIsW = true;
294
- parseInstanceInfo(r, vp, version);
295
- }
296
-
297
- /* ---------------------------------------------------------------- 索引包 */
298
-
299
- function parseIndexPackages(r: BinaryReader, version: number): IndexPackage[] {
300
- const count = r.u32();
301
- const packs: IndexPackage[] = [];
302
-
303
- for (let i = 0; i < count; i++) {
304
- if (version >= 3) r.u32(); // streamSize
305
-
306
- const indexCount = r.u32();
307
- const indexType = r.u8();
308
- const useIndex = r.u8();
309
- const primitiveType = r.u8();
310
- r.skip(1);
311
-
312
- let indices: Uint8Array | null = null;
313
- if (indexCount > 0) {
314
- const wide = indexType === 1 || indexType === 3;
315
- const size = wide ? 4 : 2;
316
- indices = r.bytes(indexCount * size);
317
- if (!wide && indexCount % 2 !== 0) r.skip(2); // 16 位奇数索引补齐
318
- }
319
-
320
- const passNames: string[] = [];
321
- const passCount = r.u32();
322
- for (let j = 0; j < passCount; j++) passNames.push(r.string());
323
-
324
- r.align4();
325
-
326
- packs.push({ indexCount, indexType, useIndex, primitiveType, indices, passNames });
327
- }
328
- return packs;
329
- }
330
-
331
- /* ------------------------------------------------------------------ 骨架 */
332
-
333
- /**
334
- * v3 骨架末尾的包围盒。
335
- *
336
- * v3.01 的实际数据在索引包之后、包围盒之前多了一个 4 字节字段(实测恒为 0,
337
- * 推测是线框几何的长度)。规范里没有写,标准骨架是否也有无法验证,
338
- * 所以这里做一次「读一遍看像不像包围盒」的判断,不像就跳 4 字节重读。
339
- */
340
- function readV3BoundingBox(r: BinaryReader): void {
341
- const start = r.offset;
342
- const box = r.boundingBox();
343
- if (isPlausibleBox(box)) return;
344
- r.offset = start + 4;
345
- r.boundingBox();
346
- }
347
-
348
- function isPlausibleBox(box: { center: Vec3; xExtent: Vec3; yExtent: Vec3; zExtent: Vec3 }): boolean {
349
- const all = [box.center, box.xExtent, box.yExtent, box.zExtent];
350
- for (const v of all) {
351
- if (!Number.isFinite(v.x) || !Number.isFinite(v.y) || !Number.isFinite(v.z)) return false;
352
- if (Math.abs(v.x) > 1e7 || Math.abs(v.y) > 1e7 || Math.abs(v.z) > 1e7) return false;
353
- }
354
- const len = (v: Vec3): number => Math.hypot(v.x, v.y, v.z);
355
- return len(box.xExtent) > 1e-6 && len(box.yExtent) > 1e-6 && len(box.zExtent) > 1e-6;
356
- }
357
-
358
- async function parseSkeletons(
359
- r: BinaryReader,
360
- version: number,
361
- trace?: ParseOptions['trace']
362
- ): Promise<Record<string, GeoPackageEntry>> {
363
- r.u32(); // streamSize
364
- const count = r.u32();
365
- const geoPackage: Record<string, GeoPackageEntry> = {};
366
-
367
- for (let i = 0; i < count; i++) {
368
- if (version >= 3) r.u32(); // skeletonStreamSize
369
-
370
- const name = r.string();
371
- r.align4();
372
-
373
- let tag: number = r.u32();
374
- if (version >= 3) {
375
- tag =
376
- tag === VertexTagV3.STANDARD ? VertexTag.STANDARD
377
- : tag === VertexTagV3.DRACO ? VertexTag.DRACO
378
- : tag === VertexTagV3.MESHOPT ? VertexTag.COMPRESSED
379
- : VertexTag.UNKNOWN;
380
- }
381
-
382
- const vp = newVertexPackage();
383
- let indexPackages: IndexPackage[] = [];
384
-
385
- if (tag === VertexTag.STANDARD) {
386
- parseStandardSkeleton(r, vp, version);
387
- indexPackages = parseIndexPackages(r, version);
388
- } else if (tag === VertexTag.COMPRESSED) {
389
- if (version >= 3) {
390
- await parseMeshOptSkeleton(r, vp);
391
- indexPackages = await parseMeshOptIndexPackages(r);
392
- } else {
393
- parseCompressedSkeleton(r, vp, version);
394
- indexPackages = parseIndexPackages(r, version);
395
- }
396
- } else if (tag === VertexTag.DRACO) {
397
- throw new Error(`[s3m] 骨架 "${name}" 使用 Draco 几何压缩,需接入 draco3d 解码器`);
398
- } else {
399
- throw new Error(`[s3m] 未知骨架标记 ${tag}(${name})`);
400
- }
401
-
402
- if (version >= 3) readV3BoundingBox(r);
403
- trace?.(' skel', name, r.offset);
404
-
405
- geoPackage[name] = { name, vertexPackage: vp, indexPackages };
406
- }
407
-
408
- // v1/v2 在骨架数组之后还有一段附加流。官方写法是
409
- // bytesOffset += secColorSize; bytesOffset += 4;
410
- // 其中读取 secColorSize 时不推进指针,等价于「起点 + 长度 + 4」。
411
- // 此时 r.offset 已在长度字段之后,所以只补长度即可。
412
- if (version < 3) {
413
- const extra = r.u32();
414
- r.skip(extra);
415
- }
416
-
417
- return geoPackage;
418
- }
419
-
420
- /* ------------------------------------------------------------------ 纹理 */
421
-
422
- function parseTextures(r: BinaryReader): Record<string, S3MTexture> {
423
- const streamSize = r.u32();
424
- const start = r.offset;
425
- const end = start + streamSize;
426
-
427
- const count = r.u32();
428
- const textures: Record<string, S3MTexture> = {};
429
-
430
- for (let i = 0; i < count; i++) {
431
- const code = r.string();
432
- r.align4(start);
433
-
434
- const level = r.u32();
435
- const width = r.u32();
436
- const height = r.u32();
437
- const compressType = r.u32();
438
- const dataSize = r.u32();
439
- const pixelFormat = r.u32();
440
- const data = r.bytesCopy(dataSize);
441
-
442
- textures[code] = { code, level, width, height, compressType, pixelFormat, data };
443
- }
444
-
445
- r.offset = end;
446
- return textures;
447
- }
448
-
449
- /* ------------------------------------------------------------ 材质与拾取 */
450
-
451
- function parseMaterials(r: BinaryReader): unknown {
452
- const len = r.u32();
453
- const str = new TextDecoder('utf-8').decode(r.bytes(len));
454
- return str ? JSON.parse(str) : [];
455
- }
456
-
457
- /**
458
- * 单体化信息。非实例化数据里每个单体对应一段连续的顶点区间,
459
- * 这里把它展开成逐顶点的 batchId 属性,渲染层可直接做 GPU picking。
460
- */
461
- function parsePickInfo(
462
- r: BinaryReader,
463
- options: number,
464
- geoPackage: Record<string, GeoPackageEntry>,
465
- version: number
466
- ): Record<string, unknown> | null {
467
- let flags = options;
468
- if ((flags & 1) !== 1) return null;
469
-
470
- if (version >= 3) flags = r.u32();
471
- if ((flags & 1) !== 1) return null;
472
-
473
- r.u32(); // streamSize
474
- const count = r.u32();
475
- const result: Record<string, unknown> = {};
476
-
477
- for (let i = 0; i < count; i++) {
478
- const geometryName = r.string();
479
- const geo = geoPackage[geometryName];
480
- const selectCount = r.u32();
481
- const pickInfo: Record<number, PickEntry> = {};
482
- if (!geo) continue;
483
-
484
- const vp = geo.vertexPackage;
485
- const instanced = vp.instanceIndex !== -1;
486
-
487
- if (!instanced) {
488
- const batchIds = new Float32Array(vp.verticesCount || 0);
489
- for (let j = 0; j < selectCount; j++) {
490
- const id = r.u32();
491
- const segCount = r.u32();
492
- const entry: PickEntry = { batchId: j, segments: [] };
493
- pickInfo[id] = entry;
494
- for (let k = 0; k < segCount; k++) {
495
- const vertexOffset = r.u32();
496
- const vertexCount = r.u32();
497
- entry.segments!.push({ vertexOffset, vertexCount });
498
- batchIds.fill(j, vertexOffset, vertexOffset + vertexCount);
499
- }
500
- }
501
- pushAttr(vp, 'batchId', new Uint8Array(batchIds.buffer), 1, 4, 0, undefined, 'f32');
502
- } else {
503
- const instanceCount = vp.instanceCount;
504
- const instanceBatchIds = new Float32Array(instanceCount).fill(-1);
505
- for (let j = 0; j < selectCount; j++) {
506
- const id = r.u32();
507
- const n = r.u32();
508
- const entry: PickEntry = { instanceIds: [], instanceBatchId: j };
509
- pickInfo[id] = entry;
510
- for (let k = 0; k < n; k++) {
511
- const instanceId = r.u32();
512
- if (version >= 3) r.u32();
513
- if (instanceId >= 0 && instanceId < instanceCount) {
514
- entry.instanceIds!.push(instanceId);
515
- instanceBatchIds[instanceId] = j;
516
- }
517
- }
518
- }
519
- // 实例化的单体粒度是「逐实例」的,不能塞进逐顶点属性:
520
- // 顶点数(base mesh)和实例数根本不是一个量级,长度对不上会被 three 当成坏 buffer。
521
- vp.instanceBatchIds = instanceBatchIds;
522
- }
523
-
524
- geo.pickInfo = pickInfo;
525
- result[geometryName] = pickInfo;
526
- }
527
-
528
- return result;
529
- }
530
-
531
- /* ------------------------------------------------------------------ 入口 */
532
-
533
- export async function parseS3MB(arrayBuffer: ArrayBuffer, options: ParseOptions = {}): Promise<ParsedTile> {
534
- const inflate = options.inflate ?? defaultInflate;
535
- const trace = options.trace;
536
- const r = new BinaryReader(arrayBuffer);
537
-
538
- const version = Number(r.f32().toFixed(2));
539
-
540
- let compressedType = 0;
541
- if (version >= 3) compressedType = r.u32();
542
- if (version >= 2) r.u32(); // uncompressedSize
543
- const compressedSize = r.u32();
544
-
545
- const payload = new Uint8Array(arrayBuffer, r.base + r.offset, compressedSize);
546
-
547
- let raw: Uint8Array;
548
- if (version >= 3 && compressedType === 0) {
549
- raw = payload;
550
- } else {
551
- raw = await inflate(payload);
552
- }
553
-
554
- const t = new BinaryReader(raw.buffer as ArrayBuffer, raw.byteOffset ?? 0, raw.byteLength);
555
-
556
- const fileOptions = t.u32();
557
- trace?.('fileOptions', fileOptions, t.offset);
558
- const groupNode = parseGroupNode(t, version);
559
- trace?.('groupNode', groupNode.pageLods.length, t.offset);
560
- const geoPackage = await parseSkeletons(t, version, trace);
561
- trace?.('skeletons', Object.keys(geoPackage).length, t.offset);
562
- const textures = parseTextures(t);
563
- trace?.('textures', Object.keys(textures).length, t.offset);
564
- const materials = parseMaterials(t);
565
- trace?.('materials', t.offset);
566
- const pickInfo = parsePickInfo(t, fileOptions, geoPackage, version);
567
- trace?.('pickInfo', t.offset);
568
-
569
- let extensions: unknown;
570
- if (version >= 3) {
571
- const s = t.string();
572
- extensions = s ? JSON.parse(s) : undefined;
573
- }
574
-
575
- return {
576
- version,
577
- compressedType,
578
- options: fileOptions,
579
- groupNode,
580
- geoPackage,
581
- textures,
582
- materials,
583
- pickInfo,
584
- extensions,
585
- bytesRead: t.offset,
586
- bytesTotal: t.view.byteLength,
587
- };
588
- }
589
-
590
- export type { VertexAttribute };
1
+ /**
2
+ * S3MB 瓦片解析器(S3M 1.0 / 2.0 / 3.0)
3
+ *
4
+ * 实现依据:SuperMap/s3m-spec 官方 S3M_SDK/S3M_JS/S3M_module/S3MParser/S3ModelParser.js
5
+ * (Apache-2.0)+ Specification/S3MB/S3MBBinary.md。
6
+ *
7
+ * 与官方实现的差异:
8
+ * 1. 不依赖 Cesium —— 输出纯数据对象,几何上传交给渲染层。
9
+ * 2. 修复了官方 v3 非压缩分支把整个 ArrayBuffer(含文件头)当数据流的缺陷。
10
+ * 3. Draco / MeshOpt 几何压缩只做识别,不做解码(需要额外的 wasm 解码器)。
11
+ */
12
+ import { BinaryReader } from './BinaryReader.js';
13
+ import { parseMeshOptSkeleton, parseMeshOptIndexPackages } from './MeshOpt.js';
14
+ import type {
15
+ AttrDType,
16
+ GeoPackageEntry,
17
+ Geode,
18
+ GroupNode,
19
+ IndexPackage,
20
+ PageLOD,
21
+ ParsedTile,
22
+ PickEntry,
23
+ S3MTexture,
24
+ Vec3,
25
+ VertexAttribute,
26
+ VertexPackage,
27
+ } from './types.js';
28
+
29
+ export const S3M_VERSION = { V1: 1.0, V2: 2.0, V3: 3.0 } as const;
30
+
31
+ export const VertexTag = { UNKNOWN: 0, STANDARD: 1, COMPRESSED: 2, DRACO: 3 } as const;
32
+ export const VertexTagV3 = { STANDARD: 0, DRACO: 1, MESHOPT: 2 } as const;
33
+
34
+ const CompressOption = {
35
+ VERTEX: 1,
36
+ NORMAL: 2,
37
+ VERTEX_COLOR: 4,
38
+ SECOND_COLOR: 8,
39
+ TEXCOORD: 16,
40
+ TEXCOORD_IS_W: 32,
41
+ } as const;
42
+
43
+ const AttrTypeSize: Record<number, number> = { 0: 4, 1: 4, 2: 8, 3: 2 };
44
+
45
+ export type InflateFn = (data: Uint8Array) => Promise<Uint8Array> | Uint8Array;
46
+
47
+ export interface ParseOptions {
48
+ /** 自定义解压实现,默认用 DecompressionStream('deflate');Node 下传 zlib.inflateSync */
49
+ inflate?: InflateFn;
50
+ /** 逐段追踪偏移,调试解析用 */
51
+ trace?: (section: string, detail?: unknown, offset?: number) => void;
52
+ }
53
+
54
+ /** 默认解压:zlib(文件头 78 9C / 78 DA)。浏览器与 Node 18+ 均可用。 */
55
+ async function defaultInflate(bytes: Uint8Array): Promise<Uint8Array> {
56
+ if (typeof DecompressionStream === 'undefined') {
57
+ throw new Error('[s3m] 当前环境无 DecompressionStream,请通过 options.inflate 注入解压实现');
58
+ }
59
+ const stream = new Blob([bytes as BlobPart]).stream().pipeThrough(new DecompressionStream('deflate'));
60
+ return new Uint8Array(await new Response(stream).arrayBuffer());
61
+ }
62
+
63
+ function pushAttr(
64
+ vp: VertexPackage,
65
+ semantic: string,
66
+ data: Uint8Array,
67
+ components: number,
68
+ stride: number,
69
+ offset = 0,
70
+ divisor?: number,
71
+ dtype: AttrDType = 'f32'
72
+ ): void {
73
+ vp.attrLocation[semantic] = vp.vertexAttributes.length;
74
+ vp.vertexAttributes.push({
75
+ semantic,
76
+ data,
77
+ components,
78
+ stride: stride || components * 4,
79
+ offsetInBytes: offset,
80
+ divisor,
81
+ dtype,
82
+ });
83
+ }
84
+
85
+ function newVertexPackage(): VertexPackage {
86
+ return { vertexAttributes: [], attrLocation: {}, instanceCount: 0, instanceMode: 0, instanceIndex: -1, verticesCount: 0 };
87
+ }
88
+
89
+ /* ---------------------------------------------------------------- LOD 树 */
90
+
91
+ function parseGeode(r: BinaryReader): Geode {
92
+ const matrix: number[] = new Array(16);
93
+ for (let i = 0; i < 16; i++) matrix[i] = r.f64();
94
+ const n = r.u32();
95
+ const skeletonNames: string[] = [];
96
+ for (let i = 0; i < n; i++) skeletonNames.push(r.string());
97
+ return { matrix, skeletonNames };
98
+ }
99
+
100
+ function parsePageLOD(r: BinaryReader, version: number): PageLOD {
101
+ const rangeValue = r.f32();
102
+ const rangeMode = r.u16();
103
+ const boundingSphere = { center: r.vec3f64(), radius: r.f64() };
104
+
105
+ const pageLod: PageLOD = { rangeValue, rangeMode, boundingSphere, childTile: '', geodes: [] };
106
+
107
+ if (version >= 3) {
108
+ pageLod.obb = {
109
+ center: r.vec3f64(),
110
+ xExtent: r.vec3f64(),
111
+ yExtent: r.vec3f64(),
112
+ zExtent: r.vec3f64(),
113
+ };
114
+ }
115
+
116
+ // childTile 里可能带 "Geometry" 后缀,官方在切分时去掉
117
+ let childTile = r.string();
118
+ const gi = childTile.indexOf('Geometry');
119
+ if (gi !== -1) childTile = childTile.substring(0, gi);
120
+ pageLod.childTile = childTile;
121
+
122
+ const geodeCount = r.u32();
123
+ for (let i = 0; i < geodeCount; i++) pageLod.geodes.push(parseGeode(r));
124
+
125
+ if (version >= 3) r.string(); // 动画 JSON,暂不解析
126
+
127
+ return pageLod;
128
+ }
129
+
130
+ function parseGroupNode(r: BinaryReader, version: number): GroupNode {
131
+ r.u32(); // streamSize
132
+ const count = r.u32();
133
+ const pageLods: PageLOD[] = [];
134
+ for (let i = 0; i < count; i++) pageLods.push(parsePageLOD(r, version));
135
+ r.align4();
136
+ return { pageLods };
137
+ }
138
+
139
+ /* ------------------------------------------------------------ 顶点数据包 */
140
+
141
+ function parseVertex(r: BinaryReader, vp: VertexPackage): void {
142
+ const count = r.u32();
143
+ vp.verticesCount = count;
144
+ if (count === 0) return;
145
+ const dim = r.u16();
146
+ r.u16(); // stride 字段不可信,官方也忽略它
147
+ pushAttr(vp, 'position', r.bytes(count * dim * 4), dim, dim * 4);
148
+ }
149
+
150
+ function parseNormal(r: BinaryReader, vp: VertexPackage): void {
151
+ const count = r.u32();
152
+ if (count <= 0) return;
153
+ const dim = r.u16();
154
+ const stride = r.u16();
155
+ pushAttr(vp, 'normal', r.bytes(count * dim * 4), dim, stride || dim * 4);
156
+ }
157
+
158
+ function parseVertexColor(r: BinaryReader, vp: VertexPackage): void {
159
+ const count = r.u32();
160
+ let data: Uint8Array;
161
+ if (count > 0) {
162
+ r.u16(); // stride
163
+ r.skip(2);
164
+ data = r.bytes(count * 4);
165
+ } else {
166
+ // 缺省白色,保证 shader 拿得到 aColor
167
+ data = new Uint8Array((vp.verticesCount || 0) * 4).fill(255);
168
+ }
169
+ pushAttr(vp, 'color', data, 4, 4, 0, undefined, 'u8');
170
+ }
171
+
172
+ function parseSecondColor(r: BinaryReader, vp: VertexPackage): void {
173
+ const count = r.u32();
174
+ if (count <= 0) return;
175
+ r.u16();
176
+ r.skip(2);
177
+ pushAttr(vp, 'secondColor', r.bytes(count * 4), 4, 4, 0, undefined, 'u8');
178
+ }
179
+
180
+ function parseTexCoord(r: BinaryReader, vp: VertexPackage): void {
181
+ const count = r.u16();
182
+ r.skip(2);
183
+ for (let i = 0; i < count; i++) {
184
+ const n = r.u32();
185
+ const dim = r.u16();
186
+ const stride = r.u16();
187
+ pushAttr(vp, i === 0 ? 'uv' : `uv${i + 1}`, r.bytes(n * dim * 4), dim, stride || dim * 4);
188
+ }
189
+ }
190
+
191
+ /**
192
+ * 实例化信息。官方按 dimension 分派:
193
+ * 16 → 每实例一个 4x4 矩阵(另有 2/4 字节 per-instance 前缀,v3 与 v1/v2 不同)
194
+ * 17/29 → 每实例一个矩阵 + 颜色
195
+ * 其余情况按 dimension * count * 4 字节整体跳过。
196
+ */
197
+ function parseInstanceInfo(r: BinaryReader, vp: VertexPackage, version: number): void {
198
+ const count = r.u16();
199
+ r.skip(2);
200
+
201
+ for (let i = 0; i < count; i++) {
202
+ const instanceCount = r.u32();
203
+ const dim = r.u16();
204
+
205
+ if (dim === 16) {
206
+ r.offset -= 2; // 官方此处回退 2 字节,perInstance 前缀直接计入行长
207
+ const prefix = version === 3 ? 2 : 4;
208
+ const rowLen = dim * 4 + prefix;
209
+ const total = instanceCount * rowLen;
210
+ const raw = r.bytes(total);
211
+ const clean = new Uint8Array(instanceCount * dim * 4);
212
+ for (let k = 0; k < instanceCount; k++) {
213
+ clean.set(raw.subarray(k * rowLen + prefix, k * rowLen + rowLen), k * dim * 4);
214
+ }
215
+ vp.instanceCount = instanceCount;
216
+ vp.instanceMode = dim;
217
+ vp.instanceBuffer = clean;
218
+ vp.instanceIndex = 1;
219
+ const byteStride = 64;
220
+ pushAttr(vp, 'instanceMatrix0', clean, 4, byteStride, 0, 1);
221
+ pushAttr(vp, 'instanceMatrix1', clean, 4, byteStride, 16, 1);
222
+ pushAttr(vp, 'instanceMatrix2', clean, 4, byteStride, 32, 1);
223
+ pushAttr(vp, 'instanceColor', clean, 4, byteStride, 48, 1);
224
+ } else {
225
+ r.u16(); // stride
226
+ const total = instanceCount * dim * 4;
227
+ if (dim === 17 || dim === 29) {
228
+ vp.instanceCount = instanceCount;
229
+ vp.instanceMode = dim;
230
+ vp.instanceBuffer = r.bytesCopy(total);
231
+ vp.instanceIndex = 1;
232
+ } else {
233
+ r.skip(total);
234
+ }
235
+ }
236
+ }
237
+ }
238
+
239
+ function parseVertexAttrExtension(r: BinaryReader, vp: VertexPackage): void {
240
+ const n = r.u32();
241
+ for (let i = 0; i < n; i++) {
242
+ const count = r.u32();
243
+ const dim = r.u16();
244
+ const type = r.u16();
245
+ const size = AttrTypeSize[type] ?? 4;
246
+ pushAttr(vp, `custom${i}`, r.bytes(count * dim * size), dim, dim * size);
247
+ }
248
+ }
249
+
250
+ function parseTangent(r: BinaryReader): void {
251
+ const n = r.u32();
252
+ if (n <= 0) return;
253
+ const dim = r.u16();
254
+ r.u16();
255
+ r.skip(n * dim * 4);
256
+ }
257
+
258
+ function parseStandardSkeleton(r: BinaryReader, vp: VertexPackage, version: number): void {
259
+ if (version >= 3) r.u32(); // streamSize
260
+
261
+ parseVertex(r, vp);
262
+ parseNormal(r, vp);
263
+ parseVertexColor(r, vp);
264
+ if (version < 3) parseSecondColor(r, vp);
265
+ parseTexCoord(r, vp);
266
+ parseInstanceInfo(r, vp, version);
267
+
268
+ if (version >= 3) {
269
+ parseVertexAttrExtension(r, vp);
270
+ const descript = r.string();
271
+ vp.customVertexAttribute = descript ? JSON.parse(descript) : undefined;
272
+ r.align4();
273
+ parseTangent(r);
274
+ }
275
+ }
276
+
277
+ function parseCompressedSkeleton(r: BinaryReader, vp: VertexPackage, version: number): void {
278
+ const flags = r.u32();
279
+ vp.compressOptions = flags;
280
+
281
+ if (flags & CompressOption.VERTEX) throw new Error('[s3m] 顶点量化压缩(v2 旧式)暂未实现');
282
+ parseVertex(r, vp);
283
+
284
+ if (flags & CompressOption.NORMAL) throw new Error('[s3m] 法线压缩(v2 旧式)暂未实现');
285
+ parseNormal(r, vp);
286
+
287
+ parseVertexColor(r, vp);
288
+ parseSecondColor(r, vp);
289
+
290
+ if (flags & CompressOption.TEXCOORD) throw new Error('[s3m] 纹理坐标压缩(v2 旧式)暂未实现');
291
+ parseTexCoord(r, vp);
292
+
293
+ if (flags & CompressOption.TEXCOORD_IS_W) vp.textureCoordIsW = true;
294
+ parseInstanceInfo(r, vp, version);
295
+ }
296
+
297
+ /* ---------------------------------------------------------------- 索引包 */
298
+
299
+ function parseIndexPackages(r: BinaryReader, version: number): IndexPackage[] {
300
+ const count = r.u32();
301
+ const packs: IndexPackage[] = [];
302
+
303
+ for (let i = 0; i < count; i++) {
304
+ if (version >= 3) r.u32(); // streamSize
305
+
306
+ const indexCount = r.u32();
307
+ const indexType = r.u8();
308
+ const useIndex = r.u8();
309
+ const primitiveType = r.u8();
310
+ r.skip(1);
311
+
312
+ let indices: Uint8Array | null = null;
313
+ if (indexCount > 0) {
314
+ const wide = indexType === 1 || indexType === 3;
315
+ const size = wide ? 4 : 2;
316
+ indices = r.bytes(indexCount * size);
317
+ if (!wide && indexCount % 2 !== 0) r.skip(2); // 16 位奇数索引补齐
318
+ }
319
+
320
+ const passNames: string[] = [];
321
+ const passCount = r.u32();
322
+ for (let j = 0; j < passCount; j++) passNames.push(r.string());
323
+
324
+ r.align4();
325
+
326
+ packs.push({ indexCount, indexType, useIndex, primitiveType, indices, passNames });
327
+ }
328
+ return packs;
329
+ }
330
+
331
+ /* ------------------------------------------------------------------ 骨架 */
332
+
333
+ /**
334
+ * v3 骨架末尾的包围盒。
335
+ *
336
+ * v3.01 的实际数据在索引包之后、包围盒之前多了一个 4 字节字段(实测恒为 0,
337
+ * 推测是线框几何的长度)。规范里没有写,标准骨架是否也有无法验证,
338
+ * 所以这里做一次「读一遍看像不像包围盒」的判断,不像就跳 4 字节重读。
339
+ */
340
+ function readV3BoundingBox(r: BinaryReader): void {
341
+ const start = r.offset;
342
+ const box = r.boundingBox();
343
+ if (isPlausibleBox(box)) return;
344
+ r.offset = start + 4;
345
+ r.boundingBox();
346
+ }
347
+
348
+ function isPlausibleBox(box: { center: Vec3; xExtent: Vec3; yExtent: Vec3; zExtent: Vec3 }): boolean {
349
+ const all = [box.center, box.xExtent, box.yExtent, box.zExtent];
350
+ for (const v of all) {
351
+ if (!Number.isFinite(v.x) || !Number.isFinite(v.y) || !Number.isFinite(v.z)) return false;
352
+ if (Math.abs(v.x) > 1e7 || Math.abs(v.y) > 1e7 || Math.abs(v.z) > 1e7) return false;
353
+ }
354
+ const len = (v: Vec3): number => Math.hypot(v.x, v.y, v.z);
355
+ return len(box.xExtent) > 1e-6 && len(box.yExtent) > 1e-6 && len(box.zExtent) > 1e-6;
356
+ }
357
+
358
+ async function parseSkeletons(
359
+ r: BinaryReader,
360
+ version: number,
361
+ trace?: ParseOptions['trace']
362
+ ): Promise<Record<string, GeoPackageEntry>> {
363
+ r.u32(); // streamSize
364
+ const count = r.u32();
365
+ const geoPackage: Record<string, GeoPackageEntry> = {};
366
+
367
+ for (let i = 0; i < count; i++) {
368
+ if (version >= 3) r.u32(); // skeletonStreamSize
369
+
370
+ const name = r.string();
371
+ r.align4();
372
+
373
+ let tag: number = r.u32();
374
+ if (version >= 3) {
375
+ tag =
376
+ tag === VertexTagV3.STANDARD ? VertexTag.STANDARD
377
+ : tag === VertexTagV3.DRACO ? VertexTag.DRACO
378
+ : tag === VertexTagV3.MESHOPT ? VertexTag.COMPRESSED
379
+ : VertexTag.UNKNOWN;
380
+ }
381
+
382
+ const vp = newVertexPackage();
383
+ let indexPackages: IndexPackage[] = [];
384
+
385
+ if (tag === VertexTag.STANDARD) {
386
+ parseStandardSkeleton(r, vp, version);
387
+ indexPackages = parseIndexPackages(r, version);
388
+ } else if (tag === VertexTag.COMPRESSED) {
389
+ if (version >= 3) {
390
+ await parseMeshOptSkeleton(r, vp);
391
+ indexPackages = await parseMeshOptIndexPackages(r);
392
+ } else {
393
+ parseCompressedSkeleton(r, vp, version);
394
+ indexPackages = parseIndexPackages(r, version);
395
+ }
396
+ } else if (tag === VertexTag.DRACO) {
397
+ throw new Error(`[s3m] 骨架 "${name}" 使用 Draco 几何压缩,需接入 draco3d 解码器`);
398
+ } else {
399
+ throw new Error(`[s3m] 未知骨架标记 ${tag}(${name})`);
400
+ }
401
+
402
+ if (version >= 3) readV3BoundingBox(r);
403
+ trace?.(' skel', name, r.offset);
404
+
405
+ geoPackage[name] = { name, vertexPackage: vp, indexPackages };
406
+ }
407
+
408
+ // v1/v2 在骨架数组之后还有一段附加流。官方写法是
409
+ // bytesOffset += secColorSize; bytesOffset += 4;
410
+ // 其中读取 secColorSize 时不推进指针,等价于「起点 + 长度 + 4」。
411
+ // 此时 r.offset 已在长度字段之后,所以只补长度即可。
412
+ if (version < 3) {
413
+ const extra = r.u32();
414
+ r.skip(extra);
415
+ }
416
+
417
+ return geoPackage;
418
+ }
419
+
420
+ /* ------------------------------------------------------------------ 纹理 */
421
+
422
+ function parseTextures(r: BinaryReader): Record<string, S3MTexture> {
423
+ const streamSize = r.u32();
424
+ const start = r.offset;
425
+ const end = start + streamSize;
426
+
427
+ const count = r.u32();
428
+ const textures: Record<string, S3MTexture> = {};
429
+
430
+ for (let i = 0; i < count; i++) {
431
+ const code = r.string();
432
+ r.align4(start);
433
+
434
+ const level = r.u32();
435
+ const width = r.u32();
436
+ const height = r.u32();
437
+ const compressType = r.u32();
438
+ const dataSize = r.u32();
439
+ const pixelFormat = r.u32();
440
+ const data = r.bytesCopy(dataSize);
441
+
442
+ textures[code] = { code, level, width, height, compressType, pixelFormat, data };
443
+ }
444
+
445
+ r.offset = end;
446
+ return textures;
447
+ }
448
+
449
+ /* ------------------------------------------------------------ 材质与拾取 */
450
+
451
+ function parseMaterials(r: BinaryReader): unknown {
452
+ const len = r.u32();
453
+ const str = new TextDecoder('utf-8').decode(r.bytes(len));
454
+ return str ? JSON.parse(str) : [];
455
+ }
456
+
457
+ /**
458
+ * 单体化信息。非实例化数据里每个单体对应一段连续的顶点区间,
459
+ * 这里把它展开成逐顶点的 batchId 属性,渲染层可直接做 GPU picking。
460
+ */
461
+ function parsePickInfo(
462
+ r: BinaryReader,
463
+ options: number,
464
+ geoPackage: Record<string, GeoPackageEntry>,
465
+ version: number
466
+ ): Record<string, unknown> | null {
467
+ let flags = options;
468
+ if ((flags & 1) !== 1) return null;
469
+
470
+ if (version >= 3) flags = r.u32();
471
+ if ((flags & 1) !== 1) return null;
472
+
473
+ r.u32(); // streamSize
474
+ const count = r.u32();
475
+ const result: Record<string, unknown> = {};
476
+
477
+ for (let i = 0; i < count; i++) {
478
+ const geometryName = r.string();
479
+ const geo = geoPackage[geometryName];
480
+ const selectCount = r.u32();
481
+ const pickInfo: Record<number, PickEntry> = {};
482
+ if (!geo) continue;
483
+
484
+ const vp = geo.vertexPackage;
485
+ const instanced = vp.instanceIndex !== -1;
486
+
487
+ if (!instanced) {
488
+ const batchIds = new Float32Array(vp.verticesCount || 0);
489
+ for (let j = 0; j < selectCount; j++) {
490
+ const id = r.u32();
491
+ const segCount = r.u32();
492
+ const entry: PickEntry = { batchId: j, segments: [] };
493
+ pickInfo[id] = entry;
494
+ for (let k = 0; k < segCount; k++) {
495
+ const vertexOffset = r.u32();
496
+ const vertexCount = r.u32();
497
+ entry.segments!.push({ vertexOffset, vertexCount });
498
+ batchIds.fill(j, vertexOffset, vertexOffset + vertexCount);
499
+ }
500
+ }
501
+ pushAttr(vp, 'batchId', new Uint8Array(batchIds.buffer), 1, 4, 0, undefined, 'f32');
502
+ } else {
503
+ const instanceCount = vp.instanceCount;
504
+ const instanceBatchIds = new Float32Array(instanceCount).fill(-1);
505
+ for (let j = 0; j < selectCount; j++) {
506
+ const id = r.u32();
507
+ const n = r.u32();
508
+ const entry: PickEntry = { instanceIds: [], instanceBatchId: j };
509
+ pickInfo[id] = entry;
510
+ for (let k = 0; k < n; k++) {
511
+ const instanceId = r.u32();
512
+ if (version >= 3) r.u32();
513
+ if (instanceId >= 0 && instanceId < instanceCount) {
514
+ entry.instanceIds!.push(instanceId);
515
+ instanceBatchIds[instanceId] = j;
516
+ }
517
+ }
518
+ }
519
+ // 实例化的单体粒度是「逐实例」的,不能塞进逐顶点属性:
520
+ // 顶点数(base mesh)和实例数根本不是一个量级,长度对不上会被 three 当成坏 buffer。
521
+ vp.instanceBatchIds = instanceBatchIds;
522
+ }
523
+
524
+ geo.pickInfo = pickInfo;
525
+ result[geometryName] = pickInfo;
526
+ }
527
+
528
+ return result;
529
+ }
530
+
531
+ /* ------------------------------------------------------------------ 入口 */
532
+
533
+ export async function parseS3MB(arrayBuffer: ArrayBuffer, options: ParseOptions = {}): Promise<ParsedTile> {
534
+ const inflate = options.inflate ?? defaultInflate;
535
+ const trace = options.trace;
536
+ const r = new BinaryReader(arrayBuffer);
537
+
538
+ const version = Number(r.f32().toFixed(2));
539
+
540
+ let compressedType = 0;
541
+ if (version >= 3) compressedType = r.u32();
542
+ if (version >= 2) r.u32(); // uncompressedSize
543
+ const compressedSize = r.u32();
544
+
545
+ const payload = new Uint8Array(arrayBuffer, r.base + r.offset, compressedSize);
546
+
547
+ let raw: Uint8Array;
548
+ if (version >= 3 && compressedType === 0) {
549
+ raw = payload;
550
+ } else {
551
+ raw = await inflate(payload);
552
+ }
553
+
554
+ const t = new BinaryReader(raw.buffer as ArrayBuffer, raw.byteOffset ?? 0, raw.byteLength);
555
+
556
+ const fileOptions = t.u32();
557
+ trace?.('fileOptions', fileOptions, t.offset);
558
+ const groupNode = parseGroupNode(t, version);
559
+ trace?.('groupNode', groupNode.pageLods.length, t.offset);
560
+ const geoPackage = await parseSkeletons(t, version, trace);
561
+ trace?.('skeletons', Object.keys(geoPackage).length, t.offset);
562
+ const textures = parseTextures(t);
563
+ trace?.('textures', Object.keys(textures).length, t.offset);
564
+ const materials = parseMaterials(t);
565
+ trace?.('materials', t.offset);
566
+ const pickInfo = parsePickInfo(t, fileOptions, geoPackage, version);
567
+ trace?.('pickInfo', t.offset);
568
+
569
+ let extensions: unknown;
570
+ if (version >= 3) {
571
+ const s = t.string();
572
+ extensions = s ? JSON.parse(s) : undefined;
573
+ }
574
+
575
+ return {
576
+ version,
577
+ compressedType,
578
+ options: fileOptions,
579
+ groupNode,
580
+ geoPackage,
581
+ textures,
582
+ materials,
583
+ pickInfo,
584
+ extensions,
585
+ bytesRead: t.offset,
586
+ bytesTotal: t.view.byteLength,
587
+ };
588
+ }
589
+
590
+ export type { VertexAttribute };