s3m-three 0.4.17 → 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 (145) 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/AttributeQuery.d.ts +13 -0
  5. package/dist/core/AttributeQuery.d.ts.map +1 -1
  6. package/dist/core/AttributeQuery.js +20 -3
  7. package/dist/core/AttributeQuery.js.map +1 -1
  8. package/dist/core/AuthFetch.d.ts +52 -0
  9. package/dist/core/AuthFetch.d.ts.map +1 -0
  10. package/dist/core/AuthFetch.js +44 -0
  11. package/dist/core/AuthFetch.js.map +1 -0
  12. package/dist/core/GaussianSplat.d.ts +64 -0
  13. package/dist/core/GaussianSplat.d.ts.map +1 -0
  14. package/dist/core/GaussianSplat.js +139 -0
  15. package/dist/core/GaussianSplat.js.map +1 -0
  16. package/dist/core/S3M1Parser.d.ts +1 -1
  17. package/dist/core/S3M1Parser.d.ts.map +1 -1
  18. package/dist/core/S3M1Parser.js +320 -25
  19. package/dist/core/S3M1Parser.js.map +1 -1
  20. package/dist/core/S3MBParser.d.ts.map +1 -1
  21. package/dist/core/S3MBParser.js +13 -0
  22. package/dist/core/S3MBParser.js.map +1 -1
  23. package/dist/core/S3MBlockParser.d.ts.map +1 -1
  24. package/dist/core/S3MBlockParser.js +56 -14
  25. package/dist/core/S3MBlockParser.js.map +1 -1
  26. package/dist/core/SplatLod.d.ts +36 -0
  27. package/dist/core/SplatLod.d.ts.map +1 -0
  28. package/dist/core/SplatLod.js +54 -0
  29. package/dist/core/SplatLod.js.map +1 -0
  30. package/dist/core/SplatProjection.d.ts +75 -0
  31. package/dist/core/SplatProjection.d.ts.map +1 -0
  32. package/dist/core/SplatProjection.js +162 -0
  33. package/dist/core/SplatProjection.js.map +1 -0
  34. package/dist/core/SplatSort.d.ts +13 -0
  35. package/dist/core/SplatSort.d.ts.map +1 -0
  36. package/dist/core/SplatSort.js +14 -0
  37. package/dist/core/SplatSort.js.map +1 -0
  38. package/dist/core/TexCoordZ.d.ts +94 -0
  39. package/dist/core/TexCoordZ.d.ts.map +1 -0
  40. package/dist/core/TexCoordZ.js +125 -0
  41. package/dist/core/TexCoordZ.js.map +1 -0
  42. package/dist/core/TextureDecode.d.ts +19 -0
  43. package/dist/core/TextureDecode.d.ts.map +1 -1
  44. package/dist/core/TextureDecode.js +53 -11
  45. package/dist/core/TextureDecode.js.map +1 -1
  46. package/dist/core/TileGeometry.d.ts +29 -0
  47. package/dist/core/TileGeometry.d.ts.map +1 -1
  48. package/dist/core/TileGeometry.js +130 -14
  49. package/dist/core/TileGeometry.js.map +1 -1
  50. package/dist/core/degradations.d.ts +37 -0
  51. package/dist/core/degradations.d.ts.map +1 -0
  52. package/dist/core/degradations.js +32 -0
  53. package/dist/core/degradations.js.map +1 -0
  54. package/dist/core/types.d.ts +36 -0
  55. package/dist/core/types.d.ts.map +1 -1
  56. package/dist/index.d.ts +11 -1
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +6 -1
  59. package/dist/index.js.map +1 -1
  60. package/dist/render/DatasetManager.d.ts +18 -1
  61. package/dist/render/DatasetManager.d.ts.map +1 -1
  62. package/dist/render/DatasetManager.js +4 -1
  63. package/dist/render/DatasetManager.js.map +1 -1
  64. package/dist/render/FeatureStyleManager.d.ts.map +1 -1
  65. package/dist/render/FeatureStyleManager.js +7 -5
  66. package/dist/render/FeatureStyleManager.js.map +1 -1
  67. package/dist/render/GeometryBuilder.d.ts +24 -0
  68. package/dist/render/GeometryBuilder.d.ts.map +1 -1
  69. package/dist/render/GeometryBuilder.js +196 -5
  70. package/dist/render/GeometryBuilder.js.map +1 -1
  71. package/dist/render/HostRenderer.d.ts +105 -0
  72. package/dist/render/HostRenderer.d.ts.map +1 -0
  73. package/dist/render/HostRenderer.js +170 -0
  74. package/dist/render/HostRenderer.js.map +1 -0
  75. package/dist/render/Picker.d.ts.map +1 -1
  76. package/dist/render/Picker.js +19 -5
  77. package/dist/render/Picker.js.map +1 -1
  78. package/dist/render/S3MViewer.d.ts +79 -0
  79. package/dist/render/S3MViewer.d.ts.map +1 -1
  80. package/dist/render/S3MViewer.js +235 -16
  81. package/dist/render/S3MViewer.js.map +1 -1
  82. package/dist/render/SplatMaterial.d.ts +103 -0
  83. package/dist/render/SplatMaterial.d.ts.map +1 -0
  84. package/dist/render/SplatMaterial.js +388 -0
  85. package/dist/render/SplatMaterial.js.map +1 -0
  86. package/dist/render/SplatSorter.d.ts +14 -0
  87. package/dist/render/SplatSorter.d.ts.map +1 -0
  88. package/dist/render/SplatSorter.js +59 -0
  89. package/dist/render/SplatSorter.js.map +1 -0
  90. package/dist/render/StyleMaterial.d.ts +48 -0
  91. package/dist/render/StyleMaterial.d.ts.map +1 -1
  92. package/dist/render/StyleMaterial.js +292 -69
  93. package/dist/render/StyleMaterial.js.map +1 -1
  94. package/dist/render/TextureFactory.d.ts +10 -0
  95. package/dist/render/TextureFactory.d.ts.map +1 -1
  96. package/dist/render/TextureFactory.js +35 -5
  97. package/dist/render/TextureFactory.js.map +1 -1
  98. package/dist/render/lightingPreset.d.ts +53 -0
  99. package/dist/render/lightingPreset.d.ts.map +1 -0
  100. package/dist/render/lightingPreset.js +36 -0
  101. package/dist/render/lightingPreset.js.map +1 -0
  102. package/dist/render/materialAlpha.d.ts +51 -0
  103. package/dist/render/materialAlpha.d.ts.map +1 -0
  104. package/dist/render/materialAlpha.js +68 -0
  105. package/dist/render/materialAlpha.js.map +1 -0
  106. package/dist/tileset/S3MTileset.d.ts +50 -0
  107. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  108. package/dist/tileset/S3MTileset.js +60 -5
  109. package/dist/tileset/S3MTileset.js.map +1 -1
  110. package/dist/vendor/browser-node-stub.js +10 -0
  111. package/dist/worker/s3mWorker.js +1166 -979
  112. package/dist/worker/s3mWorker.js.map +1 -1
  113. package/docs/API.md +55 -1
  114. package/docs/FORMAT-NOTES.md +338 -0
  115. package/package.json +1 -1
  116. package/src/core/AttributeQuery.ts +26 -3
  117. package/src/core/AuthFetch.ts +82 -0
  118. package/src/core/GaussianSplat.ts +175 -0
  119. package/src/core/S3M1Parser.ts +330 -24
  120. package/src/core/S3MBParser.ts +18 -0
  121. package/src/core/S3MBlockParser.ts +56 -11
  122. package/src/core/SplatLod.ts +83 -0
  123. package/src/core/SplatProjection.ts +231 -0
  124. package/src/core/SplatSort.ts +30 -0
  125. package/src/core/TexCoordZ.ts +160 -0
  126. package/src/core/TextureDecode.ts +63 -11
  127. package/src/core/TileGeometry.ts +138 -14
  128. package/src/core/degradations.ts +74 -0
  129. package/src/core/types.ts +32 -0
  130. package/src/index.ts +12 -1
  131. package/src/render/DatasetManager.ts +19 -1
  132. package/src/render/FeatureStyleManager.ts +7 -5
  133. package/src/render/GeometryBuilder.ts +239 -6
  134. package/src/render/HostRenderer.ts +222 -0
  135. package/src/render/Picker.ts +21 -5
  136. package/src/render/S3MViewer.ts +277 -15
  137. package/src/render/SplatMaterial.ts +433 -0
  138. package/src/render/SplatSorter.ts +59 -0
  139. package/src/render/StyleMaterial.ts +339 -69
  140. package/src/render/TextureFactory.ts +39 -5
  141. package/src/render/lightingPreset.ts +76 -0
  142. package/src/render/materialAlpha.ts +75 -0
  143. package/src/tileset/S3MTileset.ts +91 -5
  144. package/src/vendor/browser-node-stub.js +10 -0
  145. package/dist/browser/assets/s3mWorker-BjPOUgjg.js +0 -1
