s3m-three 0.4.16 → 0.4.18

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 (47) hide show
  1. package/dist/browser/s3m-three.iife.ext-three.js +41 -23
  2. package/dist/browser/s3m-three.iife.js +41 -23
  3. package/dist/core/AttributeQuery.d.ts +13 -0
  4. package/dist/core/AttributeQuery.d.ts.map +1 -1
  5. package/dist/core/AttributeQuery.js +20 -3
  6. package/dist/core/AttributeQuery.js.map +1 -1
  7. package/dist/core/AuthFetch.d.ts +52 -0
  8. package/dist/core/AuthFetch.d.ts.map +1 -0
  9. package/dist/core/AuthFetch.js +44 -0
  10. package/dist/core/AuthFetch.js.map +1 -0
  11. package/dist/core/types.d.ts +8 -0
  12. package/dist/core/types.d.ts.map +1 -1
  13. package/dist/index.d.ts +4 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/render/DatasetManager.d.ts +7 -1
  18. package/dist/render/DatasetManager.d.ts.map +1 -1
  19. package/dist/render/DatasetManager.js +2 -1
  20. package/dist/render/DatasetManager.js.map +1 -1
  21. package/dist/render/HostRenderer.d.ts +105 -0
  22. package/dist/render/HostRenderer.d.ts.map +1 -0
  23. package/dist/render/HostRenderer.js +170 -0
  24. package/dist/render/HostRenderer.js.map +1 -0
  25. package/dist/render/S3MViewer.d.ts +28 -8
  26. package/dist/render/S3MViewer.d.ts.map +1 -1
  27. package/dist/render/S3MViewer.js +101 -42
  28. package/dist/render/S3MViewer.js.map +1 -1
  29. package/dist/render/StyleMaterial.d.ts +6 -4
  30. package/dist/render/StyleMaterial.d.ts.map +1 -1
  31. package/dist/render/StyleMaterial.js +48 -3
  32. package/dist/render/StyleMaterial.js.map +1 -1
  33. package/dist/tileset/S3MTileset.d.ts +8 -0
  34. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  35. package/dist/tileset/S3MTileset.js +23 -5
  36. package/dist/tileset/S3MTileset.js.map +1 -1
  37. package/docs/API.md +40 -22
  38. package/package.json +1 -1
  39. package/src/core/AttributeQuery.ts +26 -3
  40. package/src/core/AuthFetch.ts +82 -0
  41. package/src/core/types.ts +8 -0
  42. package/src/index.ts +4 -0
  43. package/src/render/DatasetManager.ts +9 -1
  44. package/src/render/HostRenderer.ts +222 -0
  45. package/src/render/S3MViewer.ts +112 -39
  46. package/src/render/StyleMaterial.ts +59 -3
  47. package/src/tileset/S3MTileset.ts +28 -5
@@ -23,8 +23,9 @@
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 { authFetch, type AuthenticateFn } from '../core/AuthFetch.js';
26
27
  import { setMaxAnisotropy, setCompressedTexturesSupported, setTextureOffThreadDecode, setMaxTextureSize as setTextureSizeCap } from './TextureFactory.js';
27
- import { setMaterialQuality } from './StyleMaterial.js';
28
+ import { getMaterialQuality, setMaterialQuality } from './StyleMaterial.js';
28
29
  import { decodeTextureWithWorker } from '../worker/S3MWorkerClient.js';
