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
@@ -22,10 +22,15 @@
22
22
  */
23
23
  import * as THREE from 'three';
24
24
  import { usableGeoBounds } from '../tileset/S3MTileset.js';
25
- import { setMaxAnisotropy, setCompressedTexturesSupported, setTextureOffThreadDecode, setMaxTextureSize as setTextureSizeCap } from './TextureFactory.js';
26
- import { getMaterialQuality, setMaterialQuality } from './StyleMaterial.js';
25
+ import { authFetch } from '../core/AuthFetch.js';
26
+ import { sortSplatMesh } from './SplatSorter.js';
27
+ import { splatCenterBounds } from './SplatMaterial.js';
28
+ import { setSplatViewport } from './GeometryBuilder.js';
29
+ import { setMaxAnisotropy, setAnisotropyCap, setCompressedTexturesSupported, setTextureOffThreadDecode, setMaxTextureSize as setTextureSizeCap, setSrgbTextureDecode } from './TextureFactory.js';
30
+ import { getMaterialQuality, setMaterialQuality, setColorPipeline } from './StyleMaterial.js';
31
+ import { resolveLightingPreset } from './lightingPreset.js';
27
32
  import { decodeTextureWithWorker } from '../worker/S3MWorkerClient.js';
28
- import { STYLE, STYLE_LABELS, SELECT_MODE, DEFAULT_STYLE, createSharedUniforms, createStyledMaterial, createOutlineMaterial, resolveEdgeModes, sameEdgeModes, } from './StyleMaterial.js';
33
+ import { STYLE, STYLE_LABELS, SELECT_MODE, DEFAULT_STYLE, createSharedUniforms, createStyledMaterial, createOutlineMaterial, resolveEdgeModes, sameEdgeModes, restoreBaseBlendState, } from './StyleMaterial.js';
29
34
  import { applyPointCloudStyle, isOwnedPointCloudMaterial } from './PointCloudMaterial.js';
30
35
  import { Picker } from './Picker.js';
31
36
  import { GpuEdgePass } from './GpuEdgePass.js';