@@ -23,8 +23,13 @@
23
23
  import * as THREE from 'three';
24
24
  import { S3MTileset, usableGeoBounds } from '../tileset/S3MTileset.js';
25
25
  import type { BuildTileFn, TileNode } from '../tileset/S3MTileset.js';
26
- import { setMaxAnisotropy, setCompressedTexturesSupported, setTextureOffThreadDecode, setMaxTextureSize as setTextureSizeCap } from './TextureFactory.js';
27
- import { getMaterialQuality, setMaterialQuality } from './StyleMaterial.js';
26
+ import { authFetch, type AuthenticateFn } from '../core/AuthFetch.js';
27
+ import { sortSplatMesh } from './SplatSorter.js';
28
+ import { splatCenterBounds } from './SplatMaterial.js';
29
+ import { setSplatViewport } from './GeometryBuilder.js';
30
+ import { setMaxAnisotropy, setAnisotropyCap, setCompressedTexturesSupported, setTextureOffThreadDecode, setMaxTextureSize as setTextureSizeCap, setSrgbTextureDecode } from './TextureFactory.js';
31
+ import { getMaterialQuality, setMaterialQuality, setColorPipeline } from './StyleMaterial.js';
32
+ import { resolveLightingPreset, type LightingPreset } from './lightingPreset.js';
28
33
  import { decodeTextureWithWorker } from '../worker/S3MWorkerClient.js';
