s3m-three 0.4.18 → 0.4.19

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 (130) hide show
  1. package/dist/browser/assets/s3mWorker-4iTAmWIu.js +1 -0
  2. package/dist/browser/s3m-three.iife.ext-three.js +406 -91
  3. package/dist/browser/s3m-three.iife.js +434 -119
  4. package/dist/core/GaussianSplat.d.ts +64 -0
  5. package/dist/core/GaussianSplat.d.ts.map +1 -0
  6. package/dist/core/GaussianSplat.js +139 -0
  7. package/dist/core/GaussianSplat.js.map +1 -0
  8. package/dist/core/S3M1Parser.d.ts +1 -1
  9. package/dist/core/S3M1Parser.d.ts.map +1 -1
  10. package/dist/core/S3M1Parser.js +320 -25
  11. package/dist/core/S3M1Parser.js.map +1 -1
  12. package/dist/core/S3MBParser.d.ts.map +1 -1
  13. package/dist/core/S3MBParser.js +13 -0
  14. package/dist/core/S3MBParser.js.map +1 -1
  15. package/dist/core/S3MBlockParser.d.ts.map +1 -1
  16. package/dist/core/S3MBlockParser.js +56 -14
  17. package/dist/core/S3MBlockParser.js.map +1 -1
  18. package/dist/core/SplatLod.d.ts +36 -0
  19. package/dist/core/SplatLod.d.ts.map +1 -0
  20. package/dist/core/SplatLod.js +54 -0
  21. package/dist/core/SplatLod.js.map +1 -0
  22. package/dist/core/SplatProjection.d.ts +75 -0
  23. package/dist/core/SplatProjection.d.ts.map +1 -0
  24. package/dist/core/SplatProjection.js +162 -0
  25. package/dist/core/SplatProjection.js.map +1 -0
  26. package/dist/core/SplatSort.d.ts +13 -0
  27. package/dist/core/SplatSort.d.ts.map +1 -0
  28. package/dist/core/SplatSort.js +14 -0
  29. package/dist/core/SplatSort.js.map +1 -0
  30. package/dist/core/TexCoordZ.d.ts +94 -0
  31. package/dist/core/TexCoordZ.d.ts.map +1 -0
  32. package/dist/core/TexCoordZ.js +125 -0
  33. package/dist/core/TexCoordZ.js.map +1 -0
  34. package/dist/core/TextureDecode.d.ts +19 -0
  35. package/dist/core/TextureDecode.d.ts.map +1 -1
  36. package/dist/core/TextureDecode.js +53 -11
  37. package/dist/core/TextureDecode.js.map +1 -1
  38. package/dist/core/TileGeometry.d.ts +29 -0
  39. package/dist/core/TileGeometry.d.ts.map +1 -1
  40. package/dist/core/TileGeometry.js +130 -14
  41. package/dist/core/TileGeometry.js.map +1 -1
  42. package/dist/core/degradations.d.ts +37 -0
  43. package/dist/core/degradations.d.ts.map +1 -0
  44. package/dist/core/degradations.js +32 -0
  45. package/dist/core/degradations.js.map +1 -0
  46. package/dist/core/types.d.ts +36 -0
  47. package/dist/core/types.d.ts.map +1 -1
  48. package/dist/index.d.ts +7 -1
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +4 -1
  51. package/dist/index.js.map +1 -1
  52. package/dist/render/DatasetManager.d.ts +11 -0
  53. package/dist/render/DatasetManager.d.ts.map +1 -1
  54. package/dist/render/DatasetManager.js +2 -0
  55. package/dist/render/DatasetManager.js.map +1 -1
  56. package/dist/render/FeatureStyleManager.d.ts.map +1 -1
  57. package/dist/render/FeatureStyleManager.js +7 -5
  58. package/dist/render/FeatureStyleManager.js.map +1 -1
  59. package/dist/render/GeometryBuilder.d.ts +24 -0
  60. package/dist/render/GeometryBuilder.d.ts.map +1 -1
  61. package/dist/render/GeometryBuilder.js +196 -5
  62. package/dist/render/GeometryBuilder.js.map +1 -1
  63. package/dist/render/Picker.d.ts.map +1 -1
  64. package/dist/render/Picker.js +19 -5
  65. package/dist/render/Picker.js.map +1 -1
  66. package/dist/render/S3MViewer.d.ts +69 -0
  67. package/dist/render/S3MViewer.d.ts.map +1 -1
  68. package/dist/render/S3MViewer.js +222 -12
  69. package/dist/render/S3MViewer.js.map +1 -1
  70. package/dist/render/SplatMaterial.d.ts +103 -0
  71. package/dist/render/SplatMaterial.d.ts.map +1 -0
  72. package/dist/render/SplatMaterial.js +388 -0
  73. package/dist/render/SplatMaterial.js.map +1 -0
  74. package/dist/render/SplatSorter.d.ts +14 -0
  75. package/dist/render/SplatSorter.d.ts.map +1 -0
  76. package/dist/render/SplatSorter.js +59 -0
  77. package/dist/render/SplatSorter.js.map +1 -0
  78. package/dist/render/StyleMaterial.d.ts +48 -0
  79. package/dist/render/StyleMaterial.d.ts.map +1 -1
  80. package/dist/render/StyleMaterial.js +292 -69
  81. package/dist/render/StyleMaterial.js.map +1 -1
  82. package/dist/render/TextureFactory.d.ts +10 -0
  83. package/dist/render/TextureFactory.d.ts.map +1 -1
  84. package/dist/render/TextureFactory.js +35 -5
  85. package/dist/render/TextureFactory.js.map +1 -1
  86. package/dist/render/lightingPreset.d.ts +53 -0
  87. package/dist/render/lightingPreset.d.ts.map +1 -0
  88. package/dist/render/lightingPreset.js +36 -0
  89. package/dist/render/lightingPreset.js.map +1 -0
  90. package/dist/render/materialAlpha.d.ts +51 -0
  91. package/dist/render/materialAlpha.d.ts.map +1 -0
  92. package/dist/render/materialAlpha.js +68 -0
  93. package/dist/render/materialAlpha.js.map +1 -0
  94. package/dist/tileset/S3MTileset.d.ts +42 -0
  95. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  96. package/dist/tileset/S3MTileset.js +52 -1
  97. package/dist/tileset/S3MTileset.js.map +1 -1
  98. package/dist/vendor/browser-node-stub.js +10 -0
  99. package/dist/worker/s3mWorker.js +1166 -979
  100. package/dist/worker/s3mWorker.js.map +1 -1
  101. package/docs/API.md +55 -1
  102. package/docs/FORMAT-NOTES.md +338 -0
  103. package/package.json +1 -1
  104. package/src/core/GaussianSplat.ts +175 -0
  105. package/src/core/S3M1Parser.ts +330 -24
  106. package/src/core/S3MBParser.ts +18 -0
  107. package/src/core/S3MBlockParser.ts +56 -11
  108. package/src/core/SplatLod.ts +83 -0
  109. package/src/core/SplatProjection.ts +231 -0
  110. package/src/core/SplatSort.ts +30 -0
  111. package/src/core/TexCoordZ.ts +160 -0
  112. package/src/core/TextureDecode.ts +63 -11
  113. package/src/core/TileGeometry.ts +138 -14
  114. package/src/core/degradations.ts +74 -0
  115. package/src/core/types.ts +32 -0
  116. package/src/index.ts +8 -1
  117. package/src/render/DatasetManager.ts +10 -0
  118. package/src/render/FeatureStyleManager.ts +7 -5
  119. package/src/render/GeometryBuilder.ts +239 -6
  120. package/src/render/Picker.ts +21 -5
  121. package/src/render/S3MViewer.ts +255 -11
  122. package/src/render/SplatMaterial.ts +433 -0
  123. package/src/render/SplatSorter.ts +59 -0
  124. package/src/render/StyleMaterial.ts +339 -69
  125. package/src/render/TextureFactory.ts +39 -5
  126. package/src/render/lightingPreset.ts +76 -0
  127. package/src/render/materialAlpha.ts +75 -0
  128. package/src/tileset/S3MTileset.ts +78 -1
  129. package/src/vendor/browser-node-stub.js +10 -0
  130. package/dist/browser/assets/s3mWorker-BjPOUgjg.js +0 -1