@@ -81,6 +86,8 @@ const STYLE_SCALAR_KEYS = [
81
86
  'texMix',
82
87
  'roofThreshold',
83
88
  'ambientGain',
89
+ 'metalness',
90
+ 'roughness',
84
91
  'edgeWidth',
85
92
  'edgeOpacity',
86
93
  'edgeOffset',
@@ -190,6 +197,9 @@ export class S3MViewer {
190
197
  _nativeEdgeLines = new Set();
191
198
  /** 点云对象集合:透明度动画时只刷它们,避免每帧遍历全部 pickables */
192
199
  _pointClouds = new Set();
200
+ /** 泼溅排序状态:相机不动且没有新瓦片时,排序结果可以直接复用 */
201
+ _lastSplatSortView = new THREE.Matrix4();
202
+ _splatsSorted = false;
193
203
  /** 当前材质是否处于 transparent 状态,用来避免逐帧重复切换 program */
194
204
  _opacityTransparent = false;
195
205
  /**
@@ -213,6 +223,8 @@ export class S3MViewer {
213
223
  _hemiLight;
214
224
  _keyLight;
215
225
  _fillLight;
226
+ /** 环境贴图的 PMREM 渲染目标(setEnvironment 建的,unmount 时释放) */
227
+ _envRT = null;
216
228
  _lightOptions = {
217
229
  ambientIntensity: 0.4,
218
230
  ambientColor: 0xffffff,
@@ -231,7 +243,11 @@ export class S3MViewer {
231
243
  /** 宿主重画信号与地图朝向,构造时注入 */
232
244
  _onRequestRepaint = () => { };
233
245
  _getBearing;
246
+ /** 鉴权适配(全局默认),构造时注入 */
247
+ _authenticate;
234
248
  _lodRangeScale = 1.0;
249
+ /** 图层级降级策略,数据集可覆盖,见 `S3MViewerOptions.unsupported` */
250
+ _unsupported;
235
251
  /** LOD 判定屏幕高度的 DPR 上限。Infinity = 不截断(默认,行为同旧版) */
236
252
  _lodPixelRatioCap = Infinity;
237
253
  /** 宿主请求重画(MapLibre 传 triggerRepaint,纯 three 宿主传自己的渲染循环) */
@@ -245,9 +261,14 @@ export class S3MViewer {
245
261
  this.options = options;
246
262
  this.pickable = options.pickable !== false;
247
263
  this._lodRangeScale = options.lodRangeScale ?? 1.0;
264
+ this._unsupported = options.unsupported ?? 'warn';
248
265
  this._lodPixelRatioCap = options.lodPixelRatioCap ?? Infinity;
249
266
  setTextureSizeCap(options.maxTextureSize ?? Infinity);
250
267
  setMaterialQuality(options.materialQuality ?? 'pbr');
268
+ if (options.lightingPreset && options.lightingPreset !== 'default') {
269
+ Object.assign(this._lightOptions, resolveLightingPreset(options.lightingPreset));
270
+ this._applyColorPipeline(options.lightingPreset);
271
+ }
251
272
  if (options.light) {
252
273
  Object.assign(this._lightOptions, options.light);
253
274
  }
@@ -256,6 +277,7 @@ export class S3MViewer {
256
277
  }
257
278
  this._onRequestRepaint = options.onRequestRepaint ?? (() => { });
258
279
  this._getBearing = options.getBearing;
280
+ this._authenticate = options.authenticate;
259
281
  this.onStats = options.onStats ?? (() => { });
260
282
  this.onReady = options.onReady ?? (() => { });
261
283
  this.onPick = options.onPick ?? (() => { });
@@ -317,7 +339,10 @@ export class S3MViewer {
317
339
  this._fps.last = performance.now();
318
340
  this._datasetQueue = this._normalizeDatasets(options);
319
341
  if (options.attributes)
320
- this.attributeQuery = new AttributeQuery(options.attributes);
342
+ this.attributeQuery = new AttributeQuery({
343
+ ...options.attributes,
344
+ authenticate: options.attributes.authenticate ?? this._authenticate,
345
+ });
321
346
  }
322
347
  /* ---------------------------------------------------------------- 生命周期 */
323
348
  /**
@@ -404,6 +429,8 @@ export class S3MViewer {
404
429
  this.groundManager.dispose();
405
430
  this.picker?.dispose();
406
431
  this.picker = null;
432
+ this._envRT?.dispose();
433
+ this._envRT = null;
407
434
  this.outlineMaterial.dispose();
408
435
  this._gpuEdge?.dispose();
409
436
  this._gpuEdge = null;
@@ -461,6 +488,32 @@ export class S3MViewer {
461
488
  this._fillLight.color.set(this._lightOptions.fillLightColor);
462
489
  }
463
490
  }
491
+ /**
492
+ * 运行时切换光照预设:覆盖四个强度,颜色/朝向保持不变。
493
+ * 之后再调 `setLight({...})` 仍然可以逐项微调。
494
+ */
495
+ /**
496
+ * supermap 完整版 = 光照强度 + 颜色管线一起切:
497
+ * direct 管线下纹理不做 sRGB 解码、片元按官方常数 Lambert 直乘(见 lightingPreset.ts)。
498
+ * 两个都是模块级全局态,只影响之后创建的材质 / 纹理 —— 已加载瓦片要重载才换。
499
+ */
500
+ _applyColorPipeline(preset) {
501
+ const direct = preset === 'supermap';
502
+ setColorPipeline(direct ? 'direct' : 'linear');
503
+ setSrgbTextureDecode(!direct);
504
+ // 官方不开各向异性:远处直接采高 mip 平均成纯色("灰色滤镜"的一部分)。
505
+ // 我们默认 Infinity(倾斜摄影立面清晰度优先),supermap 模式必须收回。
506
+ setAnisotropyCap(direct ? 1 : Infinity);
507
+ }
508
+ setLightingPreset(preset) {
509
+ Object.assign(this._lightOptions, resolveLightingPreset(preset));
510
+ this._applyColorPipeline(preset);
511
+ this.options.lightingPreset = preset;
512
+ this._applyLightOptions();
513
+ this._updateLights();
514
+ this._needsRepaint = true;
515
+ this.triggerRepaint();
516
+ }
464
517
  /**
465
518
  * 动态设置光照配置
466
519
  */
@@ -525,20 +578,28 @@ export class S3MViewer {
525
578
  };
526
579
  place(this._keyLight, this._lightOptions.azimuth, this._lightOptions.altitude);
527
580
  place(this._fillLight, this._lightOptions.azimuth + 65, Math.max(10, this._lightOptions.altitude - 20));
581
+ // direct 管线的官方 Lambert 用主光方向(共享 uniform,所有材质同步生效)
582
+ if (this.uniforms?.uSunDirection && this._keyLight) {
583
+ this.uniforms.uSunDirection.value.copy(this._keyLight.position).normalize();
584
+ }
528
585
  }
529
586
  /* ---------------------------------------------------------------- 数据集 */
530
587
  _normalizeDatasets(options) {
531
588
  if (Array.isArray(options.datasets))
532
- return options.datasets.filter((d) => !!d?.configUrl);
589
+ return options.datasets
590
+ .filter((d) => !!d?.configUrl)
591
+ .map((d) => ({ ...d, polygonOffset: d.polygonOffset ?? options.polygonOffset }));
533
592
  if (options.configUrl)
534
- return [{ id: 'default', name: '默认数据集', configUrl: options.configUrl, useWorker: options.useWorker }];
593
+ return [{ id: 'default', name: '默认数据集', configUrl: options.configUrl, useWorker: options.useWorker, polygonOffset: options.polygonOffset }];
535
594
  return [];
536
595
  }
537
596
  /** 追加一个数据集。图层 addLayer 之前调用会抛错。 */
538
597
  async addDataset(ds) {
539
598
  if (!this.scene)
540
599
  throw new Error('[s3m] 图层尚未 addLayer,不能添加数据集');
541
- const entry = await this._datasetManager.addDataset(ds, this.scene, this.geo, this.uniforms, this._lodRangeScale, this.options.buildTile, this.options.maxTiles ?? 64, this.options.maxConcurrent ?? 4, {
600
+ // 图层级默认值:数据集自己指定了就用自己的,见 `S3MViewerOptions.unsupported`
601
+ const merged = { ...ds, unsupported: ds.unsupported ?? this._unsupported };
602
+ const entry = await this._datasetManager.addDataset(merged, this.scene, this.geo, this.uniforms, this._lodRangeScale, this.options.buildTile, this.options.maxTiles ?? 64, this.options.maxConcurrent ?? 4, {
542
603
  onTile: (node, e) => {
543
604
  this._registerTile(node, e);
544
605
  this.onStats(this.stats);
@@ -569,7 +630,7 @@ export class S3MViewer {
569
630
  triggerRepaint: () => {
570
631
  this._requestRepaint();
571
632
  },
572
- }, this.options.maxTriangles ?? Infinity, this.options.maxImageCache ?? 128);
633
+ }, this.options.maxTriangles ?? Infinity, this.options.maxImageCache ?? 128, this._authenticate);
573
634
  return entry;
574
635
  }
575
636
  /**
@@ -1147,7 +1208,9 @@ export class S3MViewer {
1147
1208
  }
1148
1209
  /* ---------------------------------------------------------------- 按业务字段着色 */
1149
1210
  setAttributeSource(options) {
1150
- this.attributeQuery = options ? new AttributeQuery(options) : null;
1211
+ this.attributeQuery = options
1212
+ ? new AttributeQuery({ ...options, authenticate: options.authenticate ?? this._authenticate })
1213
+ : null;
1151
1214
  }
1152
1215
  getLoadedSmids() {
1153
1216
  return [...this._smidMeshes.keys()];
@@ -1345,6 +1408,9 @@ export class S3MViewer {
1345
1408
  continue;
1346
1409
  if (isPoints)
1347
1410
  this._pointClouds.add(child);
1411
+ // 新瓦片里的泼溅实例还没按当前视角排过,下一帧要重排
1412
+ if (isPoints)
1413
+ this._splatsSorted = false;
1348
1414
  const ud = mesh.userData;
1349
1415
  ud.pickId = this._allocPickId();
1350
1416
  ud.datasetId = entry.id;
@@ -1378,9 +1444,11 @@ export class S3MViewer {
1378
1444
  if (mat) {
1379
1445
  const o = clampOpacity(this.style.opacity);
1380
1446
  // 不写 mat.opacity:全局透明度走共享 uOpacity,材质自身 opacity 保留模型自带 alpha
1381
- mat.transparent = o < 0.999;
1447
+ // 混合开关不能写成 `o < 0.999` 一刀切:材质自身可能是 blend / 顶点 alpha
1448
+ // 透明(s3mOpts.transparent,如 SMID 4421),注册时就被这句踩掉,
1449
+ // 表现为「刚进去没透明度,切换材质质感重建后才恢复」。
1382
1450
  const keepDepth = this.style.depthWrite !== false;
1383
- mat.depthWrite = mat.transparent ? keepDepth : (this.style.depthWrite !== false);
1451
+ restoreBaseBlendState(mat, o < 0.999, keepDepth);
1384
1452
  // shading 走共享 uniform(uFlatShading),不写 mat.flatShading / needsUpdate——
1385
1453
  // transparent / depthWrite 也不是 program 决定项,整套样式继承零重编译
1386
1454
  if (mat.userData.uFlatShading) {
@@ -1434,9 +1502,10 @@ export class S3MViewer {
1434
1502
  mesh.userData.tileFadeKey = undefined;
1435
1503
  this.animator.unregister(key);
1436
1504
  // 淡入期间强制开了 transparent + depthWrite,结束时不能自己拍板关掉——
1437
- // 图层可能本来就半透明、单体也可能还有淡化。交给淡化流程重新裁决一次。
1505
+ // 先恢复「材质自身语义 || 图层透明度」的基础状态(材质可能是 blend /
1506
+ // 顶点 alpha 透明,如 SMID 4421),再让单体淡化流程有需要时重新打开。
1507
+ restoreBaseBlendState(mat, this._opacityTransparent, this.style.depthWrite !== false);
1438
1508
  this._featureStyleManager.applyOverridesToMesh(mesh);
1439
- mat.depthWrite = this.style.depthWrite !== false;
1440
1509
  this._requestRepaint();
1441
1510
  },
1442
1511
  });
@@ -1637,6 +1706,46 @@ export class S3MViewer {
1637
1706
  getStyle() {
1638
1707
  return { ...this._styleTarget, modeLabel: STYLE_LABELS[this._styleTarget.mode] };
1639
1708
  }
1709
+ /**
1710
+ * 挂 / 摘环境贴图。
1711
+ *
1712
+ * 金属度(`setStyle({ metalness })`)**必须**有环境图才看得出质感:
1713
+ * PBR 的金属几乎没有漫反射,画面全靠 `scene.environment` 里的环境反射,
1714
+ * 没有它时 metalness 一高就整片发黑,只剩两盏平行灯打成的小高光点。
1715
+ *
1716
+ * @param source 等距柱状图(.hdr / .jpg / .png,2:1)地址,或已加载的 `THREE.Texture`;
1717
+ * `null` = 关掉环境图
1718
+ * @param intensity 环境光强度(`scene.environmentIntensity`,默认 1)
1719
+ *
1720
+ * 传入 URL 时纹理由内部加载并转成 PMREM(预滤波 mipmap);传 Texture 时不会接管它的生命周期。
1721
+ */
1722
+ async setEnvironment(source, intensity = 1) {
1723
+ if (!this.renderer || !this.scene)
1724
+ return;
1725
+ this.scene.environmentIntensity = Math.max(0, intensity);
1726
+ if (source == null) {
1727
+ this.scene.environment = null;
1728
+ this._envRT?.dispose();
1729
+ this._envRT = null;
1730
+ this._requestRepaint();
1731
+ return;
1732
+ }
1733
+ const fromUrl = typeof source === 'string';
1734
+ const tex = fromUrl ? await new THREE.TextureLoader().loadAsync(source) : source;
1735
+ // PMREM 只认等距柱状图;普通贴图也按 2:1 全景处理
1736
+ tex.mapping = THREE.EquirectangularReflectionMapping;
1737
+ const pmrem = new THREE.PMREMGenerator(this.renderer);
1738
+ pmrem.compileEquirectangularShader();
1739
+ const rt = pmrem.fromEquirectangular(tex);
1740
+ pmrem.dispose();
1741
+ if (fromUrl)
1742
+ tex.dispose();
1743
+ this.scene.environment = rt.texture;
1744
+ // 上一张(同一个 viewer 反复换)要释放,否则显存里会一直堆
1745
+ this._envRT?.dispose();
1746
+ this._envRT = rt;
1747
+ this._requestRepaint();
1748
+ }
1640
1749
  /** 把某个样式键的取值转成动画通道能吃的形状(标量 or 线性 RGB 三元组) */
1641
1750
  _animTargetOf(key, patch) {
1642
1751
  const value = patch[key];
@@ -1816,6 +1925,11 @@ export class S3MViewer {
1816
1925
  if (u.uAmbientGain && s.ambientGain != null) {
1817
1926
  u.uAmbientGain.value = s.ambientGain;
1818
1927
  }
1928
+ // 金属度 / 粗糙度:负值 = 不覆盖,保持材质(数据)自身的值
1929
+ if (u.uMetalness)
1930
+ u.uMetalness.value = s.metalness ?? -1;
1931
+ if (u.uRoughness)
1932
+ u.uRoughness.value = s.roughness ?? -1;
1819
1933
  if (u.uOpacity && s.opacity != null) {
1820
1934
  u.uOpacity.value = clampOpacity(s.opacity);
1821
1935
  }
@@ -1862,8 +1976,9 @@ export class S3MViewer {
1862
1976
  continue;
1863
1977
  if ((m.userData.uTileFade?.value ?? 1) < 1)
1864
1978
  continue;
1865
- m.transparent = transparent;
1866
- m.depthWrite = transparent ? keepDepth : true;
1979
+ // 材质自身可能是 blend / 顶点 alpha 透明(s3mOpts.transparent),
1980
+ // 关全局混合时不能把它一刀切成不透明
1981
+ restoreBaseBlendState(m, transparent, keepDepth);
1867
1982
  }
1868
1983
  }
1869
1984
  // 点云不走 StyleMaterial,自己有一套 uOpacity,逐帧刷的只有这几个对象
@@ -2202,7 +2317,7 @@ export class S3MViewer {
2202
2317
  * @param url 棱线缓存地址(二进制 Float32 序列:x,y,z,x,y,z,… 成对成段)
2203
2318
  */
2204
2319
  async loadCreases(url) {
2205
- const resp = await fetch(url);
2320
+ const resp = await authFetch(url, this._authenticate, 'creases');
2206
2321
  if (!resp.ok)
2207
2322
  throw new Error(`[s3m] 棱线缓存加载失败: ${resp.status} ${url}`);
2208
2323
  const buf = await resp.arrayBuffer();
@@ -2541,6 +2656,10 @@ export class S3MViewer {
2541
2656
  }
2542
2657
  _buildPickResult(hit, ref, exactPoint, ray) {
2543
2658
  const ud = ref.mesh.userData;
2659
+ // 泼溅走独立分支:它既没有 instanceMatrix,也没有 geoRef.pickInfo 里的单体表,
2660
+ // 走下面那段会在 `_instanceFeature()` 拿到 null 直接返回 —— 表现是「点得中但拿不到结果」。
2661
+ if (ud.isGaussianSplat)
2662
+ return this._buildSplatPickResult(hit, ref, exactPoint, ray);
2544
2663
  let smid = null;
2545
2664
  let batchId = hit.batchId;
2546
2665
  let instanceIndex = hit.instanceIndex;
@@ -2684,6 +2803,72 @@ export class S3MViewer {
2684
2803
  mesh: ref.mesh,
2685
2804
  };
2686
2805
  }
2806
+ /**
2807
+ * 泼溅的拾取结果。
2808
+ *
2809
+ * 整块泼溅瓦片当一个单体(没有单体级 smid 可报),命中点由 `splatRaycast` 给出 ——
2810
+ * 它按每个高斯的 3σ 球求交,比拾取那一趟为了命中率摊开的 4px 方块准得多。
2811
+ * 包围盒必须由 `iCenter` 算:geometry 的 position 只是 [-1,1]² 的四边形角点。
2812
+ */
2813
+ _buildSplatPickResult(hit, ref, exactPoint, ray) {
2814
+ const anchor = this.sceneAnchor;
2815
+ if (!anchor)
2816
+ return null;
2817
+ const mesh = ref.mesh;
2818
+ let hitPoint = exactPoint ?? null;
2819
+ if (!hitPoint && ray)
2820
+ hitPoint = this._intersectMeshAtRay(mesh, ray);
2821
+ const local = splatCenterBounds(mesh.geometry);
2822
+ const box = local
2823
+ ? {
2824
+ minX: local.minX,
2825
+ minY: local.minY,
2826
+ minZ: local.minZ,
2827
+ maxX: local.maxX,
2828
+ maxY: local.maxY,
2829
+ maxZ: local.maxZ,
2830
+ count: local.count,
2831
+ triangles: 0,
2832
+ }
2833
+ : null;
2834
+ const attributes = buildAttributes({
2835
+ smid: null,
2836
+ box,
2837
+ triangles: 0,
2838
+ floorHeight: this.style.floorHeight,
2839
+ });
2840
+ const s = anchor.meterInMercator;
2841
+ let point = null;
2842
+ let lngLat = null;
2843
+ let altitude = null;
2844
+ let centerLngLat = null;
2845
+ if (hitPoint) {
2846
+ point = [hitPoint.x, hitPoint.y, hitPoint.z];
2847
+ lngLat = this._lngLatFromWorld(hitPoint.x * s + anchor.x, -hitPoint.y * s + anchor.y);
2848
+ altitude = hitPoint.z + (anchor.altitude ?? 0);
2849
+ // 泼溅命中的就是一个高斯,没有「单体中心」可言,用命中点当中心
2850
+ centerLngLat = lngLat;
2851
+ }
2852
+ else if (box) {
2853
+ const v = new THREE.Vector3((box.minX + box.maxX) / 2, (box.minY + box.maxY) / 2, (box.minZ + box.maxZ) / 2).applyMatrix4(mesh.matrixWorld);
2854
+ centerLngLat = this._lngLatFromWorld(v.x * s + anchor.x, -v.y * s + anchor.y);
2855
+ altitude = v.z + (anchor.altitude ?? 0);
2856
+ }
2857
+ return {
2858
+ datasetId: ref.datasetId,
2859
+ datasetName: this.datasets.get(ref.datasetId)?.name ?? ref.datasetId,
2860
+ objectId: hit.objectId,
2861
+ batchId: -1,
2862
+ instanceIndex: undefined,
2863
+ skeletonName: ref.skeletonName,
2864
+ centerLngLat,
2865
+ point,
2866
+ lngLat,
2867
+ altitude,
2868
+ ...attributes,
2869
+ mesh,
2870
+ };
2871
+ }
2687
2872
  /** 按 canvas 左上角的 CSS 像素坐标拾取。 */
2688
2873
  pick(x, y) {
2689
2874
  if (!this.picker || !this.scene || !this.camera || !this.sceneAnchor)
@@ -3369,6 +3554,25 @@ export class S3MViewer {
3369
3554
  }
3370
3555
  this._requestRepaint();
3371
3556
  }
3557
+ /**
3558
+ * 泼溅实例按视深 back-to-front 重排:alpha 混合是顺序相关的,
3559
+ * 乱序混合就是满屏彩色噪点。相机没动且没有新瓦片时整个跳过;
3560
+ * 单个瓦片顺序本来就对时 sortSplatMesh 内部也会跳过重写。
3561
+ */
3562
+ _sortSplats() {
3563
+ const cam = this.camera;
3564
+ if (!cam)
3565
+ return;
3566
+ if (this._splatsSorted && this._lastSplatSortView.equals(cam.matrixWorldInverse))
3567
+ return;
3568
+ for (const pc of this._pointClouds) {
3569
+ if (!pc.userData.isGaussianSplat)
3570
+ continue;
3571
+ sortSplatMesh(pc, cam.matrixWorldInverse);
3572
+ }
3573
+ this._lastSplatSortView.copy(cam.matrixWorldInverse);
3574
+ this._splatsSorted = true;
3575
+ }
3372
3576
  /**
3373
3577
  * 渲染一帧。所有宿主相关量都由调用方喂进来,viewer 不碰任何地图 API:
3374
3578
  *
@@ -3440,11 +3644,14 @@ export class S3MViewer {
3440
3644
  this._updateLights();
3441
3645
  // 等宽棱线按设备像素撑开,分辨率每帧同步一次
3442
3646
  this._updateLineResolution(canvas);
3647
+ // 泼溅的最小像素半径 / 抗锯齿低通同样要把 ndc 换算成像素
3648
+ setSplatViewport(canvas.width, canvas.height);
3443
3649
  this.renderer.setViewport(0, 0, canvas.width, canvas.height);
3444
3650
  this.renderer.resetState();
3445
3651
  // 完全透明时什么都不画:透明物体照样写深度,画了会在底图上挖出一个洞。
3446
3652
  // 这里跳过的是「本层的绘制」,宿主自己的清屏 / 底图照旧。
3447
3653
  if ((this.uniforms.uOpacity?.value ?? 1) > OPACITY_EPS) {
3654
+ this._sortSplats();
3448
3655
  this.renderer.render(this.scene, this.camera);
3449
3656
  // GPU 描边必须画在主场景之后:它的全屏 quad 参与宿主深度测试,
3450
3657
  // 依赖本帧已经写好的深度(被地形挡住的地方不该描边)
@@ -3488,7 +3695,9 @@ export class S3MViewer {
3488
3695
  programs: 0,
3489
3696
  textures: 0,
3490
3697
  geometries: 0,
3698
+ degradations: [],
3491
3699
  };
3700
+ let degradations = null;
3492
3701
  for (const entry of this.datasets.values()) {
3493
3702
  const s = entry.tileset?.stats;
3494
3703
  if (!s)
@@ -3499,7 +3708,17 @@ export class S3MViewer {
3499
3708
  total.meshes += s.meshes;
3500
3709
  total.vertices += s.vertices;
3501
3710
  total.triangles += s.triangles;
3711
+ // 降级项跨数据集去重:同一类只报一次给 UI
3712
+ if (s.degradations?.length) {
3713
+ degradations ??= [];
3714
+ for (const d of s.degradations) {
3715
+ if (!degradations.includes(d))
3716
+ degradations.push(d);
3717
+ }
3718
+ }
3502
3719
  }
3720
+ if (degradations)
3721
+ total.degradations = degradations;
3503
3722
  // GPU 侧预算观测:draw call / program / 显存驻留。没有渲染器(未 mount /
3504
3723
  // 纯解析场景)时保持 0,不抛错。
3505
3724
  const info = this.renderer?.info;