29
34
  import {
30
35
  STYLE,
@@ -36,6 +41,7 @@ import {
36
41
  createOutlineMaterial,
37
42
  resolveEdgeModes,
38
43
  sameEdgeModes,
44
+ restoreBaseBlendState,
39
45
  } from './StyleMaterial.js';
40
46
  import type {
41
47
  OutlineMaterial,
@@ -50,6 +56,7 @@ import type {
50
56
  MaterialQuality,
51
57
  } from './StyleMaterial.js';
52
58
  import { applyPointCloudStyle, isOwnedPointCloudMaterial } from './PointCloudMaterial.js';
59
+ import type { DegradationKind, UnsupportedPolicy } from '../core/degradations.js';
53
60
  import { Picker } from './Picker.js';
54
61
  import { GpuEdgePass } from './GpuEdgePass.js';
55
62
  import { Animator } from '../anim/Animator.js';
@@ -208,8 +215,21 @@ export interface LightOptions {
208
215
  }
209
216
 
210
217
  export interface S3MViewerOptions {
218
+ /**
219
+ * 光照预设:
220
+ * - `'default'`(默认):现有 PBR 打光,亮部线性总量 ≈ 2.55;
221
+ * - `'supermap'`:对齐 iClient3D 观感(暗部 0.46 / 亮部 2.44),远处不发灰、饱和度更高。
222
+ * 只改光源,不动材质与色调映射;`light` 里的显式字段仍然优先生效。
223
+ */
224
+ lightingPreset?: LightingPreset;
211
225
  /** 光照参数配置(强度、颜色、朝向模式、太阳方位角/仰角等) */
212
226
  light?: LightOptions;
227
+ /**
228
+ * 三角面材质深度偏移(共面摩尔纹 / 重合面归属的人工定序)。
229
+ * 例:factor -1 / units -2 把配置材质的片元深度略微拉近。
230
+ * 这是 mesh 粒度开关,见 BuildTileOptions.polygonOffset 的说明。
231
+ */
232
+ polygonOffset?: { factor: number; units: number };
213
233
  /**
214
234
  * 色调映射模式(默认 THREE.NoToneMapping,避免 ACESFilmic 压暗中暗部及导致底图与建筑色彩失真;
215
235
  * 可传入 THREE.ACESFilmicToneMapping、THREE.LinearToneMapping、THREE.ReinhardToneMapping 等)
@@ -231,6 +251,11 @@ export interface S3MViewerOptions {
231
251
  maxTiles?: number;
232
252
  /** 每个数据集并发请求数 */
233
253
  maxConcurrent?: number;
254
+ /**
255
+ * 图层级默认的降级处置策略(默认 `'warn'`),数据集可用 `unsupported` 单独覆盖。
256
+ * 见 `S3MTilesetOptions.unsupported`。
257
+ */
258
+ unsupported?: UnsupportedPolicy;
234
259
  /** 是否用 Web Worker 解析经典 .s3mb 瓦片(默认开,崩溃自动降级主线程),见 S3MTilesetOptions.useWorker */
235
260
  useWorker?: boolean;
236
261
  /** LOD 切换范围缩放系数(默认 1.0),> 1 更倾向保留粗模,< 1 提前换细模 */
@@ -290,6 +315,13 @@ export interface S3MViewerOptions {
290
315
  onRequestRepaint?: () => void;
291
316
  /** 地图朝向(弧度),用于让灯光跟着视角走。不传则灯光方向固定 */
292
317
  getBearing?: () => number;
318
+ /**
319
+ * 鉴权适配函数(全局默认,数据集可用自己的 `authenticate` 覆盖)。
320
+ * 内核主线程发出的全部请求(配置 scp / 瓦片 / 子图集 / 棱线缓存 / 属性查询)
321
+ * 发出前都过一遍,可改写 URL(加 `?token=`)或附加请求头。
322
+ * 不传 = 行为与旧版完全一致。见 `AuthenticateFn`。
323
+ */
324
+ authenticate?: AuthenticateFn;
293
325
  onStats?: (stats: Stats) => void;
294
326
  onReady?: (entry: S3MDatasetEntry) => void;
295
327
  onPick?: (result: PickResult | null) => void;
@@ -387,6 +419,8 @@ const STYLE_SCALAR_KEYS = [
387
419
  'texMix',
388
420
  'roofThreshold',
389
421
  'ambientGain',
422
+ 'metalness',
423
+ 'roughness',
390
424
  'edgeWidth',
391
425
  'edgeOpacity',
392
426
  'edgeOffset',
@@ -526,6 +560,9 @@ export class S3MViewer {
526
560
  private readonly _nativeEdgeLines = new Set<THREE.LineSegments>();
527
561
  /** 点云对象集合:透明度动画时只刷它们,避免每帧遍历全部 pickables */
528
562
  private readonly _pointClouds = new Set<THREE.Points>();
563
+ /** 泼溅排序状态:相机不动且没有新瓦片时,排序结果可以直接复用 */
564
+ private readonly _lastSplatSortView = new THREE.Matrix4();
565
+ private _splatsSorted = false;
529
566
  /** 当前材质是否处于 transparent 状态,用来避免逐帧重复切换 program */
530
567
  private _opacityTransparent = false;
531
568
 
@@ -552,6 +589,8 @@ export class S3MViewer {
552
589
  private _hemiLight!: THREE.HemisphereLight;
553
590
  private _keyLight!: THREE.DirectionalLight;
554
591
  private _fillLight!: THREE.DirectionalLight;
592
+ /** 环境贴图的 PMREM 渲染目标(setEnvironment 建的,unmount 时释放) */
593
+ private _envRT: THREE.WebGLRenderTarget | null = null;
555
594
  private readonly _lightOptions: Required<Omit<LightOptions, 'mainIntensity' | 'mainColor'>> = {
556
595
  ambientIntensity: 0.4,
557
596
  ambientColor: 0xffffff,
@@ -572,7 +611,11 @@ export class S3MViewer {
572
611
  /** 宿主重画信号与地图朝向,构造时注入 */
573
612
  private _onRequestRepaint: () => void = () => {};
574
613
  private _getBearing: (() => number) | undefined;
614
+ /** 鉴权适配(全局默认),构造时注入 */
615
+ private readonly _authenticate: AuthenticateFn | undefined;
575
616
  private _lodRangeScale = 1.0;
617
+ /** 图层级降级策略,数据集可覆盖,见 `S3MViewerOptions.unsupported` */
618
+ private readonly _unsupported: UnsupportedPolicy;
576
619
  /** LOD 判定屏幕高度的 DPR 上限。Infinity = 不截断(默认,行为同旧版) */
577
620
  private _lodPixelRatioCap = Infinity;
578
621
 
@@ -589,9 +632,14 @@ export class S3MViewer {
589
632
  this.options = options;
590
633
  this.pickable = options.pickable !== false;
591
634
  this._lodRangeScale = options.lodRangeScale ?? 1.0;
635
+ this._unsupported = options.unsupported ?? 'warn';
592
636
  this._lodPixelRatioCap = options.lodPixelRatioCap ?? Infinity;
593
637
  setTextureSizeCap(options.maxTextureSize ?? Infinity);
594
638
  setMaterialQuality(options.materialQuality ?? 'pbr');
639
+ if (options.lightingPreset && options.lightingPreset !== 'default') {
640
+ Object.assign(this._lightOptions, resolveLightingPreset(options.lightingPreset));
641
+ this._applyColorPipeline(options.lightingPreset);
642
+ }
595
643
  if (options.light) {
596
644
  Object.assign(this._lightOptions, options.light);
597
645
  }
@@ -600,6 +648,7 @@ export class S3MViewer {
600
648
  }
601
649
  this._onRequestRepaint = options.onRequestRepaint ?? (() => {});
602
650
  this._getBearing = options.getBearing;
651
+ this._authenticate = options.authenticate;
603
652
  this.onStats = options.onStats ?? (() => {});
604
653
  this.onReady = options.onReady ?? (() => {});
605
654
  this.onPick = options.onPick ?? (() => {});
@@ -670,7 +719,11 @@ export class S3MViewer {
670
719
 
671
720
  this._fps.last = performance.now();
672
721
  this._datasetQueue = this._normalizeDatasets(options);
673
- if (options.attributes) this.attributeQuery = new AttributeQuery(options.attributes);
722
+ if (options.attributes)
723
+ this.attributeQuery = new AttributeQuery({
724
+ ...options.attributes,
725
+ authenticate: options.attributes.authenticate ?? this._authenticate,
726
+ });
674
727
  }
675
728
 
676
729
  /* ---------------------------------------------------------------- 生命周期 */
@@ -765,6 +818,8 @@ export class S3MViewer {
765
818
  this.groundManager.dispose();
766
819
  this.picker?.dispose();
767
820
  this.picker = null;
821
+ this._envRT?.dispose();
822
+ this._envRT = null;
768
823
  this.outlineMaterial.dispose();
769
824
  this._gpuEdge?.dispose();
770
825
  this._gpuEdge = null;
@@ -828,6 +883,34 @@ export class S3MViewer {
828
883
  }
829
884
  }
830
885
 
886
+ /**
887
+ * 运行时切换光照预设:覆盖四个强度,颜色/朝向保持不变。
888
+ * 之后再调 `setLight({...})` 仍然可以逐项微调。
889
+ */
890
+ /**
891
+ * supermap 完整版 = 光照强度 + 颜色管线一起切:
892
+ * direct 管线下纹理不做 sRGB 解码、片元按官方常数 Lambert 直乘(见 lightingPreset.ts)。
893
+ * 两个都是模块级全局态,只影响之后创建的材质 / 纹理 —— 已加载瓦片要重载才换。
894
+ */
895
+ private _applyColorPipeline(preset: LightingPreset): void {
896
+ const direct = preset === 'supermap';
897
+ setColorPipeline(direct ? 'direct' : 'linear');
898
+ setSrgbTextureDecode(!direct);
899
+ // 官方不开各向异性:远处直接采高 mip 平均成纯色("灰色滤镜"的一部分)。
900
+ // 我们默认 Infinity(倾斜摄影立面清晰度优先),supermap 模式必须收回。
901
+ setAnisotropyCap(direct ? 1 : Infinity);
902
+ }
903
+
904
+ setLightingPreset(preset: LightingPreset): void {
905
+ Object.assign(this._lightOptions, resolveLightingPreset(preset));
906
+ this._applyColorPipeline(preset);
907
+ this.options.lightingPreset = preset;
908
+ this._applyLightOptions();
909
+ this._updateLights();
910
+ this._needsRepaint = true;
911
+ this.triggerRepaint();
912
+ }
913
+
831
914
  /**
832
915
  * 动态设置光照配置
833
916
  */
@@ -894,22 +977,31 @@ export class S3MViewer {
894
977
  };
895
978
  place(this._keyLight, this._lightOptions.azimuth, this._lightOptions.altitude);
896
979
  place(this._fillLight, this._lightOptions.azimuth + 65, Math.max(10, this._lightOptions.altitude - 20));
980
+ // direct 管线的官方 Lambert 用主光方向(共享 uniform,所有材质同步生效)
981
+ if (this.uniforms?.uSunDirection && this._keyLight) {
982
+ (this.uniforms.uSunDirection.value as THREE.Vector3).copy(this._keyLight.position).normalize();
983
+ }
897
984
  }
898
985
 
899
986
  /* ---------------------------------------------------------------- 数据集 */
900
987
 
901
988
  private _normalizeDatasets(options: S3MViewerOptions): S3MDatasetOptions[] {
902
- if (Array.isArray(options.datasets)) return options.datasets.filter((d) => !!d?.configUrl);
989
+ if (Array.isArray(options.datasets))
990
+ return options.datasets
991
+ .filter((d) => !!d?.configUrl)
992
+ .map((d) => ({ ...d, polygonOffset: d.polygonOffset ?? options.polygonOffset }));
903
993
  if (options.configUrl)
904
- return [{ id: 'default', name: '默认数据集', configUrl: options.configUrl, useWorker: options.useWorker }];
994
+ return [{ id: 'default', name: '默认数据集', configUrl: options.configUrl, useWorker: options.useWorker, polygonOffset: options.polygonOffset }];
905
995
  return [];
906
996
  }
907
997
 
908
998
  /** 追加一个数据集。图层 addLayer 之前调用会抛错。 */
909
999
  async addDataset(ds: S3MDatasetOptions): Promise<S3MDatasetEntry> {
910
1000
  if (!this.scene) throw new Error('[s3m] 图层尚未 addLayer,不能添加数据集');
1001
+ // 图层级默认值:数据集自己指定了就用自己的,见 `S3MViewerOptions.unsupported`
1002
+ const merged: S3MDatasetOptions = { ...ds, unsupported: ds.unsupported ?? this._unsupported };
911
1003
  const entry = await this._datasetManager.addDataset(
912
- ds,
1004
+ merged,
913
1005
  this.scene,
914
1006
  this.geo,
915
1007
  this.uniforms,
@@ -950,7 +1042,8 @@ export class S3MViewer {
950
1042
  },
951
1043
  },
952
1044
  this.options.maxTriangles ?? Infinity,
953
- this.options.maxImageCache ?? 128
1045
+ this.options.maxImageCache ?? 128,
1046
+ this._authenticate
954
1047
  );
955
1048
  return entry;
956
1049
  }
@@ -1616,7 +1709,9 @@ export class S3MViewer {
1616
1709
  /* ---------------------------------------------------------------- 按业务字段着色 */
1617
1710
 
1618
1711
  setAttributeSource(options: AttributeQueryOptions | null): void {
1619
- this.attributeQuery = options ? new AttributeQuery(options) : null;
1712
+ this.attributeQuery = options
1713
+ ? new AttributeQuery({ ...options, authenticate: options.authenticate ?? this._authenticate })
1714
+ : null;
1620
1715
  }
1621
1716
 
1622
1717
  getLoadedSmids(): number[] {
@@ -1833,6 +1928,8 @@ export class S3MViewer {
1833
1928
  const mesh = child as THREE.Mesh;
1834
1929
  if (!mesh.isMesh && !isPoints) continue;
1835
1930
  if (isPoints) this._pointClouds.add(child as THREE.Points);
1931
+ // 新瓦片里的泼溅实例还没按当前视角排过,下一帧要重排
1932
+ if (isPoints) this._splatsSorted = false;
1836
1933
  const ud = mesh.userData as MeshUserData;
1837
1934
  ud.pickId = this._allocPickId();
1838
1935
  ud.datasetId = entry.id;
@@ -1864,9 +1961,11 @@ export class S3MViewer {
1864
1961
  if (mat) {
1865
1962
  const o = clampOpacity(this.style.opacity);
1866
1963
  // 不写 mat.opacity:全局透明度走共享 uOpacity,材质自身 opacity 保留模型自带 alpha
1867
- mat.transparent = o < 0.999;
1964
+ // 混合开关不能写成 `o < 0.999` 一刀切:材质自身可能是 blend / 顶点 alpha
1965
+ // 透明(s3mOpts.transparent,如 SMID 4421),注册时就被这句踩掉,
1966
+ // 表现为「刚进去没透明度,切换材质质感重建后才恢复」。
1868
1967
  const keepDepth = this.style.depthWrite !== false;
1869
- mat.depthWrite = mat.transparent ? keepDepth : (this.style.depthWrite !== false);
1968
+ restoreBaseBlendState(mat, o < 0.999, keepDepth);
1870
1969
  // shading 走共享 uniform(uFlatShading),不写 mat.flatShading / needsUpdate——
1871
1970
  // transparent / depthWrite 也不是 program 决定项,整套样式继承零重编译
1872
1971
  if (mat.userData.uFlatShading) {
@@ -1922,9 +2021,10 @@ export class S3MViewer {
1922
2021
  (mesh.userData as MeshUserData).tileFadeKey = undefined;
1923
2022
  this.animator.unregister(key);
1924
2023
  // 淡入期间强制开了 transparent + depthWrite,结束时不能自己拍板关掉——
1925
- // 图层可能本来就半透明、单体也可能还有淡化。交给淡化流程重新裁决一次。
2024
+ // 先恢复「材质自身语义 || 图层透明度」的基础状态(材质可能是 blend /
2025
+ // 顶点 alpha 透明,如 SMID 4421),再让单体淡化流程有需要时重新打开。
2026
+ restoreBaseBlendState(mat, this._opacityTransparent, this.style.depthWrite !== false);
1926
2027
  this._featureStyleManager.applyOverridesToMesh(mesh);
1927
- mat.depthWrite = this.style.depthWrite !== false;
1928
2028
  this._requestRepaint();
1929
2029
  },
1930
2030
  });
@@ -2128,6 +2228,52 @@ export class S3MViewer {
2128
2228
  return { ...this._styleTarget, modeLabel: STYLE_LABELS[this._styleTarget.mode] };
2129
2229
  }
2130
2230
 
2231
+ /**
2232
+ * 挂 / 摘环境贴图。
2233
+ *
2234
+ * 金属度(`setStyle({ metalness })`)**必须**有环境图才看得出质感:
2235
+ * PBR 的金属几乎没有漫反射,画面全靠 `scene.environment` 里的环境反射,
2236
+ * 没有它时 metalness 一高就整片发黑,只剩两盏平行灯打成的小高光点。
2237
+ *
2238
+ * @param source 等距柱状图(.hdr / .jpg / .png,2:1)地址,或已加载的 `THREE.Texture`;
2239
+ * `null` = 关掉环境图
2240
+ * @param intensity 环境光强度(`scene.environmentIntensity`,默认 1)
2241
+ *
2242
+ * 传入 URL 时纹理由内部加载并转成 PMREM(预滤波 mipmap);传 Texture 时不会接管它的生命周期。
2243
+ */
2244
+ async setEnvironment(
2245
+ source: string | THREE.Texture | null,
2246
+ intensity = 1
2247
+ ): Promise<void> {
2248
+ if (!this.renderer || !this.scene) return;
2249
+ this.scene.environmentIntensity = Math.max(0, intensity);
2250
+
2251
+ if (source == null) {
2252
+ this.scene.environment = null;
2253
+ this._envRT?.dispose();
2254
+ this._envRT = null;
2255
+ this._requestRepaint();
2256
+ return;
2257
+ }
2258
+
2259
+ const fromUrl = typeof source === 'string';
2260
+ const tex = fromUrl ? await new THREE.TextureLoader().loadAsync(source) : source;
2261
+ // PMREM 只认等距柱状图;普通贴图也按 2:1 全景处理
2262
+ tex.mapping = THREE.EquirectangularReflectionMapping;
2263
+
2264
+ const pmrem = new THREE.PMREMGenerator(this.renderer);
2265
+ pmrem.compileEquirectangularShader();
2266
+ const rt = pmrem.fromEquirectangular(tex);
2267
+ pmrem.dispose();
2268
+ if (fromUrl) tex.dispose();
2269
+
2270
+ this.scene.environment = rt.texture;
2271
+ // 上一张(同一个 viewer 反复换)要释放,否则显存里会一直堆
2272
+ this._envRT?.dispose();
2273
+ this._envRT = rt;
2274
+ this._requestRepaint();
2275
+ }
2276
+
2131
2277
  /** 把某个样式键的取值转成动画通道能吃的形状(标量 or 线性 RGB 三元组) */
2132
2278
  private _animTargetOf(key: string, patch: Partial<S3MStyle>): number | number[] {
2133
2279
  const value = (patch as Record<string, unknown>)[key];
@@ -2309,6 +2455,9 @@ export class S3MViewer {
2309
2455
  if (u.uAmbientGain && s.ambientGain != null) {
2310
2456
  u.uAmbientGain.value = s.ambientGain;
2311
2457
  }
2458
+ // 金属度 / 粗糙度:负值 = 不覆盖,保持材质(数据)自身的值
2459
+ if (u.uMetalness) u.uMetalness.value = s.metalness ?? -1;
2460
+ if (u.uRoughness) u.uRoughness.value = s.roughness ?? -1;
2312
2461
  if (u.uOpacity && s.opacity != null) {
2313
2462
  u.uOpacity.value = clampOpacity(s.opacity);
2314
2463
  }
@@ -2350,8 +2499,9 @@ export class S3MViewer {
2350
2499
  // 有单体淡化 / 正在淡入的材质,transparent 由各自的流程管,这里不要抢
2351
2500
  if (m.userData.uBatchTexActive?.value) continue;
2352
2501
  if ((m.userData.uTileFade?.value ?? 1) < 1) continue;
2353
- m.transparent = transparent;
2354
- m.depthWrite = transparent ? keepDepth : true;
2502
+ // 材质自身可能是 blend / 顶点 alpha 透明(s3mOpts.transparent),
2503
+ // 关全局混合时不能把它一刀切成不透明
2504
+ restoreBaseBlendState(m, transparent, keepDepth);
2355
2505
  }
2356
2506
  }
2357
2507
 
@@ -2683,7 +2833,7 @@ export class S3MViewer {
2683
2833
  * @param url 棱线缓存地址(二进制 Float32 序列:x,y,z,x,y,z,… 成对成段)
2684
2834
  */
2685
2835
  async loadCreases(url: string): Promise<void> {
2686
- const resp = await fetch(url);
2836
+ const resp = await authFetch(url, this._authenticate, 'creases');
2687
2837
  if (!resp.ok) throw new Error(`[s3m] 棱线缓存加载失败: ${resp.status} ${url}`);
2688
2838
  const buf = await resp.arrayBuffer();
2689
2839
  const arr = new Float32Array(buf);
@@ -3041,6 +3191,9 @@ export class S3MViewer {
3041
3191
  ray?: THREE.Ray
3042
3192
  ): PickResult | null {
3043
3193
  const ud = ref.mesh.userData as MeshUserData;
3194
+ // 泼溅走独立分支:它既没有 instanceMatrix,也没有 geoRef.pickInfo 里的单体表,
3195
+ // 走下面那段会在 `_instanceFeature()` 拿到 null 直接返回 —— 表现是「点得中但拿不到结果」。
3196
+ if (ud.isGaussianSplat) return this._buildSplatPickResult(hit, ref, exactPoint, ray);
3044
3197
  let smid: number | null = null;
3045
3198
  let batchId = hit.batchId;
3046
3199
  let instanceIndex = hit.instanceIndex;
@@ -3188,6 +3341,85 @@ export class S3MViewer {
3188
3341
  };
3189
3342
  }
3190
3343
 
3344
+ /**
3345
+ * 泼溅的拾取结果。
3346
+ *
3347
+ * 整块泼溅瓦片当一个单体(没有单体级 smid 可报),命中点由 `splatRaycast` 给出 ——
3348
+ * 它按每个高斯的 3σ 球求交,比拾取那一趟为了命中率摊开的 4px 方块准得多。
3349
+ * 包围盒必须由 `iCenter` 算:geometry 的 position 只是 [-1,1]² 的四边形角点。
3350
+ */
3351
+ private _buildSplatPickResult(
3352
+ hit: { objectId: number; batchId: number; instanceIndex?: number },
3353
+ ref: PickableRef,
3354
+ exactPoint?: THREE.Vector3,
3355
+ ray?: THREE.Ray
3356
+ ): PickResult | null {
3357
+ const anchor = this.sceneAnchor;
3358
+ if (!anchor) return null;
3359
+ const mesh = ref.mesh;
3360
+
3361
+ let hitPoint = exactPoint ?? null;
3362
+ if (!hitPoint && ray) hitPoint = this._intersectMeshAtRay(mesh, ray);
3363
+
3364
+ const local = splatCenterBounds(mesh.geometry);
3365
+ const box: SegmentBox | null = local
3366
+ ? {
3367
+ minX: local.minX,
3368
+ minY: local.minY,
3369
+ minZ: local.minZ,
3370
+ maxX: local.maxX,
3371
+ maxY: local.maxY,
3372
+ maxZ: local.maxZ,
3373
+ count: local.count,
3374
+ triangles: 0,
3375
+ }
3376
+ : null;
3377
+
3378
+ const attributes = buildAttributes({
3379
+ smid: null,
3380
+ box,
3381
+ triangles: 0,
3382
+ floorHeight: this.style.floorHeight,
3383
+ });
3384
+
3385
+ const s = anchor.meterInMercator;
3386
+ let point: [number, number, number] | null = null;
3387
+ let lngLat: [number, number] | null = null;
3388
+ let altitude: number | null = null;
3389
+ let centerLngLat: [number, number] | null = null;
3390
+
3391
+ if (hitPoint) {
3392
+ point = [hitPoint.x, hitPoint.y, hitPoint.z];
3393
+ lngLat = this._lngLatFromWorld(hitPoint.x * s + anchor.x, -hitPoint.y * s + anchor.y);
3394
+ altitude = hitPoint.z + (anchor.altitude ?? 0);
3395
+ // 泼溅命中的就是一个高斯,没有「单体中心」可言,用命中点当中心
3396
+ centerLngLat = lngLat;
3397
+ } else if (box) {
3398
+ const v = new THREE.Vector3(
3399
+ (box.minX + box.maxX) / 2,
3400
+ (box.minY + box.maxY) / 2,
3401
+ (box.minZ + box.maxZ) / 2
3402
+ ).applyMatrix4(mesh.matrixWorld);
3403
+ centerLngLat = this._lngLatFromWorld(v.x * s + anchor.x, -v.y * s + anchor.y);
3404
+ altitude = v.z + (anchor.altitude ?? 0);
3405
+ }
3406
+
3407
+ return {
3408
+ datasetId: ref.datasetId,
3409
+ datasetName: this.datasets.get(ref.datasetId)?.name ?? ref.datasetId,
3410
+ objectId: hit.objectId,
3411
+ batchId: -1,
3412
+ instanceIndex: undefined,
3413
+ skeletonName: ref.skeletonName,
3414
+ centerLngLat,
3415
+ point,
3416
+ lngLat,
3417
+ altitude,
3418
+ ...attributes,
3419
+ mesh,
3420
+ };
3421
+ }
3422
+
3191
3423
  /** 按 canvas 左上角的 CSS 像素坐标拾取。 */
3192
3424
  pick(x: number, y: number): PickResult | null {
3193
3425
  if (!this.picker || !this.scene || !this.camera || !this.sceneAnchor) return null;
@@ -3888,6 +4120,23 @@ export class S3MViewer {
3888
4120
  this._requestRepaint();
3889
4121
  }
3890
4122
 
4123
+ /**
4124
+ * 泼溅实例按视深 back-to-front 重排:alpha 混合是顺序相关的,
4125
+ * 乱序混合就是满屏彩色噪点。相机没动且没有新瓦片时整个跳过;
4126
+ * 单个瓦片顺序本来就对时 sortSplatMesh 内部也会跳过重写。
4127
+ */
4128
+ private _sortSplats(): void {
4129
+ const cam = this.camera;
4130
+ if (!cam) return;
4131
+ if (this._splatsSorted && this._lastSplatSortView.equals(cam.matrixWorldInverse)) return;
4132
+ for (const pc of this._pointClouds) {
4133
+ if (!(pc.userData as { isGaussianSplat?: boolean }).isGaussianSplat) continue;
4134
+ sortSplatMesh(pc as unknown as THREE.Mesh, cam.matrixWorldInverse);
4135
+ }
4136
+ this._lastSplatSortView.copy(cam.matrixWorldInverse);
4137
+ this._splatsSorted = true;
4138
+ }
4139
+
3891
4140
  /**
3892
4141
  * 渲染一帧。所有宿主相关量都由调用方喂进来,viewer 不碰任何地图 API:
3893
4142
  *
@@ -3979,12 +4228,15 @@ export class S3MViewer {
3979
4228
 
3980
4229
  // 等宽棱线按设备像素撑开,分辨率每帧同步一次
3981
4230
  this._updateLineResolution(canvas);
4231
+ // 泼溅的最小像素半径 / 抗锯齿低通同样要把 ndc 换算成像素
4232
+ setSplatViewport(canvas.width, canvas.height);
3982
4233
 
3983
4234
  this.renderer.setViewport(0, 0, canvas.width, canvas.height);
3984
4235
  this.renderer.resetState();
3985
4236
  // 完全透明时什么都不画:透明物体照样写深度,画了会在底图上挖出一个洞。
3986
4237
  // 这里跳过的是「本层的绘制」,宿主自己的清屏 / 底图照旧。
3987
4238
  if ((this.uniforms.uOpacity?.value ?? 1) > OPACITY_EPS) {
4239
+ this._sortSplats();
3988
4240
  this.renderer.render(this.scene, this.camera);
3989
4241
  // GPU 描边必须画在主场景之后:它的全屏 quad 参与宿主深度测试,
3990
4242
  // 依赖本帧已经写好的深度(被地形挡住的地方不该描边)
@@ -4035,7 +4287,9 @@ export class S3MViewer {
4035
4287
  programs: 0,
4036
4288
  textures: 0,
4037
4289
  geometries: 0,
4290
+ degradations: [],
4038
4291
  };
4292
+ let degradations: DegradationKind[] | null = null;
4039
4293
  for (const entry of this.datasets.values()) {
4040
4294
  const s = entry.tileset?.stats;
4041
4295
  if (!s) continue;
@@ -4045,7 +4299,15 @@ export class S3MViewer {
4045
4299
  total.meshes += s.meshes;
4046
4300
  total.vertices += s.vertices;
4047
4301
  total.triangles += s.triangles;
4302
+ // 降级项跨数据集去重:同一类只报一次给 UI
4303
+ if (s.degradations?.length) {
4304
+ degradations ??= [];
4305
+ for (const d of s.degradations) {
4306
+ if (!degradations.includes(d)) degradations.push(d);
4307
+ }
4308
+ }
4048
4309
  }
4310
+ if (degradations) total.degradations = degradations;
4049
4311
  // GPU 侧预算观测:draw call / program / 显存驻留。没有渲染器(未 mount /
4050
4312
  // 纯解析场景)时保持 0,不抛错。
4051
4313
  const info = this.renderer?.info;