s3m-three 0.4.21 → 0.4.23

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 (43) hide show
  1. package/dist/browser/s3m-three.iife.ext-three.js +35 -35
  2. package/dist/browser/s3m-three.iife.js +30 -30
  3. package/dist/render/DatasetManager.d.ts +30 -0
  4. package/dist/render/DatasetManager.d.ts.map +1 -1
  5. package/dist/render/DatasetManager.js +41 -0
  6. package/dist/render/DatasetManager.js.map +1 -1
  7. package/dist/render/GeometryBuilder.d.ts +18 -0
  8. package/dist/render/GeometryBuilder.d.ts.map +1 -1
  9. package/dist/render/GeometryBuilder.js +4 -1
  10. package/dist/render/GeometryBuilder.js.map +1 -1
  11. package/dist/render/S3MViewer.d.ts +68 -11
  12. package/dist/render/S3MViewer.d.ts.map +1 -1
  13. package/dist/render/S3MViewer.js +181 -19
  14. package/dist/render/S3MViewer.js.map +1 -1
  15. package/dist/render/StyleMaterial.d.ts +23 -2
  16. package/dist/render/StyleMaterial.d.ts.map +1 -1
  17. package/dist/render/StyleMaterial.js +35 -5
  18. package/dist/render/StyleMaterial.js.map +1 -1
  19. package/dist/render/TextureFactory.d.ts +23 -1
  20. package/dist/render/TextureFactory.d.ts.map +1 -1
  21. package/dist/render/TextureFactory.js +32 -8
  22. package/dist/render/TextureFactory.js.map +1 -1
  23. package/dist/render/viewer/LightRig.d.ts +21 -7
  24. package/dist/render/viewer/LightRig.d.ts.map +1 -1
  25. package/dist/render/viewer/LightRig.js +11 -13
  26. package/dist/render/viewer/LightRig.js.map +1 -1
  27. package/dist/render/viewer/types.d.ts +16 -2
  28. package/dist/render/viewer/types.d.ts.map +1 -1
  29. package/dist/render/viewer/types.js.map +1 -1
  30. package/dist/tileset/S3MTileset.d.ts +33 -0
  31. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  32. package/dist/tileset/S3MTileset.js +27 -0
  33. package/dist/tileset/S3MTileset.js.map +1 -1
  34. package/docs/API.md +26 -5
  35. package/package.json +1 -1
  36. package/src/render/DatasetManager.ts +47 -0
  37. package/src/render/GeometryBuilder.ts +22 -1
  38. package/src/render/S3MViewer.ts +192 -19
  39. package/src/render/StyleMaterial.ts +48 -6
  40. package/src/render/TextureFactory.ts +50 -8
  41. package/src/render/viewer/LightRig.ts +23 -13
  42. package/src/render/viewer/types.ts +16 -2
  43. package/src/tileset/S3MTileset.ts +43 -0
@@ -25,8 +25,9 @@ import { S3MTileset, usableGeoBounds } from '../tileset/S3MTileset.js';
25
25
  import type { BuildTileFn, TileNode } from '../tileset/S3MTileset.js';
26
26
  import type { AuthenticateFn } from '../core/AuthFetch.js';
27
27
  import { setSplatViewport } from './GeometryBuilder.js';
28
- import { setMaxAnisotropy, setCompressedTexturesSupported, setTextureOffThreadDecode, setMaxTextureSize as setTextureSizeCap } from './TextureFactory.js';
29
- import { getMaterialQuality, setMaterialQuality } from './StyleMaterial.js';
28
+ import { setMaxAnisotropy, setCompressedTexturesSupported, setTextureOffThreadDecode, setMaxTextureSize as setTextureSizeCap, applyAnisotropyCap } from './TextureFactory.js';
29
+ import type { TextureCreateOptions } from './TextureFactory.js';
30
+ import { getMaterialQuality, normalizeMaterialQuality, getColorPipeline, normalizeColorPipeline } from './StyleMaterial.js';
30
31
  import type { LightingPreset } from './lightingPreset.js';
31
32
  import { LightRig } from './viewer/LightRig.js';
32
33
  import { HeightIndex } from './viewer/HeightIndex.js';
@@ -62,6 +63,7 @@ import type {
62
63
  EdgeModes,
63
64
  EdgesOption,
64
65
  MaterialQuality,
66
+ ColorPipeline,
65
67
  } from './StyleMaterial.js';
66
68
  import { applyPointCloudStyle, isOwnedPointCloudMaterial } from './PointCloudMaterial.js';
67
69
  import type { DegradationKind, UnsupportedPolicy } from '../core/degradations.js';
@@ -204,6 +206,18 @@ const STYLE_SCALAR_KEYS = [
204
206
  'outlineWidth',
205
207
  ] as const;
206
208
 
209
+ /**
210
+ * 标量样式键在「用户还没设过」时的动画起点。
211
+ *
212
+ * 只对 `metalness` / `roughness` 有意义:它们的 uniform 默认是 -1(不覆盖,保持数据
213
+ * 自身的值),从 -1 插值没意义 —— 取数据侧常见值当起点,过渡才不会先跳到 0。
214
+ * 其余键缺省仍是 0(与旧行为一致)。
215
+ */
216
+ const SCALAR_ANIM_START: Partial<Record<(typeof STYLE_SCALAR_KEYS)[number], number>> = {
217
+ metalness: 0, // S3M 数据的 metalness 恒为 0
218
+ roughness: 0.95, // shininess 映射后的常见值,见 GeometryBuilder 的 roughness
219
+ };
220
+
207
221
  /** 能逐帧插值的颜色键(在线性空间插值,与 shader 消费端一致) */