@@ -106,8 +106,16 @@ export function deinterleave(attr: VertexAttribute): PlainAttribute {
106
106
  }
107
107
 
108
108
  /** 把 u8 顶点色转成 float vec3,供着色器直接相乘(原 toColorAttribute) */
109
+ /**
110
+ * 顶点色解成 RGBA。
111
+ *
112
+ * **必须带 alpha**:BIM 数据里"材质不透明、靠顶点色 alpha 表达半透明"是常见做法。
113
+ * 实测 `3D-BIMbuilding-2` 的 SMID 4421(`WhiteMaterial_1`,无纹理、`diffuse.a=1`)
114
+ * 顶点色是 u8 `(0,128,192,63)`,alpha = 0.247 —— 之前只取 RGB,这一片就变成实心青色。
115
+ * 官方片元里 `baseColorWithAlpha = vColor` 是 vec4,走的就是这条路。
116
+ */
109
117
  function colorData(attr: VertexAttribute | null, vertexCount: number): Float32Array {
110
- const out = new Float32Array(vertexCount * 3);
118
+ const out = new Float32Array(vertexCount * 4);
111
119
  if (!attr) {
112
120
  out.fill(1);
113
121
  return out;
@@ -115,11 +123,14 @@ function colorData(attr: VertexAttribute | null, vertexCount: number): Float32Ar
115
123
  const src = attr.data;
116
124
  const comp = attr.components ?? 4;
117
125
  const scale = attr.dtype === 'u8' ? 1 / 255 : 1;
126
+ // 只有 RGB 的旧数据补 alpha = 1,避免留下 0(那会整片全透明)
127
+ const hasAlpha = comp >= 4;
118
128
  for (let i = 0; i < vertexCount; i++) {
119
129
  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;
130
+ out[i * 4] = (src[o] ?? 255) * scale;
131
+ out[i * 4 + 1] = (src[o + 1] ?? 255) * scale;
132
+ out[i * 4 + 2] = (src[o + 2] ?? 255) * scale;
133
+ out[i * 4 + 3] = hasAlpha ? (src[o + 3] ?? 255) * scale : 1;
123
134
  }
124
135
  return out;
125
136
  }
@@ -232,6 +243,11 @@ export interface PrimitiveAttributes {
232
243
  aAtlasRect?: PlainAttribute;
233
244
  aAtlasRect1?: PlainAttribute;
234
245
  aSecUv?: PlainAttribute;
246
+ /**
247
+ * uv 第三分量(文件里的 `custom0`,官方 `aTextureCoordMatrix`)。
248
+ * 只有存在该流时才产生;解码语义见 core/TexCoordZ.ts。
249
+ */
250
+ aTexCoordZ?: PlainAttribute;
235
251
  }
236
252
 
237
253
  export interface PrimitiveData {
@@ -258,6 +274,8 @@ export interface TileGeometryStats {
258
274
  edgeLines: number;
259
275
  /** 合批前独立网格数(batching 生效时 > meshes) */
260
276
  originalMeshes?: number;
277
+ /** 被剔除的「同单体共面重复」三角形数(数据自重叠,见 dropCoincidentTriangles) */
278
+ deduped?: number;
261
279
  skipped: string[];
262
280
  }
263
281
 
@@ -355,6 +373,7 @@ function mergeCandidates(candidates: Candidate[]): PrimitiveData {
355
373
  let hasUv = false;
356
374
  let hasUv1 = false;
357
375
  let hasAtlas = false;
376
+ let hasTexZ = false;
358
377
 
359
378
  for (const c of candidates) {
360
379
  totalVertices += c.vertexCount;
@@ -363,17 +382,19 @@ function mergeCandidates(candidates: Candidate[]): PrimitiveData {
363
382
  if (c.attrs.uv) hasUv = true;
364
383
  if (c.attrs.uv1) hasUv1 = true;
365
384
  if (c.attrs.aAtlasRect) hasAtlas = true;
385
+ if (c.attrs.aTexCoordZ) hasTexZ = true;
366
386
  }
367
387
 
368
388
  const mergedPos: PlainAttribute = { array: new Float32Array(totalVertices * 3), components: 3, normalized: false };
369
389
  const mergedNorm = hasNormal ? { array: new Float32Array(totalVertices * 3), components: 3, normalized: false } : null;
370
390
  const mergedUv = hasUv ? { array: new Float32Array(totalVertices * 2), components: 2, normalized: false } : null;
371
391
  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 };
392
+ const mergedColor: PlainAttribute = { array: new Float32Array(totalVertices * 4), components: 4, normalized: false };
373
393
  const mergedBatchId: PlainAttribute = { array: new Float32Array(totalVertices), components: 1, normalized: false };
374
394
  const mergedRect0 = hasAtlas ? { array: new Float32Array(totalVertices * 4), components: 4, normalized: false } : null;
375
395
  const mergedRect1 = hasAtlas ? { array: new Float32Array(totalVertices * 4), components: 4, normalized: false } : null;
376
396
  const mergedSecUv = hasAtlas ? { array: new Float32Array(totalVertices * 2), components: 2, normalized: false } : null;
397
+ const mergedTexZ = hasTexZ ? { array: new Float32Array(totalVertices), components: 1, normalized: false } : null;
377
398
  const mergedIndices = (totalVertices > 65535 ? new Uint32Array(totalIndices) : new Uint16Array(totalIndices)) as Uint16Array | Uint32Array;
378
399
 
379
400
  const tmp = [0, 0, 0];
@@ -389,6 +410,8 @@ function mergeCandidates(candidates: Candidate[]): PrimitiveData {
389
410
  const normArr = attrs.normal?.array;
390
411
  const uvArr = attrs.uv?.array;
391
412
  const uv1Arr = attrs.uv1?.array;
413
+ const tzArr = attrs.aTexCoordZ?.array;
414
+ const tzOut = mergedTexZ?.array as Float32Array | undefined;
392
415
  const colArr = attrs.aColor.array;
393
416
  const bArr = attrs.aBatchId.array;
394
417
  const posOut = mergedPos.array as Float32Array;
@@ -433,15 +456,19 @@ function mergeCandidates(candidates: Candidate[]): PrimitiveData {
433
456
  uv1Out[dst * 2] = uv1Arr[i * 2];
434
457
  uv1Out[dst * 2 + 1] = uv1Arr[i * 2 + 1];
435
458
  }
436
- // aColor
459
+ // aTexCoordZ(uv 第三分量):缺这条流的候选补 0 = 单位变换
460
+ if (tzOut) tzOut[dst] = tzArr ? tzArr[i]! : 0;
461
+ // aColor(RGBA:alpha 是 4421 这类「材质不透明、顶点半透明」的唯一来源)
437
462
  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];
463
+ colOut[dst * 4] = colArr[i * 4];
464
+ colOut[dst * 4 + 1] = colArr[i * 4 + 1];
465
+ colOut[dst * 4 + 2] = colArr[i * 4 + 2];
466
+ colOut[dst * 4 + 3] = colArr[i * 4 + 3] ?? 1;
441
467
  } else {
442
- colOut[dst * 3] = 1;
443
- colOut[dst * 3 + 1] = 1;
444
- colOut[dst * 3 + 2] = 1;
468
+ colOut[dst * 4] = 1;
469
+ colOut[dst * 4 + 1] = 1;
470
+ colOut[dst * 4 + 2] = 1;
471
+ colOut[dst * 4 + 3] = 1;
445
472
  }
446
473
  // aBatchId
447
474
  bOut[dst] = bArr ? bArr[i] : -1;
@@ -501,6 +528,7 @@ function mergeCandidates(candidates: Candidate[]): PrimitiveData {
501
528
  aAtlasRect: mergedRect0 ?? undefined,
502
529
  aAtlasRect1: mergedRect1 ?? undefined,
503
530
  aSecUv: mergedSecUv ?? undefined,
531
+ aTexCoordZ: mergedTexZ ?? undefined,
504
532
  },
505
533
  index: mergedIndices,
506
534
  bounds,
@@ -529,7 +557,7 @@ function assembleSkeleton(name: string, geo: GeoPackageEntry, tile: ParsedTile,
529
557
  const vertexCount = vp.verticesCount || 0;
530
558
  const attrs: PrimitiveAttributes = {
531
559
  position: deinterleave(positionAttr),
532
- aColor: { array: new Float32Array(0), components: 3, normalized: false },
560
+ aColor: { array: new Float32Array(0), components: 4, normalized: false },
533
561
  aBatchId: { array: new Float32Array(0), components: 1, normalized: false },
534
562
  };
535
563
  let colorAttr: VertexAttribute | null = null;
@@ -537,6 +565,7 @@ function assembleSkeleton(name: string, geo: GeoPackageEntry, tile: ParsedTile,
537
565
  let normalData: PlainAttribute | null = null;
538
566
  let uvData: PlainAttribute | null = null;
539
567
  let uv1Data: PlainAttribute | null = null;
568
+ let texCoordZData: PlainAttribute | null = null;
540
569
 
541
570
  for (const attr of vp.vertexAttributes) {
542
571
  if (attr.divisor !== undefined) continue; // 实例属性不进顶点流
@@ -546,15 +575,22 @@ function assembleSkeleton(name: string, geo: GeoPackageEntry, tile: ParsedTile,
546
575
  else if (attr.semantic === 'normal') normalData = deinterleave(attr);
547
576
  else if (attr.semantic === 'uv') uvData = deinterleave(attr);
548
577
  else if (attr.semantic === 'uv2') uv1Data = deinterleave(attr);
578
+ // custom0 = uv 的第三分量,官方 aTextureCoordMatrix:决定「这张贴图用不用、
579
+ // 用图集的哪个子区域、按 2 的几次幂缩放」。BIM/OSGB 转出来的数据依赖它,
580
+ // 丢掉就会把本该纯色的墙面贴上整张材质图。语义见 core/TexCoordZ.ts。
581
+ else if (attr.semantic === 'custom0' && (attr.components ?? 1) >= 1) {
582
+ texCoordZData = deinterleave(attr);
583
+ }
549
584
  }
550
585
 
551
- attrs.aColor = { array: colorData(colorAttr, vertexCount), components: 3, normalized: false };
586
+ attrs.aColor = { array: colorData(colorAttr, vertexCount), components: 4, normalized: false };
552
587
  attrs.aBatchId = batchAttr
553
588
  ? { array: deinterleave(batchAttr).array, components: 1, normalized: false }
554
589
  : { array: new Float32Array(vertexCount).fill(-1), components: 1, normalized: false };
555
590
  if (normalData) attrs.normal = normalData;
556
591
  if (uvData) attrs.uv = uvData;
557
592
  if (uv1Data) attrs.uv1 = uv1Data;
593
+ if (texCoordZData) attrs.aTexCoordZ = texCoordZData;
558
594
 
559
595
  // S3MBLOCK 图集:逐顶点带子区域矩形;烘焙图用第二套 UV(aSecUv)
560
596
  const rects = tile.atlasRects?.[name];
@@ -624,6 +660,77 @@ function primitiveOf(
624
660
  };
625
661
  }
626
662
 
663
+ /**
664
+ * 丢弃「同单体、同位置、同绕向」的重复三角形。
665
+ *
666
+ * 为什么需要:OSGB 转出来的 S3M 里确实存在**模型自重叠**。实测
667
+ * `3D-BIMbuilding-2` 的 `Tile_-85071_248749_0000` 中 geo E2F8F0 有 7156 个三角形,
668
+ * 其中 2276 个与前面某个三角形的三个顶点坐标完全相同(4 位小数,0.1 mm 内),
669
+ * `Tile_-85071_248750_0000` 的 geo 386340 是 58/156 —— 文件级统计,不是我们合批造出来的。
670
+ *
671
+ * 两个共面三角形各自光栅化时深度值只差最后几位,深度缓冲分不出谁在前,
672
+ * 屏幕上就是一片斜向摩尔纹(用户说的「模型重叠的斜线」);若两份材质不同,
673
+ * 还会随绘制顺序闪成另一种颜色。
674
+ *
675
+ * 只丢**同 batchId** 的重复:跨单体的重叠是数据本身两个单体压在一起,
676
+ * 丢掉任何一个都会让那个 SMID 变得不可见 / 不可拾取,只能留给业务层处理。
677
+ *
678
+ * @param quantum 顶点坐标量化步长(米)。1 mm 足以合并「同一条边」的重复,
679
+ * 又不会误伤真正相邻的两个面。
680
+ */
681
+ export function dropCoincidentTriangles(
682
+ index: Uint16Array | Uint32Array,
683
+ positions: ArrayLike<number>,
684
+ batchIds: ArrayLike<number> | null,
685
+ quantum = 1e-3
686
+ ): { index: Uint16Array | Uint32Array; dropped: number } {
687
+ const triCount = Math.floor(index.length / 3);
688
+ if (triCount === 0) return { index, dropped: 0 };
689
+
690
+ const inv = 1 / quantum;
691
+ const keyOf = (v: number): string =>
692
+ `${Math.round(positions[v * 3] * inv)},${Math.round(positions[v * 3 + 1] * inv)},${Math.round(positions[v * 3 + 2] * inv)}`;
693
+
694
+ const seen = new Set<string>();
695
+ const out = new (index instanceof Uint32Array ? Uint32Array : Uint16Array)(index.length);
696
+ let w = 0;
697
+ let dropped = 0;
698
+ for (let t = 0; t < triCount; t++) {
699
+ const a = index[t * 3];
700
+ const b = index[t * 3 + 1];
701
+ const c = index[t * 3 + 2];
702
+ const ka = keyOf(a);
703
+ const kb = keyOf(b);
704
+ const kc = keyOf(c);
705
+ // 把字典序最小的顶点转到首位,**保持绕向**:
706
+ // 绕向相同的两份才是「同一个面画了两遍」;正反两份是有意做的双面片,
707
+ // 它们共面且绕向相反,深度相等时后者覆盖前者、着色一致,不会出摩尔纹。
708
+ let k1 = ka;
709
+ let k2 = kb;
710
+ let k3 = kc;
711
+ if (kb <= ka && kb <= kc) {
712
+ k1 = kb;
713
+ k2 = kc;
714
+ k3 = ka;
715
+ } else if (kc <= ka && kc <= kb) {
716
+ k1 = kc;
717
+ k2 = ka;
718
+ k3 = kb;
719
+ }
720
+ const key = k1 + k2 + k3 + (batchIds ? `#${batchIds[a]}` : '');
721
+ if (seen.has(key)) {
722
+ dropped++;
723
+ continue;
724
+ }
725
+ seen.add(key);
726
+ out[w++] = a;
727
+ out[w++] = b;
728
+ out[w++] = c;
729
+ }
730
+ if (dropped === 0) return { index, dropped: 0 };
731
+ return { index: out.slice(0, w) as Uint16Array | Uint32Array, dropped };
732
+ }
733
+
627
734
  /**
628
735
  * 瓦片几何组装入口。与 GeometryBuilder 的摆放/分组逻辑逐位对齐,
629
736
  * 但只产出纯数据 —— three 包装在渲染层。
@@ -741,5 +848,22 @@ export function assembleTileGeometry(tile: ParsedTile, options: AssembleOptions
741
848
  }
742
849
  }
743
850
 
851
+ // 数据自重叠:同单体、同位置、同绕向的重复三角形会互相压深度,出斜向摩尔纹。
852
+ // 只在**单 geo** 的 primitive 上做:合批产物里多个 geo 的 batchId 各自从 0 起,
853
+ // 混在一起会把「不同单体恰好共面」误判成同一条。
854
+ for (const p of primitives) {
855
+ if (p.primitiveType !== 4 || !p.index) continue;
856
+ if (p.geoNames.length > 1) continue;
857
+ const positions = p.attributes.position?.array;
858
+ if (!positions) continue;
859
+ const batchIds = p.attributes.aBatchId?.array ?? null;
860
+ const { index, dropped } = dropCoincidentTriangles(p.index, positions, batchIds);
861
+ if (dropped > 0) {
862
+ p.index = index;
863
+ p.indexCount = index.length;
864
+ stats.deduped = (stats.deduped ?? 0) + dropped;
865
+ }
866
+ }
867
+
744
868
  return { primitives, stats };
745
869
  }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * 降级上报:解析成功、但能力不完整的数据要让调用方看得见
3
+ *
4
+ * 分界:**「解析不了」应该抛错,「解析得了但画不出该有的样子」必须显式说明。**
5
+ * 前者是 fail loudly,后者最容易被写成静默降级 —— 用户看到一个能跑但不对的画面,
6
+ * 会去怀疑数据、怀疑网络、怀疑显卡,很难怀疑到「这个分支还没做完」。
7
+ *
8
+ * 目前唯一一类降级是 3DGS:数据里的 `gsScale` / `gsRotation` 已经保留成顶点属性,
9
+ * 但那 4 字节的量化格式没有标定,所以只能按 Points 渲染,画不出 EWA splatting。
10
+ * 详见 `docs/FORMAT-NOTES.md` 第九节。
11
+ *
12
+ * 策略:
13
+ * 'warn'(默认) —— 首次遇到告警一次,画照画。不传参时与旧版行为一致(旧版不抛)。
14
+ * 'throw' —— 直接拒绝这块瓦片,标 `permanent` 让调度器不要重试。
15
+ * 给「宁可不出图,也不要出一张错的图」的场景用。
16
+ * 'silent' —— 只记录到 `stats.degradations`,不告警。给已经自己接管提示的 UI 用。
17
+ */
18
+ export type DegradationKind = 'gaussian-splatting-points';
19
+
20
+ export type UnsupportedPolicy = 'warn' | 'throw' | 'silent';
21
+
22
+ /** 告警文案要自己解释清楚「看到了什么」和「为什么不对」,不要只说一句 "unsupported" */
23
+ const MESSAGES: Record<DegradationKind, string> = {
24
+ 'gaussian-splatting-points':
25
+ '[s3m] 数据声明是 3DGS(高斯泼溅),但骨架里没有 gsScale / gsRotation 这两条流,' +
26
+ '只能按点云渲染。这不是数据损坏,但也不是预期的泼溅效果 —— ' +
27
+ '若确认是导出配置问题,可以 unsupported: "throw" 让它直接报错。',
28
+ };
29
+
30
+ export interface DegradationPlan {
31
+ /** 本次新出现的降级项(调用方据此告警) */
32
+ fresh: DegradationKind[];
33
+ /** 合并后的已知清单,写回 `stats.degradations` */
34
+ known: DegradationKind[];
35
+ /** 需要输出的告警文案(`silent` 时为空) */
36
+ messages: string[];
37
+ /** `throw` 策略下要抛的错(已标 `permanent`,调度器不要重试) */
38
+ error?: Error;
39
+ }
40
+
41
+ /**
42
+ * 纯函数:给定「本次遇到的降级项」和「已经报过的」,算出要做什么。
43
+ *
44
+ * 去重是必须的:一个 3DGS 服务一上来几十块瓦片都是同一个原因,
45
+ * 不去重就是几十条一模一样的告警,把真正有用的日志冲掉。
46
+ */
47
+ export function planDegradations(
48
+ incoming: readonly DegradationKind[],
49
+ known: readonly DegradationKind[],
50
+ policy: UnsupportedPolicy
51
+ ): DegradationPlan {
52
+ const knownSet = new Set(known);
53
+ const fresh: DegradationKind[] = [];
54
+ for (const kind of incoming) {
55
+ if (!knownSet.has(kind)) {
56
+ knownSet.add(kind);
57
+ fresh.push(kind);
58
+ }
59
+ }
60
+
61
+ const messages = policy === 'warn' ? fresh.map((k) => MESSAGES[k] ?? `[s3m] 降级:${k}`) : [];
62
+
63
+ if (policy === 'throw' && fresh.length > 0) {
64
+ const err = new Error(
65
+ `[s3m] 数据包含当前实现不完整的特性,已按 unsupported: "throw" 拒绝:${fresh.join(', ')}。` +
66
+ `详见 docs/FORMAT-NOTES.md`
67
+ ) as Error & { permanent?: boolean };
68
+ // 格式层面的能力缺失,重试一万次也不会变好
69
+ err.permanent = true;
70
+ return { fresh, known: [...knownSet], messages, error: err };
71
+ }
72
+
73
+ return { fresh, known: [...knownSet], messages };
74
+ }
package/src/core/types.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * 这里只放「跨模块共享」的结构定义。模块内部的私有类型各自就近声明。
5
5
  */
6
6
  import type { Color } from 'three';
7
+ import type { DegradationKind } from './degradations.js';
7
8
 
8
9
  /* ------------------------------------------------------------------ 数学 */
9
10
 
@@ -200,6 +201,14 @@ export interface ParsedTile {
200
201
  materials: unknown;
201
202
  pickInfo: unknown;
202
203
  extensions?: unknown;
204
+ /**
205
+ * 解析成功、但渲染能力不完整的项(机器可读)。
206
+ *
207
+ * 空 / 不传 = 这份数据的每一项能力都实现了。目前只会由 3DGS 数据填上
208
+ * `'gaussian-splatting-points'` —— 那种数据能加载、能看出轮廓,但画不出泼溅。
209
+ * 处置策略见 `core/degradations.ts` 与 `S3MTilesetOptions.unsupported`。
210
+ */
211
+ degradations?: DegradationKind[];
203
212
  /** 实际消费的字节数,应与 bytesTotal 相等 */
204
213
  bytesRead: number;
205
214
  bytesTotal: number;
@@ -230,6 +239,20 @@ export interface Stats {
230
239
  textures: number;
231
240
  /** 显存里驻留的几何数(renderer.info.memory.geometries) */
232
241
  geometries: number;
242
+ /**
243
+ * 泼溅瓦片里高斯最长轴的中位长度(米),用来估「屏幕上一个高斯多大」。
244
+ * 非泼溅数据为 undefined;顶点统计接口也会带上这个可选字段。
245
+ */
246
+ splatSigma?: number;
247
+ /** 泼溅瓦片里的高斯实例数;非泼溅数据为 undefined */
248
+ splatCount?: number;
249
+ /**
250
+ * 当前场景里出现过的降级项(去重)。空数组 = 所有数据的能力都完整实现了。
251
+ *
252
+ * 这是给 UI 用的:状态栏 / 诊断面板可以直接读它,不必去翻 console。
253
+ * 语义与 `ParsedTile.degradations` 一致,只是从「单块瓦片」聚到「整个场景」。
254
+ */
255
+ degradations: DegradationKind[];
233
256
  }
234
257
 
235
258
  /* ------------------------------------------------------------------ 单体属性 */
@@ -291,6 +314,15 @@ export interface StyleUniforms {
291
314
  uTexMix?: { value: number };
292
315
  /** 基础漫反射底色增益 / 直通自发光微调(0~1,默认 0.0) */
293
316
  uAmbientGain?: { value: number };
317
+ /**
318
+ * 金属度覆盖(0~1,**负数 = 不覆盖**,保持材质自身的值)。
319
+ *
320
+ * PBR 材质才有意义:注入在 `metalnessmap_fragment` 之后直接改写 metalnessFactor,
321
+ * 不重建材质也就不会重编译 program。lambert 光照模型没有这个 chunk,会被忽略。
322
+ */
323
+ uMetalness?: { value: number };
324
+ /** 粗糙度覆盖(0~1,负数 = 不覆盖)。同上,只有 pbr 生效。 */
325
+ uRoughness?: { value: number };
294
326
  /** 双纹理交叉淡化插值因子(0~1,0 = 状态 A,1 = 状态 B) */
295
327
  uBatchMix?: { value: number };
296
328
  /** 图层整体透明度(共享 uniform,透明度动画靠它做到 O(1)) */
package/src/index.ts CHANGED
@@ -14,6 +14,8 @@ export { parseS3MB, S3MFormatError, S3M_VERSION, VertexTag, VertexTagV3 } from '
14
14
  export { parseS3M1, isS3M1Buffer } from './core/S3M1Parser.js';
15
15
  export type { ParseOptions, InflateFn } from './core/S3MBParser.js';
16
16
  export { BinaryReader } from './core/BinaryReader.js';
17
+ export { planDegradations } from './core/degradations.js';
18
+ export type { DegradationKind, UnsupportedPolicy, DegradationPlan } from './core/degradations.js';
17
19
  export { resolveFeature, measureSegments, buildAttributes, DEFAULT_FLOOR_HEIGHT } from './core/FeatureInfo.js';
18
20
  export { AttributeQuery } from './core/AttributeQuery.js';
19
21
  export type {
@@ -110,10 +112,15 @@ export type {
110
112
  PathFollowOptions,
111
113
  } from './render/S3MViewer.js';
112
114
 
115
+ export { LIGHT_PRESETS, DEFAULT_LIGHT_PRESET, SUPERMAP_LIGHT_PRESET, resolveLightingPreset } from './render/lightingPreset.js';
116
+ export type { LightingPreset, PresetLightIntensities } from './render/lightingPreset.js';
117
+
113
118
  export { buildTileObjects } from './render/GeometryBuilder.js';
114
119
  export type { BuildTileOptions, BuildTileResult, TileBuildStats } from './render/GeometryBuilder.js';
115
120
 
116
- export { createTexture, PixelFormat, setMaxAnisotropy, setMaxTextureSize } from './render/TextureFactory.js';
121
+ export { createTexture, PixelFormat, setMaxAnisotropy, setMaxTextureSize, setSrgbTextureDecode } from './render/TextureFactory.js';
122
+ export { setColorPipeline, getColorPipeline } from './render/StyleMaterial.js';
123
+ export type { ColorPipeline } from './render/StyleMaterial.js';
117
124
  export type { DecodedTexture } from './render/TextureFactory.js';
118
125
 
119
126
  export { resolveTextureKind, dxtPixelFormatOf, TextureCompress } from './core/TextureFormat.js';
@@ -11,6 +11,7 @@ import * as THREE from 'three';
11
11
  import { S3MTileset, usableGeoBounds } from '../tileset/S3MTileset.js';
12
12
  import type { AuthenticateFn } from '../core/AuthFetch.js';
13
13
  import type { Anchor } from '../core/types.js';
14
+ import type { UnsupportedPolicy } from '../core/degradations.js';
14
15
  import { worldFromLngLat, type GeoAdapter } from './GeoAdapter.js';
15
16
 
16
17
  export type DatasetAlignment = 'origin' | 'bottom' | 'top' | 'center';
@@ -38,6 +39,8 @@ export interface S3MDatasetOptions {
38
39
  heightOffset?: number;
39
40
  /** 是否用 Web Worker 解析经典 .s3mb 瓦片(默认开,崩溃自动降级主线程),见 S3MTilesetOptions.useWorker */
40
41
  useWorker?: boolean;
42
+ /** 三角面材质深度偏移(共面摩尔纹 / 重合面归属),见 BuildTileOptions.polygonOffset */
43
+ polygonOffset?: { factor: number; units: number };
41
44
  /**
42
45
  * 是否对该数据集生成几何棱线(CPU 端 EdgesGeometry)。
43
46
  * - 不填:跟随全局 `style.edges`
@@ -49,6 +52,11 @@ export interface S3MDatasetOptions {
49
52
  * 适合多数据集走不同网关 / 不同 token 的场景。见 `AuthenticateFn`。
50
53
  */
51
54
  authenticate?: AuthenticateFn;
55
+ /**
56
+ * 本数据集遇到「解析得了但画不出该有的样子」时怎么办(默认 `'warn'`)。
57
+ * 目前只有 3DGS:按点渲染,做不出泼溅。见 `S3MTilesetOptions.unsupported`。
58
+ */
59
+ unsupported?: UnsupportedPolicy;
52
60
  }
53
61
 
54
62
  export interface S3MDatasetEntry {
@@ -211,8 +219,10 @@ export class DatasetManager {
211
219
  maxImageCache: ds.maxImageCache ?? maxImageCache,
212
220
  lodRangeScale: ds.lodRangeScale ?? defaultLodRangeScale,
213
221
  buildTile: buildTile ?? null,
222
+ polygonOffset: ds.polygonOffset,
214
223
  useWorker: ds.useWorker,
215
224
  authenticate: ds.authenticate ?? authenticate,
225
+ unsupported: ds.unsupported,
216
226
  uniforms: uniforms,
217
227
  onTile: (node) => {
218
228
  callbacks.onTile(node, entry);
@@ -20,6 +20,7 @@ import {
20
20
  } from '../core/FeatureInfo.js';
21
21
  import type { AttributeQuery, AttributeQueryOptions, AttributeRow } from '../core/AttributeQuery.js';
22
22
  import type { StyledMaterial } from './StyleMaterial.js';
23
+ import { restoreBaseBlendState } from './StyleMaterial.js';
23
24
 
24
25
  export interface GPUPathOptions {
25
26
  /** 绑定的路径索引 ID(0, 1, 2...) */
@@ -1011,8 +1012,9 @@ export class FeatureStyleManager {
1011
1012
  if (mat.userData.uBatchTexActive) {
1012
1013
  mat.userData.uBatchTexActive.value = 0;
1013
1014
  }
1014
- // 单体淡化清除后,把材质 transparent 还原成全局透明度语义
1015
- if (mat.transparent && !this._isLayerTransparent()) mat.transparent = false;
1015
+ // 单体淡化清除后,把材质 transparent 还原成「自身语义 || 全局透明度」
1016
+ // (只看全局透明度会把 alphaMode blend / 顶点 alpha 材质误关成不透明)
1017
+ if (mat.transparent && !this._isLayerTransparent()) restoreBaseBlendState(mat, false);
1016
1018
  return;
1017
1019
  }
1018
1020
 
@@ -1020,7 +1022,7 @@ export class FeatureStyleManager {
1020
1022
  if (mat.userData.uBatchTexActive) {
1021
1023
  mat.userData.uBatchTexActive.value = 0;
1022
1024
  }
1023
- if (mat.transparent && !this._isLayerTransparent()) mat.transparent = false;
1025
+ if (mat.transparent && !this._isLayerTransparent()) restoreBaseBlendState(mat, false);
1024
1026
  return;
1025
1027
  }
1026
1028
 
@@ -1052,9 +1054,9 @@ export class FeatureStyleManager {
1052
1054
  this.featureStyles
1053
1055
  );
1054
1056
 
1055
- // 有淡化单体时整块材质要开混合才有透明效果;清除后还原为全局透明度语义
1057
+ // 有淡化单体时整块材质要开混合才有透明效果;清除后还原为「自身语义 || 全局透明度」
1056
1058
  if (hasFade && !mat.transparent) mat.transparent = true;
1057
- else if (!hasFade && mat.transparent && !this._isLayerTransparent()) mat.transparent = false;
1059
+ else if (!hasFade && mat.transparent && !this._isLayerTransparent()) restoreBaseBlendState(mat, false);
1058
1060
 
1059
1061
  if (conflicts > 0 && !this._batchConflictWarned) {
1060
1062
  this._batchConflictWarned = true;