29
30
  import {
30
31
  STYLE,
@@ -32,6 +33,7 @@ import {
32
33
  SELECT_MODE,
33
34
  DEFAULT_STYLE,
34
35
  createSharedUniforms,
36
+ createStyledMaterial,
35
37
  createOutlineMaterial,
36
38
  resolveEdgeModes,
37
39
  sameEdgeModes,
@@ -40,11 +42,13 @@ import type {
40
42
  OutlineMaterial,
41
43
  OutlineStyleUniforms,
42
44
  StyledMaterial,
45
+ StyledMaterialOptions,
43
46
  StyleMode,
44
47
  SelectMode,
45
48
  S3MStyle,
46
49
  EdgeModes,
47
50
  EdgesOption,
51
+ MaterialQuality,
48
52
  } from './StyleMaterial.js';
49
53
  import { applyPointCloudStyle, isOwnedPointCloudMaterial } from './PointCloudMaterial.js';
50
54
  import { Picker } from './Picker.js';
@@ -287,6 +291,13 @@ export interface S3MViewerOptions {
287
291
  onRequestRepaint?: () => void;
288
292
  /** 地图朝向(弧度),用于让灯光跟着视角走。不传则灯光方向固定 */
289
293
  getBearing?: () => number;
294
+ /**
295
+ * 鉴权适配函数(全局默认,数据集可用自己的 `authenticate` 覆盖)。
296
+ * 内核主线程发出的全部请求(配置 scp / 瓦片 / 子图集 / 棱线缓存 / 属性查询)
297
+ * 发出前都过一遍,可改写 URL(加 `?token=`)或附加请求头。
298
+ * 不传 = 行为与旧版完全一致。见 `AuthenticateFn`。
299
+ */
300
+ authenticate?: AuthenticateFn;
290
301
  onStats?: (stats: Stats) => void;
291
302
  onReady?: (entry: S3MDatasetEntry) => void;
292
303
  onPick?: (result: PickResult | null) => void;
@@ -569,6 +580,8 @@ export class S3MViewer {
569
580
  /** 宿主重画信号与地图朝向,构造时注入 */
570
581
  private _onRequestRepaint: () => void = () => {};
571
582
  private _getBearing: (() => number) | undefined;
583
+ /** 鉴权适配(全局默认),构造时注入 */
584
+ private readonly _authenticate: AuthenticateFn | undefined;
572
585
  private _lodRangeScale = 1.0;
573
586
  /** LOD 判定屏幕高度的 DPR 上限。Infinity = 不截断(默认,行为同旧版) */
574
587
  private _lodPixelRatioCap = Infinity;
@@ -597,6 +610,7 @@ export class S3MViewer {
597
610
  }
598
611
  this._onRequestRepaint = options.onRequestRepaint ?? (() => {});
599
612
  this._getBearing = options.getBearing;
613
+ this._authenticate = options.authenticate;
600
614
  this.onStats = options.onStats ?? (() => {});
601
615
  this.onReady = options.onReady ?? (() => {});
602
616
  this.onPick = options.onPick ?? (() => {});
@@ -667,7 +681,11 @@ export class S3MViewer {
667
681
 
668
682
  this._fps.last = performance.now();
669
683
  this._datasetQueue = this._normalizeDatasets(options);
670
- if (options.attributes) this.attributeQuery = new AttributeQuery(options.attributes);
684
+ if (options.attributes)
685
+ this.attributeQuery = new AttributeQuery({
686
+ ...options.attributes,
687
+ authenticate: options.attributes.authenticate ?? this._authenticate,
688
+ });
671
689
  }
672
690
 
673
691
  /* ---------------------------------------------------------------- 生命周期 */
@@ -947,7 +965,8 @@ export class S3MViewer {
947
965
  },
948
966
  },
949
967
  this.options.maxTriangles ?? Infinity,
950
- this.options.maxImageCache ?? 128
968
+ this.options.maxImageCache ?? 128,
969
+ this._authenticate
951
970
  );
952
971
  return entry;
953
972
  }
@@ -1613,7 +1632,9 @@ export class S3MViewer {
1613
1632
  /* ---------------------------------------------------------------- 按业务字段着色 */
1614
1633
 
1615
1634
  setAttributeSource(options: AttributeQueryOptions | null): void {
1616
- this.attributeQuery = options ? new AttributeQuery(options) : null;
1635
+ this.attributeQuery = options
1636
+ ? new AttributeQuery({ ...options, authenticate: options.authenticate ?? this._authenticate })
1637
+ : null;
1617
1638
  }
1618
1639
 
1619
1640
  getLoadedSmids(): number[] {
@@ -1864,8 +1885,11 @@ export class S3MViewer {
1864
1885
  mat.transparent = o < 0.999;
1865
1886
  const keepDepth = this.style.depthWrite !== false;
1866
1887
  mat.depthWrite = mat.transparent ? keepDepth : (this.style.depthWrite !== false);
1867
- mat.flatShading = this.style.shading !== 'smooth';
1868
- mat.needsUpdate = true;
1888
+ // shading 走共享 uniform(uFlatShading),不写 mat.flatShading / needsUpdate——
1889
+ // transparent / depthWrite 也不是 program 决定项,整套样式继承零重编译
1890
+ if (mat.userData.uFlatShading) {
1891
+ mat.userData.uFlatShading.value = this.style.shading !== 'smooth' ? 1 : 0;
1892
+ }
1869
1893
  this._featureStyleManager.applyOverridesToMesh(mesh);
1870
1894
  this._startTileFade(mesh, mat);
1871
1895
  }
@@ -2369,26 +2393,74 @@ export class S3MViewer {
2369
2393
 
2370
2394
  /** 平面/平滑着色:FLAT_SHADED 是编译期宏,改它必须重编译 */
2371
2395
  private _applyShading(): void {
2372
- const flat = this.style.shading !== 'smooth';
2396
+ // shading 现在是共享 uniform(uFlatShading)驱动的运行期分支,不再是
2397
+ // flatShading define:翻一个 float 就完成切换,零重编译、零卡顿。
2398
+ const target = this.style.shading !== 'smooth' ? 1 : 0;
2373
2399
  for (const p of this._pickables.values()) {
2374
2400
  const m = styledMaterialOf(p.mesh);
2375
- if (!m || m.flatShading === flat) continue;
2376
- m.flatShading = flat;
2377
- m.needsUpdate = true;
2401
+ const u = m?.userData.uFlatShading;
2402
+ if (!u || u.value === target) continue;
2403
+ u.value = target;
2378
2404
  }
2379
2405
  this._requestRepaint();
2380
2406
  }
2381
2407
 
2382
2408
  /**
2383
- * 预热另一种 `shading` 变体,让首次切换不再同步编译着色器。
2409
+ * 运行时切换材质质感(`'pbr'` | `'lambert'`)。
2384
2410
  *
2385
- * 做法是「把材质翻到另一种变体 → 编译 → 翻回来」:three 的 program 缓存按
2386
- * cacheKey(含 flatShading / defines)分桶,编译一次就留在缓存里,之后
2387
- * `_applyShading()` 切过去只是取缓存。
2411
+ * 档位是模块级设置(影响之后新建的材质);已加载瓦片的材质在这里**原位重建**:
2412
+ * 不重拉瓦片、不重建几何,按创建时记录的参数(`userData.s3mOpts`)换一个
2413
+ * 新材质实例,并保留逐材质状态(选中编号、瓦片淡入、批量状态纹理)。
2414
+ * 切换后 program 需要重新编译一次(lambert / pbr 是不同着色器),这是唯一开销。
2415
+ */
2416
+ setMaterialQuality(q: MaterialQuality): void {
2417
+ setMaterialQuality(q);
2418
+ this._rebuildStyledMaterials();
2419
+ this._requestRepaint();
2420
+ }
2421
+
2422
+ /** 当前材质质感档位 */
2423
+ get materialQuality(): MaterialQuality {
2424
+ return getMaterialQuality();
2425
+ }
2426
+
2427
+ /** 按创建参数原位重建所有 styled 材质(materialQuality 切换用) */
2428
+ private _rebuildStyledMaterials(): void {
2429
+ // 逐材质 own uniform 的状态要在重建时搬过去,否则选中高亮 / 瓦片淡入会闪没
2430
+ const PRESERVED = [
2431
+ 'uSelBatch',
2432
+ 'uSelWhole',
2433
+ 'uBatchTex',
2434
+ 'uBatchTex2',
2435
+ 'uBatchTexWidth',
2436
+ 'uBatchTexActive',
2437
+ 'uTileFade',
2438
+ ] as const;
2439
+ for (const p of this._pickables.values()) {
2440
+ const mesh = p.mesh;
2441
+ const raw = mesh.material;
2442
+ if (Array.isArray(raw)) continue; // styled 材质恒为单材质;数组材质(点云等)不归这里管
2443
+ const old = raw as StyledMaterial | undefined;
2444
+ if (!old?.userData?.isStyled || !old.userData?.s3mOpts) continue;
2445
+ const opts = old.userData.s3mOpts as StyledMaterialOptions;
2446
+ const fresh = createStyledMaterial({ ...opts, uniforms: this.uniforms });
2447
+ for (const key of PRESERVED) {
2448
+ const src = old.userData[key] as { value: unknown } | undefined;
2449
+ const dst = fresh.userData[key] as { value: unknown } | undefined;
2450
+ if (src && dst) dst.value = src.value;
2451
+ }
2452
+ mesh.material = fresh;
2453
+ old.dispose();
2454
+ }
2455
+ }
2456
+
2457
+ /**
2458
+ * 预热着色 program,避免首帧同步编译。
2388
2459
  *
2389
- * 翻转期间材质是「错误变体」,所以用 `_prewarming` 让 renderFrame 跳过渲染——
2390
- * 少画一两帧看不出来,把平滑的楼画成平面的就很明显。
2391
- * 优先用 `compileAsync`(KHR_parallel_shader_compile),没有就退回同步 `compile`。
2460
+ * shading 改为 `uFlatShading` 共享 uniform 驱动后(见 `_applyShading`),
2461
+ * flat / smooth 共用同一个 program,不再存在「另一种变体」,也不需要
2462
+ * 「翻变体 → 编译 → 翻回来」的那套流程与 `_prewarming` 跳帧。这里只把当前
2463
+ * 唯一的 program 编一次(优先 `compileAsync`,KHR_parallel_shader_compile)。
2392
2464
  */
2393
2465
  async prewarmShading(): Promise<void> {
2394
2466
  if (this._shadingPrewarmed || this._prewarming) return;
@@ -2397,23 +2469,17 @@ export class S3MViewer {
2397
2469
  const camera = this.camera;
2398
2470
  if (!r || !scene || !camera) return;
2399
2471
 
2400
- const mats: StyledMaterial[] = [];
2401
- const seen = new Set<THREE.Material>();
2472
+ // 场上还没有材质就没必要预热:program 会在首个材质出现时编译
2473
+ let hasMaterial = false;
2402
2474
  for (const p of this._pickables.values()) {
2403
- const m = styledMaterialOf(p.mesh);
2404
- if (!m || seen.has(m)) continue;
2405
- seen.add(m);
2406
- mats.push(m);
2475
+ if (styledMaterialOf(p.mesh)) {
2476
+ hasMaterial = true;
2477
+ break;
2478
+ }
2407
2479
  }
2408
- if (mats.length === 0) return;
2480
+ if (!hasMaterial) return;
2409
2481
 
2410
2482
  this._shadingPrewarmed = true;
2411
- this._prewarming = true;
2412
- // 当前是 smooth(flatShading = false)就预热 flat,反之预热 smooth
2413
- const target = this.style.shading === 'smooth';
2414
- const saved = mats.map((m) => m.flatShading);
2415
- for (const m of mats) m.flatShading = target;
2416
-
2417
2483
  try {
2418
2484
  const anyR = r as unknown as {
2419
2485
  compileAsync?: (s: THREE.Object3D, c: THREE.Camera) => Promise<unknown>;
@@ -2421,15 +2487,9 @@ export class S3MViewer {
2421
2487
  if (typeof anyR.compileAsync === 'function') await anyR.compileAsync(scene, camera);
2422
2488
  else r.compile(scene, camera);
2423
2489
  } catch {
2424
- // 预热失败不影响主流程:最坏情况退回「首次切换时编译一次」
2425
- } finally {
2426
- for (let i = 0; i < mats.length; i++) {
2427
- mats[i].flatShading = saved[i];
2428
- mats[i].needsUpdate = true;
2429
- }
2430
- this._prewarming = false;
2431
- this._requestRepaint();
2490
+ // 预热失败不影响主流程:最坏情况退回「首次渲染时编译一次」
2432
2491
  }
2492
+ this._requestRepaint();
2433
2493
  }
2434
2494
 
2435
2495
  /**
@@ -2641,7 +2701,7 @@ export class S3MViewer {
2641
2701
  * @param url 棱线缓存地址(二进制 Float32 序列:x,y,z,x,y,z,… 成对成段)
2642
2702
  */
2643
2703
  async loadCreases(url: string): Promise<void> {
2644
- const resp = await fetch(url);
2704
+ const resp = await authFetch(url, this._authenticate, 'creases');
2645
2705
  if (!resp.ok) throw new Error(`[s3m] 棱线缓存加载失败: ${resp.status} ${url}`);
2646
2706
  const buf = await resp.arrayBuffer();
2647
2707
  const arr = new Float32Array(buf);
@@ -3989,6 +4049,10 @@ export class S3MViewer {
3989
4049
  vertices: 0,
3990
4050
  triangles: 0,
3991
4051
  datasets: this.datasets.size,
4052
+ drawCalls: 0,
4053
+ programs: 0,
4054
+ textures: 0,
4055
+ geometries: 0,
3992
4056
  };
3993
4057
  for (const entry of this.datasets.values()) {
3994
4058
  const s = entry.tileset?.stats;
@@ -4000,6 +4064,15 @@ export class S3MViewer {
4000
4064
  total.vertices += s.vertices;
4001
4065
  total.triangles += s.triangles;
4002
4066
  }
4067
+ // GPU 侧预算观测:draw call / program / 显存驻留。没有渲染器(未 mount /
4068
+ // 纯解析场景)时保持 0,不抛错。
4069
+ const info = this.renderer?.info;
4070
+ if (info) {
4071
+ total.drawCalls = info.render.calls;
4072
+ total.programs = info.programs?.length ?? 0;
4073
+ total.textures = info.memory.textures;
4074
+ total.geometries = info.memory.geometries;
4075
+ }
4003
4076
  return total;
4004
4077
  }
4005
4078
 
@@ -375,6 +375,12 @@ export function createSharedUniforms(): StyleUniforms {
375
375
  * (压暗 → 换模式 → 亮回来),比硬切自然得多。
376
376
  */
377
377
  uFade: { value: 1 },
378
+ /**
379
+ * 平面 / 平滑着色开关:0 = 平滑(插值法线),1 = 平面(屏幕导数法线)。
380
+ * 走 uniform 而不是 three 的 `flatShading` define,所以切换 shading 不会触发
381
+ * 着色器重编译(旧实现靠 prewarm 把卡顿藏起来,现在连卡顿都没有)。
382
+ */
383
+ uFlatShading: { value: 1 },
378
384
  };
379
385
  }
380
386
  const VERT_PARS = /* glsl */ `
@@ -411,6 +417,8 @@ const VERT_PARS = /* glsl */ `
411
417
  varying float vWorldZ;
412
418
  varying vec3 vWorldNormal;
413
419
  varying vec3 vWorldPos;
420
+ // 视图空间位置:平面着色用它算屏幕导数法线,符号与 three 的 vViewPosition 一致
421
+ varying vec3 vS3MViewPos;
414
422
 
415
423
  // 跨 chunk 共享的顶点姿态变量(在 beginnormal_vertex 建立,begin_vertex / worldpos 使用)
416
424
  vec3 s3mCustomOffset;
@@ -576,6 +584,9 @@ const VERT_WORLD = /* glsl */ `
576
584
  mat3 s3mNormMat = mat3( modelMatrix );
577
585
  #endif
578
586
  vWorldNormal = normalize( s3mNormMat * ( s3mRot * normal ) );
587
+ // 视图空间位置直接拷 three 的 vViewPosition(此处已在 project_vertex 之后,
588
+ // 含实例化 / 形变的最终值):平面着色的导数法线与 FLAT_SHADED 分支逐点一致
589
+ vS3MViewPos = vViewPosition;
579
590
  `;
580
591
 
581
592
  const FRAG_PARS = /* glsl */ `
@@ -684,6 +695,8 @@ const FRAG_PARS = /* glsl */ `
684
695
  varying float vWorldZ;
685
696
  varying vec3 vWorldNormal;
686
697
  varying vec3 vWorldPos;
698
+ varying vec3 vS3MViewPos;
699
+ uniform float uFlatShading;
687
700
 
688
701
  vec4 s3mSampleBatchRow0( sampler2D texA, sampler2D texB, vec2 uv, float mixVal ) {
689
702
  vec4 vA = texture2D( texA, uv );
@@ -885,6 +898,12 @@ export interface StyledMaterialOptions {
885
898
  transparent?: boolean;
886
899
  opacity?: number;
887
900
  roughness?: number;
901
+ /**
902
+ * @deprecated 旧版用 three 的 `flatShading` define 表达,切换必重编译;现在
903
+ * shading 走共享 uniform `uFlatShading`(见 `createSharedUniforms`),运行期
904
+ * 二选一。此参数仅作为 uniform 的**初始值**被兼容(false = smooth),
905
+ * 运行期切换请走 viewer 的 `setStyle({ shading })`。
906
+ */
888
907
  flatShading?: boolean;
889
908
  /** >0 时开启 alphaTest:镂空贴图(S3MBLOCK 图集)的透明片元直接丢弃,避免渲染成黑面 */
890
909
  alphaTest?: number;
@@ -898,6 +917,27 @@ export interface StyledMaterialOptions {
898
917
  * 创建一个带样式的 PBR 材质。
899
918
  * 每个 Mesh 独享一个材质实例,但 uniform 对象是全局共享的。
900
919
  */
920
+ /**
921
+ * 片元法线覆盖:平面着色时用屏幕导数算真实几何法线。
922
+ *
923
+ * 注入在 `<normal_fragment_begin>` 之后——此时 three 已按平滑路径算好 `normal`
924
+ * (来自插值 vNormal);uFlatShading > 0.5 时用视图空间位置的导数叉积覆盖它,
925
+ * 数学上与 three 的 FLAT_SHADED 分支逐点一致(vS3MViewPos 符号对齐 vViewPosition)。
926
+ * 因为是运行期分支而非编译期 define,切 shading 不再触发重编译。
927
+ */
928
+ const FRAG_NORMAL = /* glsl */ `
929
+ if ( uFlatShading > 0.5 ) {
930
+ vec3 s3mFlatN = normalize( cross( dFdx( vS3MViewPos ), dFdy( vS3MViewPos ) ) );
931
+ #ifdef USE_DOUBLE_SIDED
932
+ s3mFlatN = s3mFlatN * ( float( gl_FrontFacing ) * 2.0 - 1.0 );
933
+ #endif
934
+ #ifdef USE_FLIP_SIDED
935
+ s3mFlatN = - s3mFlatN;
936
+ #endif
937
+ normal = s3mFlatN;
938
+ }
939
+ `;
940
+
901
941
  export function createStyledMaterial(opts: StyledMaterialOptions): StyledMaterial {
902
942
  const uniforms = opts.uniforms;
903
943
  // 两种光照模型共用的参数。注入的 chunk 只动 diffuseColor / normal / transformed,
@@ -909,9 +949,10 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
909
949
  transparent: !!opts.transparent,
910
950
  opacity: opts.opacity ?? 1,
911
951
  depthWrite: opts.transparent ? false : true,
912
- // 白模 / OSGB 的法线多是共享顶点平均出来的,平滑插值会让墙面出现斑驳,
913
- // 默认走平面着色(片元里用导数算法线),墙面才干净。
914
- flatShading: opts.flatShading !== false,
952
+ // 白模 / OSGB 的法线多是共享顶点平均出来的,平滑插值会让墙面出现斑驳。
953
+ // 平面 / 平滑不再走 three 的 flatShading define(那是编译期变体,切换必重编译),
954
+ // 改由共享 uniform uFlatShading 在片元里二选一:材质恒为平滑,program 因此稳定。
955
+ flatShading: false,
915
956
  // 顶点色、贴图全部在自定义 chunk 里处理,不让 three 插手
916
957
  vertexColors: false,
917
958
  alphaTest: opts.alphaTest ?? 0,
@@ -930,6 +971,10 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
930
971
  const uSelBatch = { value: -1 };
931
972
  // 没有单体化信息的几何(batchId = -1)只能整块选中
932
973
  const uSelWhole = { value: 0 };
974
+ // shading 开关:跟随共享 uniforms(改一次全场景生效);测试里可能不传
975
+ // uniforms,这里兜底一份独立对象,与旧代码容忍 uniforms === undefined 的口径一致。
976
+ // 兼容旧参数:显式传 flatShading: false(旧语义 = smooth)映射为初始值 0
977
+ const uFlatShading = uniforms?.uFlatShading ?? { value: opts.flatShading === false ? 0 : 1 };
933
978
  const uBatchTex = { value: DUMMY_BATCH_TEX };
934
979
  const uBatchTex2 = { value: DUMMY_BATCH_TEX };
935
980
  const uBatchTexWidth = { value: 1.0 };
@@ -950,6 +995,7 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
950
995
  Object.assign(shader.uniforms, uniforms, {
951
996
  uSelBatch,
952
997
  uSelWhole,
998
+ uFlatShading,
953
999
  uBatchTex,
954
1000
  uBatchTex2,
955
1001
  uBatchTexWidth,
@@ -987,6 +1033,12 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
987
1033
  } else if (secMap) {
988
1034
  shader.fragmentShader = shader.fragmentShader.replace('#include <map_fragment>', `#include <map_fragment>\n${FRAG_BAKE}`);
989
1035
  }
1036
+
1037
+ // 平面 / 平滑着色:运行期分支(见 FRAG_NORMAL),不再依赖 flatShading define
1038
+ shader.fragmentShader = shader.fragmentShader.replace(
1039
+ '#include <normal_fragment_begin>',
1040
+ `#include <normal_fragment_begin>\n${FRAG_NORMAL}`
1041
+ );
990
1042
  };
991
1043
 
992
1044
  // 不同材质共用同一段注入代码,必须让 three 认为它们的 program 可复用;
@@ -1009,6 +1061,10 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
1009
1061
  mat.userData.uBatchTexWidth = uBatchTexWidth;
1010
1062
  mat.userData.uBatchTexActive = uBatchTexActive;
1011
1063
  mat.userData.uTileFade = uTileFade;
1064
+ // 平面 / 平滑着色开关:S3MViewer._applyShading 翻它,不触发重编译
1065
+ mat.userData.uFlatShading = uFlatShading;
1066
+ // 记录创建参数:materialQuality 运行时切换时按原参数重建材质(换类不换外观)
1067
+ mat.userData.s3mOpts = opts;
1012
1068
  mat.userData.isStyled = true;
1013
1069
  return mat;
1014
1070
  }
@@ -7,6 +7,7 @@
7
7
  * 命中阈值就请求 childTile,实现渐进式细化。
8
8
  */
9
9
  import { parseS3MB } from '../core/S3MBParser.js';
10
+ import { authFetch, type AuthenticateFn } from '../core/AuthFetch.js';
10
11
  import { getSharedS3MWorker, S3MWorkerClient, WorkerDeadError } from '../worker/S3MWorkerClient.js';
11
12
  import {
12
13
  composeLeafAtlas,
@@ -281,6 +282,11 @@ export interface S3MTilesetOptions {
281
282
  * 度量方式见 `_tmp/atlas/rule2.mts`。
282
283
  */
283
284
  atlasLod?: boolean;
285
+ /**
286
+ * 鉴权适配函数(默认不传 = 行为与旧版一致)。每个请求(配置 scp / 瓦片 /
287
+ * 子图集)发出前过一遍,可改写 URL(加 `?token=`)或附加请求头。见 `AuthenticateFn`。
288
+ */
289
+ authenticate?: AuthenticateFn;
284
290
  }
285
291
 
286
292
  export class S3MTileset {
@@ -321,7 +327,21 @@ export class S3MTileset {
321
327
  readonly atlasUpgraded = new Set<string>();
322
328
  /** 拿到高清版但判定不通过 → 原因。用于排查「为什么这块还是糊的」 */
323
329
  readonly atlasUpgradeSkipped = new Map<string, string>();
324
- readonly stats: Stats = { tiles: 0, loading: 0, failed: 0, meshes: 0, vertices: 0, triangles: 0, datasets: 1 };
330
+ // drawCalls / programs / textures / geometries 是 viewer 级 GPU 观测,
331
+ // 瓦片侧不产生,恒为 0;聚合时由 S3MViewer.stats 用 renderer.info 覆盖
332
+ readonly stats: Stats = {
333
+ tiles: 0,
334
+ loading: 0,
335
+ failed: 0,
336
+ meshes: 0,
337
+ vertices: 0,
338
+ triangles: 0,
339
+ datasets: 1,
340
+ drawCalls: 0,
341
+ programs: 0,
342
+ textures: 0,
343
+ geometries: 0,
344
+ };
325
345
 
326
346
  maxConcurrent: number;
327
347
  maxTiles: number;
@@ -346,6 +366,8 @@ export class S3MTileset {
346
366
  private readonly _onError: (error: unknown) => void;
347
367
  private readonly _onProgress: (stats: Stats) => void;
348
368
  private readonly _maxRetries: number;
369
+ /** 鉴权适配函数;undefined = 不做任何改写(行为与旧版一致) */
370
+ readonly authenticate: AuthenticateFn | undefined;
349
371
  /** 失败重试计数与下次可用时间,退避期间 _enqueue 会直接跳过 */
350
372
  private readonly _retryCount = new Map<string, number>();
351
373
  private readonly _retryAt = new Map<string, number>();
@@ -374,6 +396,7 @@ export class S3MTileset {
374
396
  this._onError = options.onError ?? ((e) => console.warn(e));
375
397
  this._onProgress = options.onProgress ?? (() => {});
376
398
  this._maxRetries = Math.max(0, options.maxRetries ?? 2);
399
+ this.authenticate = options.authenticate;
377
400
 
378
401
  // 渲染层可注入:Node 环境下不需要 three,也能跑 URL / 解析链路
379
402
  this._buildTile = options.buildTile ?? null;
@@ -395,7 +418,7 @@ export class S3MTileset {
395
418
 
396
419
  async init(): Promise<this> {
397
420
  this.configUrl = toAbsolute(this.configUrl);
398
- const res = await fetch(this.configUrl);
421
+ const res = await authFetch(this.configUrl, this.authenticate, 'config');
399
422
  if (!res.ok) throw new Error(`配置读取失败 ${res.status} ${this.configUrl}`);
400
423
  const text = await res.text();
401
424
  const config = parseConfigText(text);
@@ -977,7 +1000,7 @@ export class S3MTileset {
977
1000
  const u = resolveUrl(dirOf(baseUrl), `${names[0]}.texblock?extratiles=${names.join(';')}`);
978
1001
  try {
979
1002
  if (!u) throw new Error('无法拼出 URL');
980
- const res = await fetch(u);
1003
+ const res = await authFetch(u, this.authenticate, 'texblock');
981
1004
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
982
1005
  const segments = await parseTexblockSegments(await res.arrayBuffer());
983
1006
  if (segments.length !== names.length) {
@@ -995,7 +1018,7 @@ export class S3MTileset {
995
1018
  const u = resolveUrl(dirOf(baseUrl), `${stem}.texblock`);
996
1019
  try {
997
1020
  if (!u) throw new Error('无法拼出 URL');
998
- const res = await fetch(u);
1021
+ const res = await authFetch(u, this.authenticate, 'texblock');
999
1022
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
1000
1023
  const records = (await parseTexblockSegments(await res.arrayBuffer())).flat();
1001
1024
  this.putLeafRecords(stem, records);
@@ -1038,7 +1061,7 @@ export class S3MTileset {
1038
1061
  groupNames = cachedSet.names;
1039
1062
  tile = cachedSet.tiles[groupIndex] ?? cachedSet.tiles[0]!;
1040
1063
  } else {
1041
- const res = await fetch(baseUrl);
1064
+ const res = await authFetch(baseUrl, this.authenticate, 'tile');
1042
1065
  if (!res.ok) {
1043
1066
  throw new Error(`HTTP ${res.status} 加载瓦片失败: ${baseUrl}`);
1044
1067
  }