208
222
  const STYLE_COLOR_KEYS = ['colorA', 'colorB', 'colorC', 'selectColor', 'edgeColor'] as const;
209
223
 
@@ -249,6 +263,25 @@ export class S3MViewer {
249
263
  readonly groundManager: GroundManager;
250
264
  /** 多数据集生命周期与锚点管理器 */
251
265
  private readonly _datasetManager = new DatasetManager();
266
+ /**
267
+ * 本图层的光照模型档位。
268
+ *
269
+ * `undefined` = 没指定,跟随模块级默认(`getMaterialQuality()`)—— 老代码
270
+ * `setMaterialQuality('lambert')` 一行改全场的用法靠这条继续有效。
271
+ * 传了值就只认自己,多图层同页各用各的,互不覆盖。
272
+ */
273
+ private _materialQuality: MaterialQuality | undefined;
274
+ /**
275
+ * 本图层的颜色管线。
276
+ *
277
+ * `undefined` = 没指定,跟随模块级默认(`getColorPipeline()`)。
278
+ * 与 materialQuality 同理:传了值就只认自己,同页多图层互不污染 —— 这条尤其重要,
279
+ * 因为 `lightingPreset: 'supermap'` 会同时改管线、纹理解码方式与各向异性,
280
+ * 以前是三个模块级全局一起被改写。
281
+ */
282
+ private _colorPipeline: ColorPipeline | undefined;
283
+ /** fade-through 编排的代次:中途再切一次,旧编排作废(与 setMode 同一套) */
284
+ private _pipelineGen = 0;
252
285
  /** 单体样式、专题着色与状态纹理管理器 */
253
286
  private readonly _featureStyleManager: FeatureStyleManager;
254
287
  /** GPU 路径动画管理器 */
@@ -352,7 +385,11 @@ export class S3MViewer {
352
385
  this._unsupported = options.unsupported ?? 'warn';
353
386
  this._lodPixelRatioCap = options.lodPixelRatioCap ?? Infinity;
354
387
  setTextureSizeCap(options.maxTextureSize ?? Infinity);
355
- setMaterialQuality(options.materialQuality ?? 'pbr');
388
+ // 只记本层的档位,**不写模块级全局**:以前这里 setMaterialQuality(...) 会把全局
389
+ // 改掉,于是同页后创建的图层顺手把先创建的图层也打回去了(多层互相覆盖的根因)。
390
+ this._materialQuality = normalizeMaterialQuality(options.materialQuality);
391
+ // 之后 addDataset 的数据集按这个默认建材质(数据集自己指定则用自己的)
392
+ this._datasetManager.defaultMaterialQuality = this._materialQuality;
356
393
  if (options.totalMaxTiles) {
357
394
  this._datasetManager.totalMaxTiles = options.totalMaxTiles;
358
395
  }
@@ -383,7 +420,12 @@ export class S3MViewer {
383
420
  );
384
421
 
385
422
  this.uniforms = createSharedUniforms();
386
- // 光源状态在这里就位:LightRig 构造时会按 lightingPreset 切模块级颜色管线,
423
+ // 图层 profile 先落位(显式传了就只认自己),再把 LightRig 挂上来 ——
424
+ // 它构造时会按 lightingPreset 覆盖一次管线,这一步走的是图层级通道,
425
+ // 不再去改模块级全局(以前三个全局一起被改写,同页多图层互相污染)。
426
+ this._colorPipeline = normalizeColorPipeline(options.colorPipeline);
427
+ this._applyRenderProfile({}, true);
428
+ // 光源状态在这里就位:LightRig 构造时会按 lightingPreset 切图层颜色管线,
387
429
  // 挂进场景要等 scene 建好(构造函数上面已经建了)。
388
430
  this._lights = new LightRig({
389
431
  scene: () => this.scene,
@@ -394,6 +436,8 @@ export class S3MViewer {
394
436
  // 走公共 triggerRepaint 而不是 _requestRepaint:宿主 / 单测会覆盖前者来观察重绘,
395
437
  // 绕过它会让「有没有触发重绘」这件事失去观测点
396
438
  repaint: () => this.triggerRepaint(),
439
+ // 构造期(initial)只记状态:那时还没有材质可重建、没有数据集可同步
440
+ applyRenderProfile: (p, initial) => this._applyRenderProfile(p, initial),
397
441
  });
398
442
  this._lights.addToScene();
399
443
  this._heights = new HeightIndex(this.uniforms);
@@ -1843,7 +1887,13 @@ export class S3MViewer {
1843
1887
  this.animator.register(`style.${key}`, {
1844
1888
  dim: 1,
1845
1889
  read: (out) => {
1846
- out[0] = Number((this.style as unknown as Record<string, number>)[key] ?? 0);
1890
+ // 金属度 / 粗糙度没设过时 uniform 是 -1(= 不覆盖,保持数据自身的值),
1891
+ // 动画起点不能取 0:那会让 roughness 从 0.95 先跳到 0 再爬回去。
1892
+ // 这里给的是数据侧的常见值(S3M 的 shininess 映射后 ≈0.95、metalness 恒 0),
1893
+ // 过渡起点因此是连续的。
1894
+ out[0] = Number(
1895
+ (this.style as unknown as Record<string, number>)[key] ?? SCALAR_ANIM_START[key] ?? 0
1896
+ );
1847
1897
  },
1848
1898
  write: (v) => {
1849
1899
  (this.style as unknown as Record<string, number>)[key] = v[0];
@@ -2082,22 +2132,131 @@ export class S3MViewer {
2082
2132
  }
2083
2133
 
2084
2134
  /**
2085
- * 运行时切换材质质感(`'pbr'` | `'lambert'`)。
2135
+ * 运行时切换**本图层**的材质质感(`'pbr'` | `'lambert'`)。
2086
2136
  *
2087
- * 档位是模块级设置(影响之后新建的材质);已加载瓦片的材质在这里**原位重建**:
2088
- * 不重拉瓦片、不重建几何,按创建时记录的参数(`userData.s3mOpts`)换一个
2089
- * 新材质实例,并保留逐材质状态(选中编号、瓦片淡入、批量状态纹理)。
2090
- * 切换后 program 需要重新编译一次(lambert / pbr 是不同着色器),这是唯一开销。
2137
+ * 档位是图层级的:只改这一层,同页其它图层不受影响(也不会去动模块级默认)。
2138
+ * 已加载瓦片的材质在这里**原位重建**:不重拉瓦片、不重建几何,按创建时记录的
2139
+ * 参数(`userData.s3mOpts`)换一个新材质实例,并保留逐材质状态(选中编号、
2140
+ * 瓦片淡入、批量状态纹理)。切换后 program 需要重新编译一次(lambert / pbr 是
2141
+ * 不同着色器),这是唯一开销。
2091
2142
  */
2092
2143
  setMaterialQuality(q: MaterialQuality): void {
2093
- setMaterialQuality(q);
2144
+ this._materialQuality = normalizeMaterialQuality(q) ?? 'pbr';
2145
+ // 已建好的数据集也要跟着换,否则之后新加载的瓦片还是旧档位(和重建结果打架)
2146
+ this._datasetManager.setDefaultMaterialQuality(this._materialQuality);
2094
2147
  this._rebuildStyledMaterials();
2095
2148
  this._requestRepaint();
2096
2149
  }
2097
2150
 
2098
- /** 当前材质质感档位 */
2151
+ /** 当前材质质感档位(本图层指定了就用本图层的,否则是模块级默认) */
2099
2152
  get materialQuality(): MaterialQuality {
2100
- return getMaterialQuality();
2153
+ return this._materialQuality ?? getMaterialQuality();
2154
+ }
2155
+
2156
+ /**
2157
+ * 运行时切换**本图层**的颜色管线(`'linear'` | `'direct'`)。
2158
+ *
2159
+ * `'direct'` = 官方 iClient3D 的 sRGB 直乘管线(配合纹理解码关 sRGB),
2160
+ * 见 `lightingPreset: 'supermap'`。切换牵扯三样东西:材质宏(换 program)、
2161
+ * 纹理 colorSpace(重传,**不重新下载瓦片**)、以及各向异性档位。
2162
+ *
2163
+ * 给 `duration > 0` 时走 fade-through:压暗 → 在同一帧回调里换管线 → 亮回来。
2164
+ * 逐像素混合做不到干净 —— 两条管线的差异有一部分在采样端(硬件 sRGB 解码 vs
2165
+ * 原始字节),要混合就得让贴图恒为原始字节并在片元里手动解码,那会改掉默认路径。
2166
+ */
2167
+ setColorPipeline(q: ColorPipeline, opts: { duration?: number; dim?: number } = {}): Promise<void> {
2168
+ const next = normalizeColorPipeline(q) ?? 'linear';
2169
+ if (next === this.colorPipeline) return Promise.resolve();
2170
+
2171
+ const d = Math.max(0, opts.duration ?? 0);
2172
+ if (d <= 0) {
2173
+ this._applyRenderProfile({ colorPipeline: next });
2174
+ return Promise.resolve();
2175
+ }
2176
+
2177
+ const dim = Math.min(1, Math.max(0.05, opts.dim ?? 0.35));
2178
+ const gen = ++this._pipelineGen;
2179
+ return new Promise<void>((resolve) => {
2180
+ this.animator.to('style.fade', dim, {
2181
+ duration: d,
2182
+ easing: 'easeInOutQuad',
2183
+ onComplete: () => {
2184
+ // 中途又切了一次:这次编排作废,收尾交给新的那次
2185
+ if (gen !== this._pipelineGen) {
2186
+ resolve();
2187
+ return;
2188
+ }
2189
+ this._applyRenderProfile({ colorPipeline: next });
2190
+ this.animator.to('style.fade', 1, {
2191
+ duration: d,
2192
+ easing: 'easeInOutQuad',
2193
+ onComplete: resolve,
2194
+ });
2195
+ },
2196
+ });
2197
+ });
2198
+ }
2199
+
2200
+ /** 当前颜色管线(本图层指定了就用本图层的,否则是模块级默认) */
2201
+ get colorPipeline(): ColorPipeline {
2202
+ return this._colorPipeline ?? getColorPipeline();
2203
+ }
2204
+
2205
+ /** 本图层此刻该用的纹理创建参数(图层的渲染 profile 就落在这里) */
2206
+ private _textureOptions(): TextureCreateOptions {
2207
+ return {
2208
+ // direct 管线的前提是贴图字节值不解码,两者必须一起切
2209
+ srgbDecode: this.colorPipeline !== 'direct',
2210
+ maxSize: this.options.maxTextureSize ?? Infinity,
2211
+ anisotropyCap: this.options.anisotropyCap ?? Infinity,
2212
+ };
2213
+ }
2214
+
2215
+ /**
2216
+ * 应用图层渲染 profile(颜色管线 / 各向异性上限)。
2217
+ *
2218
+ * `initial` = 构造期:只记状态并推给数据集管理器,不重建材质、不重传纹理
2219
+ * (那时还没有瓦片)。
2220
+ */
2221
+ private _applyRenderProfile(
2222
+ patch: { colorPipeline?: ColorPipeline; anisotropyCap?: number },
2223
+ initial = false
2224
+ ): void {
2225
+ const prev = this.colorPipeline;
2226
+ if (patch.colorPipeline !== undefined) {
2227
+ this._colorPipeline = normalizeColorPipeline(patch.colorPipeline) ?? 'linear';
2228
+ }
2229
+ if (patch.anisotropyCap !== undefined) this.options.anisotropyCap = patch.anisotropyCap;
2230
+
2231
+ const textureOptions = this._textureOptions();
2232
+ // 之后加载的瓦片按新参数走(已建好的数据集也要跟着换)
2233
+ this._datasetManager.setRenderProfile({ colorPipeline: this.colorPipeline, textureOptions });
2234
+ if (initial) return;
2235
+
2236
+ // 管线变了才需要换 program;只改各向异性时重建纯属浪费
2237
+ if (this.colorPipeline !== prev) this._rebuildStyledMaterials();
2238
+ this._syncCachedTextures(textureOptions);
2239
+ this._requestRepaint();
2240
+ }
2241
+
2242
+ /**
2243
+ * 把纹理解码方式 / 各向异性同步到**已上传**的纹理上。
2244
+ *
2245
+ * 两者都不需要重新解码:`colorSpace` 改了 + `needsUpdate` 让 three 重传一次像素,
2246
+ * `anisotropy` 同理。所以切管线不用重拉瓦片,代价只有一次纹理上传。
2247
+ */
2248
+ private _syncCachedTextures(opts: TextureCreateOptions): void {
2249
+ const want = opts.srgbDecode ? THREE.SRGBColorSpace : THREE.NoColorSpace;
2250
+ for (const entry of this.datasets.values()) {
2251
+ const cache = entry.tileset?.textureCache;
2252
+ if (!cache) continue;
2253
+ for (const tex of cache.values()) {
2254
+ if (tex.colorSpace === want) continue;
2255
+ tex.colorSpace = want;
2256
+ tex.needsUpdate = true;
2257
+ }
2258
+ applyAnisotropyCap(cache.values(), opts.anisotropyCap ?? Infinity);
2259
+ }
2101
2260
  }
2102
2261
 
2103
2262
  /** 按创建参数原位重建所有 styled 材质(materialQuality 切换用) */
@@ -2119,7 +2278,14 @@ export class S3MViewer {
2119
2278
  const old = raw as StyledMaterial | undefined;
2120
2279
  if (!old?.userData?.isStyled || !old.userData?.s3mOpts) continue;
2121
2280
  const opts = old.userData.s3mOpts as StyledMaterialOptions;
2122
- const fresh = createStyledMaterial({ ...opts, uniforms: this.uniforms });
2281
+ // 显式传本层档位与管线:s3mOpts 里记的是创建时的参数,可能是 undefined(跟随全局),
2282
+ // 重建时必须钉成本层当前值,否则「别处改了全局」会把这一层带跑。
2283
+ const fresh = createStyledMaterial({
2284
+ ...opts,
2285
+ uniforms: this.uniforms,
2286
+ materialQuality: this.materialQuality,
2287
+ colorPipeline: this.colorPipeline,
2288
+ });
2123
2289
  for (const key of PRESERVED) {
2124
2290
  const src = old.userData[key] as { value: unknown } | undefined;
2125
2291
  const dst = fresh.userData[key] as { value: unknown } | undefined;
@@ -2491,16 +2657,19 @@ export class S3MViewer {
2491
2657
  }
2492
2658
 
2493
2659
  /**
2494
- * 调整各向异性过滤上限(默认 Infinity = 取 GPU 最大值)。
2495
- * 只影响**之后创建**的纹理——已上传的纹理需要重新上传才会变,代价远大于收益,
2496
- * 所以切换档位请在建图层前决定,或接受「新瓦片按新档位、旧瓦片保持原样」。
2660
+ * 调整本图层的各向异性过滤上限(默认 Infinity = 取 GPU 最大值)。
2661
+ *
2662
+ * 与 0.4.22 之前不同:已上传的纹理**会立刻跟着变**(改 sampling 参数 + 重传,
2663
+ * 不需要重新解码,也不需要重载瓦片),所以切 `lightingPreset: 'supermap'` 时
2664
+ * 立面清晰度是当场变的,不再等新瓦片。
2497
2665
  */
2498
2666
  setAnisotropyCap(cap: number): void {
2499
2667
  const next = Number.isFinite(cap) && cap > 0 ? Math.max(1, cap) : Infinity;
2500
2668
  this.options.anisotropyCap = next;
2669
+ // 同步模块级默认:直接调 createTexture / buildTileObjects 的老用法仍读它
2501
2670
  const gpuMax = this.renderer?.capabilities.getMaxAnisotropy?.() ?? 1;
2502
2671
  setMaxAnisotropy(gpuMax, next);
2503
- this._requestRepaint();
2672
+ this._applyRenderProfile({ anisotropyCap: next });
2504
2673
  }
2505
2674
 
2506
2675
  /** 当前纹理长边像素上限。Infinity = 不限制 */
@@ -2511,12 +2680,16 @@ export class S3MViewer {
2511
2680
  /**
2512
2681
  * 调整纹理长边像素上限(默认 Infinity = 不限制)。
2513
2682
  * 只影响之后解码的纹理;非图集的 RGBA / WebP 产物按 2× 减半逼近上限。
2514
- * 注意这是**模块级**设置(图集 / 解码 Worker 共享),多 viewer 实例会互相覆盖。
2683
+ *
2684
+ * 图层级:只改本层,同页其它图层不受影响(同时同步模块级默认,照顾直接调
2685
+ * `createTexture` 的老用法)。已解码的纹理不会重解 —— 那是另一件事。
2515
2686
  */
2516
2687
  setMaxTextureSize(n: number): void {
2517
2688
  const next = Number.isFinite(n) && n >= 1 ? n : Infinity;
2518
2689
  this.options.maxTextureSize = next;
2519
2690
  setTextureSizeCap(next);
2691
+ // 只推给数据集,不动材质也不重传纹理(尺寸上限只影响「之后解码」的纹理)
2692
+ this._datasetManager.setRenderProfile({ textureOptions: this._textureOptions() });
2520
2693
  this._requestRepaint();
2521
2694
  }
2522
2695
 
@@ -272,13 +272,29 @@ export const DEFAULT_STYLE: S3MStyle = {
272
272
  export type MaterialQuality = 'pbr' | 'lambert';
273
273
 
274
274
  /**
275
- * 当前光照模型。模块级全局 —— 与 anisotropy / maxTextureSize 同一套设计,
276
- * 同页多个图层会互相覆盖,后创建的那个决定全局。只影响**之后创建**的材质。
275
+ * 模块级默认档位。**只是默认值**,不是全局开关:
276
+ * - `S3MViewer` / `S3MLayer` 构造时传了 `materialQuality` 的,只定自己那一层,
277
+ * 不读也不写这里(多图层同页互不干扰);
278
+ * - 没传的图层、以及直接调 `createStyledMaterial` / `buildTileObjects` 的场景,
279
+ * 仍然读这里 —— 老代码 `setMaterialQuality('lambert')` 一行改全场的用法照旧有效。
280
+ *
281
+ * 只影响**之后创建**的材质;已加载的瓦片要重载或在 viewer 上调
282
+ * `setMaterialQuality` 才会换。
277
283
  */
278
284
  let materialQuality: MaterialQuality = 'pbr';
279
285
 
280
286
  /**
281
- * 切换光照模型。
287
+ * 归一档位:只认 'pbr' / 'lambert',其余(含 undefined)返回 undefined,
288
+ * 表示「没指定,去读模块级默认」。
289
+ */
290
+ export function normalizeMaterialQuality(q: unknown): MaterialQuality | undefined {
291
+ if (q === 'lambert') return 'lambert';
292
+ if (q === 'pbr') return 'pbr';
293
+ return undefined;
294
+ }
295
+
296
+ /**
297
+ * 设置模块级默认档位(不是全局开关,见上面的 `materialQuality` 注释)。
282
298
  *
283
299
  * 'pbr'(默认)= `MeshStandardMaterial`,GGX + 环境球谐,质感最好也最贵。
284
300
  * 'lambert' = `MeshLambertMaterial`,只保留漫反射 + 平行光,片元开销约省一半;
@@ -301,7 +317,9 @@ export function getMaterialQuality(): MaterialQuality {
301
317
  * (0.8·NdotL + 0.7)、输出前抵消 renderer 的 OETF。与 `setSrgbTextureDecode(false)`
302
318
  * 配套使用,`lightingPreset: 'supermap'` 会同时打开两者。
303
319
  *
304
- * 与 materialQuality 同一套全局约定:只影响之后创建的材质,已加载瓦片要重载才换。
320
+ * 与 materialQuality 同一套约定:这里只是**模块级默认值**,不是全局开关 ——
321
+ * 图层显式传了 `colorPipeline` 就只认自己,没传才读这里。只影响之后创建的材质,
322
+ * 已加载瓦片要重载或在图层上调 `setColorPipeline` 才会换。
305
323
  */
306
324
  export type ColorPipeline = 'linear' | 'direct';
307
325
  let colorPipeline: ColorPipeline = 'linear';
@@ -314,6 +332,13 @@ export function getColorPipeline(): ColorPipeline {
314
332
  return colorPipeline;
315
333
  }
316
334
 
335
+ /** 归一颜色管线:只认 'linear' / 'direct',其余返回 undefined(= 去读模块级默认) */
336
+ export function normalizeColorPipeline(p: unknown): ColorPipeline | undefined {
337
+ if (p === 'direct') return 'direct';
338
+ if (p === 'linear') return 'linear';
339
+ return undefined;
340
+ }
341
+
317
342
  export type StyledMaterial = (THREE.MeshStandardMaterial | THREE.MeshLambertMaterial) & {
318
343
  userData: {
319
344
  uSelBatch: { value: number };
@@ -1105,6 +1130,18 @@ export interface StyledMaterialOptions {
1105
1130
  texCoordZ?: boolean;
1106
1131
  /** 图集材质的第二套纹理(官方 BakeTex,光照烘焙),与 base 相乘 */
1107
1132
  secMap?: THREE.Texture | null;
1133
+ /**
1134
+ * 光照模型档位。不传 = 读模块级默认(`getMaterialQuality()`),行为与旧版一致。
1135
+ *
1136
+ * 传了就按传的建 —— 图层级隔离靠这条:`S3MLayer` / `S3MViewer` 把自己的档位
1137
+ * 一路透传到这里,不再让所有图层共享同一份模块级状态。
1138
+ */
1139
+ materialQuality?: MaterialQuality;
1140
+ /**
1141
+ * 颜色管线。不传 = 读模块级默认(`getColorPipeline()`),行为与旧版一致。
1142
+ * 与 `materialQuality` 同一套:图层把自己的 profile 透传进来,同页互不污染。
1143
+ */
1144
+ colorPipeline?: ColorPipeline;
1108
1145
  }
1109
1146
 
1110
1147
  /**
@@ -1152,6 +1189,9 @@ const FRAG_SURFACE = /* glsl */ `
1152
1189
 
1153
1190
  export function createStyledMaterial(opts: StyledMaterialOptions): StyledMaterial {
1154
1191
  const uniforms = opts.uniforms;
1192
+ // 档位解析:显式传参 > 模块级默认。图层级隔离就靠这一行 —— 各图层把自己的
1193
+ // 档位透传进来,不再共享同一份模块级状态。
1194
+ const quality: MaterialQuality = normalizeMaterialQuality(opts.materialQuality) ?? getMaterialQuality();
1155
1195
  // 两种光照模型共用的参数。注入的 chunk 只动 diffuseColor / normal / transformed,
1156
1196
  // 在 Standard 与 Lambert 的 shader 里都存在同名 include,所以模板不用分支。
1157
1197
  const baseParams = {
@@ -1170,7 +1210,7 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
1170
1210
  alphaTest: opts.alphaTest ?? 0,
1171
1211
  };
1172
1212
  const mat = (
1173
- materialQuality === 'lambert'
1213
+ quality === 'lambert'
1174
1214
  ? new THREE.MeshLambertMaterial(baseParams)
1175
1215
  : new THREE.MeshStandardMaterial({
1176
1216
  ...baseParams,
@@ -1204,7 +1244,7 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
1204
1244
  if (texCoordZ) mat.defines = { ...(mat.defines as Record<string, string>), S3M_TEXCOORD_Z: '' };
1205
1245
  if (secMap) mat.defines = { ...mat.defines, S3M_ATLAS_SEC: '' };
1206
1246
  // supermap 完整版:官方 sRGB 直乘管线(配合 setSrgbTextureDecode(false))
1207
- const direct = colorPipeline === 'direct';
1247
+ const direct = (opts.colorPipeline ?? colorPipeline) === 'direct';
1208
1248
  if (direct) mat.defines = { ...mat.defines, S3M_DIRECT_COLOR: '' };
1209
1249
  // 官方 uTextureDim = (base宽, base高, bake宽, bake高),取实际绑定纹理的像素尺寸
1210
1250
  const uAtlasDim = { value: new THREE.Vector2(opts.map?.image?.width || 1, opts.map?.image?.height || 1) };
@@ -1311,6 +1351,8 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
1311
1351
  mat.userData.uFlatShading = uFlatShading;
1312
1352
  // 记录创建参数:materialQuality 运行时切换时按原参数重建材质(换类不换外观)
1313
1353
  mat.userData.s3mOpts = opts;
1354
+ // 记录实际生效的档位:重建 / 排查时不用再猜当时读的是哪一层设置
1355
+ mat.userData.s3mQuality = quality;
1314
1356
  mat.userData.isStyled = true;
1315
1357
  return mat;
1316
1358
  }
@@ -31,6 +31,43 @@ export function setMaxAnisotropy(n: number, cap = Infinity): void {
31
31
  anisotropyCap = Number.isFinite(cap) && cap > 0 ? Math.max(1, cap) : Infinity;
32
32
  }
33
33
 
34
+ /**
35
+ * 单次创建的纹理参数。三项都可缺省 —— 缺省时读模块级默认(`setSrgbTextureDecode`
36
+ * / `setMaxTextureSize` / `setAnisotropyCap`),所以老调用方行为逐字不变。
37
+ *
38
+ * 图层级隔离靠这条:`S3MLayer` 把自己的渲染 profile 透传进来,不再共享模块级状态。
39
+ */
40
+ export interface TextureCreateOptions {
41
+ /** 是否做 sRGB 解码(默认读 `getSrgbTextureDecode()`)。`'direct'` 管线传 false */
42
+ srgbDecode?: boolean;
43
+ /** 长边像素上限(默认读 `setMaxTextureSize` 的值)。图集恒不缩放 */
44
+ maxSize?: number;
45
+ /** 各向异性上限(默认读 `setAnisotropyCap` 的值)。Infinity = 取 GPU 最大值 */
46
+ anisotropyCap?: number;
47
+ }
48
+
49
+ /** 把「上限」解析成实际的 anisotropy 值(GPU 最大值与 cap 取小) */
50
+ function resolveAnisotropy(cap: number): number {
51
+ return Number.isFinite(cap) && cap > 0 ? Math.min(maxAnisotropy, Math.max(1, cap)) : maxAnisotropy;
52
+ }
53
+
54
+ /**
55
+ * 把各向异性上限**即时应用**到一批已上传的纹理。
56
+ *
57
+ * `anisotropy` 是采样参数,不需要重新解码 —— 改值 + `needsUpdate` 让 three 重传一次
58
+ * 即可,代价远小于重载瓦片。图集纹理(`configureAtlas` 打过 `s3mNoAniso` 标记)
59
+ * 保持 1:它关了 mip、靠子区域 rect 自己算层级,开各向异性会把相邻子区域混进来。
60
+ */
61
+ export function applyAnisotropyCap(textures: Iterable<THREE.Texture>, cap: number): void {
62
+ const n = resolveAnisotropy(cap);
63
+ for (const tex of textures) {
64
+ if (!tex || (tex.userData as { s3mNoAniso?: boolean })?.s3mNoAniso) continue;
65
+ if (tex.anisotropy === n) continue;
66
+ tex.anisotropy = n;
67
+ tex.needsUpdate = true;
68
+ }
69
+ }
70
+
34
71
  /**
35
72
  * 单独调各向异性上限(只影响之后创建的纹理)。
36
73
  * supermap 预设会设成 1 —— 官方 iClient3D 不开各向异性,斜视角直接采高 mip
@@ -114,15 +151,15 @@ export function getSrgbTextureDecode(): boolean {
114
151
  return srgbTextureDecode;
115
152
  }
116
153
 
117
- function configure<T extends THREE.Texture>(tex: T, srgb = true): T {
154
+ function configure<T extends THREE.Texture>(tex: T, srgb = true, opts: TextureCreateOptions = {}): T {
118
155
  tex.wrapS = THREE.RepeatWrapping;
119
156
  tex.wrapT = THREE.RepeatWrapping;
120
157
  tex.magFilter = THREE.LinearFilter;
121
158
  tex.minFilter = THREE.LinearMipmapLinearFilter;
122
159
  tex.generateMipmaps = true;
123
160
  // 各向异性过滤对斜视角立面清晰度的提升比提高分辨率更直接
124
- tex.anisotropy = anisotropyCap === Infinity ? maxAnisotropy : Math.min(maxAnisotropy, anisotropyCap);
125
- if (srgb && srgbTextureDecode) tex.colorSpace = THREE.SRGBColorSpace;
161
+ tex.anisotropy = resolveAnisotropy(opts.anisotropyCap ?? anisotropyCap);
162
+ if (srgb && (opts.srgbDecode ?? srgbTextureDecode)) tex.colorSpace = THREE.SRGBColorSpace;
126
163
  tex.flipY = false;
127
164
  tex.needsUpdate = true;
128
165
  return tex;
@@ -136,18 +173,23 @@ function configure<T extends THREE.Texture>(tex: T, srgb = true): T {
136
173
  * 自动选的层级会把相邻子区域混进来,远处就是一糊串色 —— 实测照抄公式但留着 GPU mip
137
174
  * 时,画面反而比不改更糟,就是因为这两个变量叠在一起了。
138
175
  */
139
- function configureAtlas<T extends THREE.Texture>(tex: T): T {
140
- configure(tex);
176
+ function configureAtlas<T extends THREE.Texture>(tex: T, opts: TextureCreateOptions = {}): T {
177
+ configure(tex, true, opts);
141
178
  tex.wrapS = THREE.ClampToEdgeWrapping;
142
179
  tex.wrapT = THREE.ClampToEdgeWrapping;
143
180
  tex.minFilter = THREE.LinearFilter;
144
181
  tex.generateMipmaps = false;
145
182
  tex.anisotropy = 1;
183
+ // 标记给 applyAnisotropyCap:图集不吃各向异性上限(见该函数注释)
184
+ (tex.userData as { s3mNoAniso?: boolean }).s3mNoAniso = true;
146
185
  tex.needsUpdate = true;
147
186
  return tex;
148
187
  }
149
188
 
150
- export async function createTexture(tex: S3MTexture | null | undefined): Promise<DecodedTexture> {
189
+ export async function createTexture(
190
+ tex: S3MTexture | null | undefined,
191
+ opts: TextureCreateOptions = {}
192
+ ): Promise<DecodedTexture> {
151
193
  if (!tex || !tex.width || !tex.height) return { texture: null, decoded: 'empty' };
152
194
 
153
195
  // 图集纹理(flipY 的原始 DXT 图集 / AtlasPack 拼出来的 RGBA 大图 / leaf 组合画布)都必须
@@ -157,7 +199,7 @@ export async function createTexture(tex: S3MTexture | null | undefined): Promise
157
199
  // 压缩直传只在「宿主 GL 支持 + 非图集」时开:图集要 flipY / 无 mip,压缩上传做不了翻行
158
200
  const wantCompressed = compressedTexturesSupported && !isAtlas;
159
201
  // 尺寸上限同样跳过图集:UV 已重映射到子区域,缩放会让整体错位
160
- const sizeCap = isAtlas ? Infinity : maxTextureSize;
202
+ const sizeCap = isAtlas ? Infinity : (opts.maxSize ?? maxTextureSize);
161
203
  const data = await decodeTexture(tex, wantCompressed, sizeCap);
162
204
 
163
205
  if (!data.rgba && !data.blocks && !data.bitmap) {
@@ -194,7 +236,7 @@ export async function createTexture(tex: S3MTexture | null | undefined): Promise
194
236
  texture = dt;
195
237
  }
196
238
  texture.needsUpdate = true;
197
- const configured = isAtlas ? configureAtlas(texture) : configure(texture);
239
+ const configured = isAtlas ? configureAtlas(texture, opts) : configure(texture, true, opts);
198
240
  if (data.blocks) {
199
241
  configured.generateMipmaps = false;
200
242
  // 带链 → 三线性取链上高层级(官方同款平均效果);纯 level0 才退回 Linear 兜底
@@ -4,17 +4,16 @@
4
4
  * 从 `S3MViewer` 里整体搬出来的第一块 —— 它只依赖 `scene` / `renderer` / 共享
5
5
  * uniform / 宿主方位角,不碰几何、瓦片、拾取,是耦合最低的一块。
6
6
  *
7
- * ## 两个模块级全局态
7
+ * ## 颜色管线是图层级的
8
8
  *
9
- * `setColorPipeline` / `setSrgbTextureDecode` / `setAnisotropyCap` 都是模块级设置,
10
- * 影响**之后创建**的材质与纹理。所以切预设时已加载的瓦片要重载才换观感,
11
- * 这是已知的架构债(一个页面两个 viewer 用不同预设会互相污染),
12
- * 拆分没有改变这一点。
9
+ * 颜色管线 / 纹理解码 / 各向异性三者一起决定「官方 supermap 观感」,都由
10
+ * `ctx.applyRenderProfile` 落到本图层(见 `S3MViewer._applyRenderProfile`),
11
+ * 不再写模块级全局 —— 同页两个 viewer 用不同预设互不影响,已加载瓦片也会
12
+ * 当场重建材质、重传纹理,不用重载。
13
13
  */
14
14
  import * as THREE from 'three';
15
15
  import { resolveLightingPreset, type LightingPreset } from '../lightingPreset.js';
16
- import { setColorPipeline } from '../StyleMaterial.js';
17
- import { setAnisotropyCap, setSrgbTextureDecode } from '../TextureFactory.js';
16
+ import type { ColorPipeline } from '../StyleMaterial.js';
18
17
  import type { LightOptions, S3MViewerOptions } from '../S3MViewer.js';
19
18
  import type { StyleUniforms } from '../../core/types.js';
20
19
 
@@ -49,6 +48,14 @@ export interface LightRigContext {
49
48
  getBearing?: () => number | undefined;
50
49
  /** 宿主需要重画的信号 */
51
50
  repaint: () => void;
51
+ /**
52
+ * 应用图层级渲染 profile(颜色管线 + 各向异性)。
53
+ *
54
+ * `initial = true` 是构造期:那时还没有瓦片与材质,落地方只记状态即可。
55
+ * 换掉以前的 `setColorPipeline` / `setSrgbTextureDecode` / `setAnisotropyCap`
56
+ * 三个模块级全局调用 —— 它们会让同页多图层互相污染。
57
+ */
58
+ applyRenderProfile: (p: { colorPipeline: ColorPipeline; anisotropyCap: number }, initial?: boolean) => void;
52
59
  }
53
60
 
54
61
  export class LightRig {
@@ -74,7 +81,7 @@ export class LightRig {
74
81
  const options = ctx.options;
75
82
  if (options.lightingPreset && options.lightingPreset !== 'default') {
76
83
  Object.assign(this._state, resolveLightingPreset(options.lightingPreset));
77
- this.applyColorPipeline(options.lightingPreset);
84
+ this.applyColorPipeline(options.lightingPreset, true);
78
85
  }
79
86
  if (options.light) Object.assign(this._state, options.light);
80
87
  }
@@ -155,15 +162,18 @@ export class LightRig {
155
162
  /**
156
163
  * supermap 完整版 = 光照强度 + 颜色管线一起切:
157
164
  * direct 管线下纹理不做 sRGB 解码、片元按官方常数 Lambert 直乘(见 lightingPreset.ts)。
158
- * 两个都是模块级全局态,只影响之后创建的材质 / 纹理 —— 已加载瓦片要重载才换。
165
+ *
166
+ * 这三样(管线 / 解码 / 各向异性)以前都是模块级全局,同页两个图层用不同预设
167
+ * 会互相污染;现在交给 `ctx.applyRenderProfile`,由图层自己持有并透传给材质与纹理。
159
168
  */
160
- applyColorPipeline(preset: LightingPreset): void {
169
+ applyColorPipeline(preset: LightingPreset, initial = false): void {
161
170
  const direct = preset === 'supermap';
162
- setColorPipeline(direct ? 'direct' : 'linear');
163
- setSrgbTextureDecode(!direct);
164
171
  // 官方不开各向异性:远处直接采高 mip 平均成纯色("灰色滤镜"的一部分)。
165
172
  // 我们默认 Infinity(倾斜摄影立面清晰度优先),supermap 模式必须收回。
166
- setAnisotropyCap(direct ? 1 : Infinity);
173
+ this.ctx.applyRenderProfile(
174
+ { colorPipeline: direct ? 'direct' : 'linear', anisotropyCap: direct ? 1 : Infinity },
175
+ initial
176
+ );
167
177
  }
168
178
 
169
179
  setLightingPreset(preset: LightingPreset): void {
@@ -168,10 +168,24 @@ export interface S3MViewerOptions {
168
168
  * - `'lambert'`:`MeshLambertMaterial`,只保留漫反射 + 平行光,片元开销约省一半;
169
169
  * 白模 / 纯色 / 按高度着色这类不需要高光的模式肉眼差别很小
170
170
  *
171
- * 模块级全局状态:同页多个图层会互相覆盖,后创建的那个决定全局。
172
- * 只影响**之后创建**的材质 —— 已加载的瓦片要重新加载才会换。
171
+ * **图层级**:只定这一层,同页多个图层各用各的,互不覆盖;构造时不会写模块级
172
+ * 默认(`setMaterialQuality`)。不传 = 跟随模块级默认,老用法照旧有效。
173
+ * 只影响**之后创建**的材质 —— 已加载的瓦片要重载,或在图层上调
174
+ * `setMaterialQuality()` 原位重建。
173
175
  */
174
176
  materialQuality?: 'pbr' | 'lambert';
177
+ /**
178
+ * 颜色管线(默认 `'linear'`)。
179
+ *
180
+ * - `'linear'`:纹理 sRGB 解码 → 线性 PBR 光照 → linear→sRGB 输出(three 标准姿势)
181
+ * - `'direct'`:官方 iClient3D 的 sRGB 直乘管线,同时把纹理解码关掉(字节值原样进
182
+ * shader)。`lightingPreset: 'supermap'` 会选它
183
+ *
184
+ * **图层级**:只定这一层,同页多图层各用各的;不传 = 跟随模块级默认。
185
+ * 已加载瓦片可以在运行时用 `setColorPipeline(q, { duration })` 平滑切过去
186
+ * (重建材质 + 重传纹理,不重新下载瓦片)。
187
+ */
188
+ colorPipeline?: 'linear' | 'direct';
175
189
  /** 自定义瓦片构建器(测试 / 服务端渲染时注入) */
176
190
  buildTile?: BuildTileFn | null;
177
191
  /**