s3m-three 0.4.22 → 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 +13 -0
  4. package/dist/render/DatasetManager.d.ts.map +1 -1
  5. package/dist/render/DatasetManager.js +20 -0
  6. package/dist/render/DatasetManager.js.map +1 -1
  7. package/dist/render/GeometryBuilder.d.ts +11 -0
  8. package/dist/render/GeometryBuilder.d.ts.map +1 -1
  9. package/dist/render/GeometryBuilder.js +2 -1
  10. package/dist/render/GeometryBuilder.js.map +1 -1
  11. package/dist/render/S3MViewer.d.ts +53 -5
  12. package/dist/render/S3MViewer.d.ts.map +1 -1
  13. package/dist/render/S3MViewer.js +157 -12
  14. package/dist/render/S3MViewer.js.map +1 -1
  15. package/dist/render/StyleMaterial.d.ts +10 -1
  16. package/dist/render/StyleMaterial.d.ts.map +1 -1
  17. package/dist/render/StyleMaterial.js +9 -1
  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 +12 -0
  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 +19 -0
  31. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  32. package/dist/tileset/S3MTileset.js +16 -0
  33. package/dist/tileset/S3MTileset.js.map +1 -1
  34. package/docs/API.md +24 -3
  35. package/package.json +1 -1
  36. package/src/render/DatasetManager.ts +21 -0
  37. package/src/render/GeometryBuilder.ts +13 -1
  38. package/src/render/S3MViewer.ts +168 -12
  39. package/src/render/StyleMaterial.ts +16 -2
  40. package/src/render/TextureFactory.ts +50 -8
  41. package/src/render/viewer/LightRig.ts +23 -13
  42. package/src/render/viewer/types.ts +12 -0
  43. package/src/tileset/S3MTileset.ts +24 -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, normalizeMaterialQuality } 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
 
@@ -257,6 +271,17 @@ export class S3MViewer {
257
271
  * 传了值就只认自己,多图层同页各用各的,互不覆盖。
258
272
  */
259
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;
260
285
  /** 单体样式、专题着色与状态纹理管理器 */
261
286
  private readonly _featureStyleManager: FeatureStyleManager;
262
287
  /** GPU 路径动画管理器 */
@@ -395,7 +420,12 @@ export class S3MViewer {
395
420
  );
396
421
 
397
422
  this.uniforms = createSharedUniforms();
398
- // 光源状态在这里就位:LightRig 构造时会按 lightingPreset 切模块级颜色管线,
423
+ // 图层 profile 先落位(显式传了就只认自己),再把 LightRig 挂上来 ——
424
+ // 它构造时会按 lightingPreset 覆盖一次管线,这一步走的是图层级通道,
425
+ // 不再去改模块级全局(以前三个全局一起被改写,同页多图层互相污染)。
426
+ this._colorPipeline = normalizeColorPipeline(options.colorPipeline);
427
+ this._applyRenderProfile({}, true);
428
+ // 光源状态在这里就位:LightRig 构造时会按 lightingPreset 切图层颜色管线,
399
429
  // 挂进场景要等 scene 建好(构造函数上面已经建了)。
400
430
  this._lights = new LightRig({
401
431
  scene: () => this.scene,
@@ -406,6 +436,8 @@ export class S3MViewer {
406
436
  // 走公共 triggerRepaint 而不是 _requestRepaint:宿主 / 单测会覆盖前者来观察重绘,
407
437
  // 绕过它会让「有没有触发重绘」这件事失去观测点
408
438
  repaint: () => this.triggerRepaint(),
439
+ // 构造期(initial)只记状态:那时还没有材质可重建、没有数据集可同步
440
+ applyRenderProfile: (p, initial) => this._applyRenderProfile(p, initial),
409
441
  });
410
442
  this._lights.addToScene();
411
443
  this._heights = new HeightIndex(this.uniforms);
@@ -1855,7 +1887,13 @@ export class S3MViewer {
1855
1887
  this.animator.register(`style.${key}`, {
1856
1888
  dim: 1,
1857
1889
  read: (out) => {
1858
- 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
+ );
1859
1897
  },
1860
1898
  write: (v) => {
1861
1899
  (this.style as unknown as Record<string, number>)[key] = v[0];
@@ -2115,6 +2153,112 @@ export class S3MViewer {
2115
2153
  return this._materialQuality ?? getMaterialQuality();
2116
2154
  }
2117
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
+ }
2260
+ }
2261
+
2118
2262
  /** 按创建参数原位重建所有 styled 材质(materialQuality 切换用) */
2119
2263
  private _rebuildStyledMaterials(): void {
2120
2264
  // 逐材质 own uniform 的状态要在重建时搬过去,否则选中高亮 / 瓦片淡入会闪没
@@ -2134,9 +2278,14 @@ export class S3MViewer {
2134
2278
  const old = raw as StyledMaterial | undefined;
2135
2279
  if (!old?.userData?.isStyled || !old.userData?.s3mOpts) continue;
2136
2280
  const opts = old.userData.s3mOpts as StyledMaterialOptions;
2137
- // 显式传本层档位:s3mOpts 里记的是创建时的参数,可能是 undefined(跟随全局),
2138
- // 重建时必须钉成本层当前档位,否则「别处改了全局」会把这一层带跑。
2139
- const fresh = createStyledMaterial({ ...opts, uniforms: this.uniforms, materialQuality: this.materialQuality });
2281
+ // 显式传本层档位与管线:s3mOpts 里记的是创建时的参数,可能是 undefined(跟随全局),
2282
+ // 重建时必须钉成本层当前值,否则「别处改了全局」会把这一层带跑。
2283
+ const fresh = createStyledMaterial({
2284
+ ...opts,
2285
+ uniforms: this.uniforms,
2286
+ materialQuality: this.materialQuality,
2287
+ colorPipeline: this.colorPipeline,
2288
+ });
2140
2289
  for (const key of PRESERVED) {
2141
2290
  const src = old.userData[key] as { value: unknown } | undefined;
2142
2291
  const dst = fresh.userData[key] as { value: unknown } | undefined;
@@ -2508,16 +2657,19 @@ export class S3MViewer {
2508
2657
  }
2509
2658
 
2510
2659
  /**
2511
- * 调整各向异性过滤上限(默认 Infinity = 取 GPU 最大值)。
2512
- * 只影响**之后创建**的纹理——已上传的纹理需要重新上传才会变,代价远大于收益,
2513
- * 所以切换档位请在建图层前决定,或接受「新瓦片按新档位、旧瓦片保持原样」。
2660
+ * 调整本图层的各向异性过滤上限(默认 Infinity = 取 GPU 最大值)。
2661
+ *
2662
+ * 与 0.4.22 之前不同:已上传的纹理**会立刻跟着变**(改 sampling 参数 + 重传,
2663
+ * 不需要重新解码,也不需要重载瓦片),所以切 `lightingPreset: 'supermap'` 时
2664
+ * 立面清晰度是当场变的,不再等新瓦片。
2514
2665
  */
2515
2666
  setAnisotropyCap(cap: number): void {
2516
2667
  const next = Number.isFinite(cap) && cap > 0 ? Math.max(1, cap) : Infinity;
2517
2668
  this.options.anisotropyCap = next;
2669
+ // 同步模块级默认:直接调 createTexture / buildTileObjects 的老用法仍读它
2518
2670
  const gpuMax = this.renderer?.capabilities.getMaxAnisotropy?.() ?? 1;
2519
2671
  setMaxAnisotropy(gpuMax, next);
2520
- this._requestRepaint();
2672
+ this._applyRenderProfile({ anisotropyCap: next });
2521
2673
  }
2522
2674
 
2523
2675
  /** 当前纹理长边像素上限。Infinity = 不限制 */
@@ -2528,12 +2680,16 @@ export class S3MViewer {
2528
2680
  /**
2529
2681
  * 调整纹理长边像素上限(默认 Infinity = 不限制)。
2530
2682
  * 只影响之后解码的纹理;非图集的 RGBA / WebP 产物按 2× 减半逼近上限。
2531
- * 注意这是**模块级**设置(图集 / 解码 Worker 共享),多 viewer 实例会互相覆盖。
2683
+ *
2684
+ * 图层级:只改本层,同页其它图层不受影响(同时同步模块级默认,照顾直接调
2685
+ * `createTexture` 的老用法)。已解码的纹理不会重解 —— 那是另一件事。
2532
2686
  */
2533
2687
  setMaxTextureSize(n: number): void {
2534
2688
  const next = Number.isFinite(n) && n >= 1 ? n : Infinity;
2535
2689
  this.options.maxTextureSize = next;
2536
2690
  setTextureSizeCap(next);
2691
+ // 只推给数据集,不动材质也不重传纹理(尺寸上限只影响「之后解码」的纹理)
2692
+ this._datasetManager.setRenderProfile({ textureOptions: this._textureOptions() });
2537
2693
  this._requestRepaint();
2538
2694
  }
2539
2695
 
@@ -317,7 +317,9 @@ export function getMaterialQuality(): MaterialQuality {
317
317
  * (0.8·NdotL + 0.7)、输出前抵消 renderer 的 OETF。与 `setSrgbTextureDecode(false)`
318
318
  * 配套使用,`lightingPreset: 'supermap'` 会同时打开两者。
319
319
  *
320
- * 与 materialQuality 同一套全局约定:只影响之后创建的材质,已加载瓦片要重载才换。
320
+ * 与 materialQuality 同一套约定:这里只是**模块级默认值**,不是全局开关 ——
321
+ * 图层显式传了 `colorPipeline` 就只认自己,没传才读这里。只影响之后创建的材质,
322
+ * 已加载瓦片要重载或在图层上调 `setColorPipeline` 才会换。
321
323
  */
322
324
  export type ColorPipeline = 'linear' | 'direct';
323
325
  let colorPipeline: ColorPipeline = 'linear';
@@ -330,6 +332,13 @@ export function getColorPipeline(): ColorPipeline {
330
332
  return colorPipeline;
331
333
  }
332
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
+
333
342
  export type StyledMaterial = (THREE.MeshStandardMaterial | THREE.MeshLambertMaterial) & {
334
343
  userData: {
335
344
  uSelBatch: { value: number };
@@ -1128,6 +1137,11 @@ export interface StyledMaterialOptions {
1128
1137
  * 一路透传到这里,不再让所有图层共享同一份模块级状态。
1129
1138
  */
1130
1139
  materialQuality?: MaterialQuality;
1140
+ /**
1141
+ * 颜色管线。不传 = 读模块级默认(`getColorPipeline()`),行为与旧版一致。
1142
+ * 与 `materialQuality` 同一套:图层把自己的 profile 透传进来,同页互不污染。
1143
+ */
1144
+ colorPipeline?: ColorPipeline;
1131
1145
  }
1132
1146
 
1133
1147
  /**
@@ -1230,7 +1244,7 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
1230
1244
  if (texCoordZ) mat.defines = { ...(mat.defines as Record<string, string>), S3M_TEXCOORD_Z: '' };
1231
1245
  if (secMap) mat.defines = { ...mat.defines, S3M_ATLAS_SEC: '' };
1232
1246
  // supermap 完整版:官方 sRGB 直乘管线(配合 setSrgbTextureDecode(false))
1233
- const direct = colorPipeline === 'direct';
1247
+ const direct = (opts.colorPipeline ?? colorPipeline) === 'direct';
1234
1248
  if (direct) mat.defines = { ...mat.defines, S3M_DIRECT_COLOR: '' };
1235
1249
  // 官方 uTextureDim = (base宽, base高, bake宽, bake高),取实际绑定纹理的像素尺寸
1236
1250
  const uAtlasDim = { value: new THREE.Vector2(opts.map?.image?.width || 1, opts.map?.image?.height || 1) };
@@ -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 {
@@ -174,6 +174,18 @@ export interface S3MViewerOptions {
174
174
  * `setMaterialQuality()` 原位重建。
175
175
  */
176
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';
177
189
  /** 自定义瓦片构建器(测试 / 服务端渲染时注入) */
178
190
  buildTile?: BuildTileFn | null;
179
191
  /**
@@ -32,6 +32,7 @@ import type {
32
32
  } from '../core/S3MBlockParser.js';
33
33
  import type { Anchor, ParsedTile, Stats, StyleUniforms } from '../core/types.js';
34
34
  import type { TileGeometryData } from '../core/TileGeometry.js';
35
+ import type { TextureCreateOptions } from '../render/TextureFactory.js';
35
36
  import type { Group, Texture } from 'three';
36
37
 
37
38
  function dirOf(url: string): string {
@@ -248,6 +249,10 @@ export type BuildTileFn = (
248
249
  polygonOffset?: { factor: number; units: number };
249
250
  /** 光照模型档位(透传给 buildTileObjects),见 BuildTileOptions.materialQuality */
250
251
  materialQuality?: 'pbr' | 'lambert';
252
+ /** 颜色管线(透传给 buildTileObjects),见 BuildTileOptions.colorPipeline */
253
+ colorPipeline?: 'linear' | 'direct';
254
+ /** 纹理创建参数(透传给 buildTileObjects → createTexture) */
255
+ textureOptions?: TextureCreateOptions;
251
256
  }
252
257
  ) => Promise<{ group: Group; stats: Partial<Stats> }>;
253
258
 
@@ -265,6 +270,10 @@ export interface S3MTilesetOptions {
265
270
  * 由 DatasetManager 从图层 / 数据集设置带下来。
266
271
  */
267
272
  materialQuality?: 'pbr' | 'lambert';
273
+ /** 颜色管线,透传给 buildTileObjects。不传 = 读模块级默认。由图层的渲染 profile 带下来 */
274
+ colorPipeline?: 'linear' | 'direct';
275
+ /** 纹理创建参数(sRGB 解码 / 尺寸上限 / 各向异性上限),透传给 createTexture */
276
+ textureOptions?: TextureCreateOptions;
268
277
  /**
269
278
  * 是否用 Web Worker 解析经典 `.s3mb` 瓦片(默认开)。
270
279
  *
@@ -479,6 +488,8 @@ export class S3MTileset {
479
488
  private _buildTile: BuildTileFn | null;
480
489
  private readonly _polygonOffset: { factor: number; units: number } | undefined;
481
490
  private _materialQuality: 'pbr' | 'lambert' | undefined;
491
+ private _colorPipeline: 'linear' | 'direct' | undefined;
492
+ private _textureOptions: TextureCreateOptions | undefined;
482
493
  private readonly _uniforms: StyleUniforms | null;
483
494
  /** 经典 .s3mb 的 Worker 解析客户端;undefined = 尚未尝试创建,null = 不可用/已降级 */
484
495
  private _workerClient: S3MWorkerClient | null | undefined; private readonly _useWorker: boolean;
@@ -500,6 +511,8 @@ export class S3MTileset {
500
511
  this._buildTile = options.buildTile ?? null;
501
512
  this._polygonOffset = options.polygonOffset;
502
513
  this._materialQuality = options.materialQuality;
514
+ this._colorPipeline = options.colorPipeline;
515
+ this._textureOptions = options.textureOptions;
503
516
  // 着色器共享 uniform,由 S3MLayer 持有并传进来
504
517
  this._uniforms = options.uniforms ?? null;
505
518
  this._useWorker = options.useWorker !== false;
@@ -740,6 +753,15 @@ export class S3MTileset {
740
753
  this._materialQuality = q;
741
754
  }
742
755
 
756
+ /**
757
+ * 运行时改本数据集的渲染 profile(颜色管线 / 纹理参数)。
758
+ * 只影响**之后**构建的瓦片材质与纹理;场上已有的由 `S3MViewer` 统一重建 / 重传。
759
+ */
760
+ setRenderProfile(p: { colorPipeline?: 'linear' | 'direct'; textureOptions?: TextureCreateOptions }): void {
761
+ if (p.colorPipeline !== undefined) this._colorPipeline = p.colorPipeline;
762
+ if (p.textureOptions !== undefined) this._textureOptions = p.textureOptions;
763
+ }
764
+
743
765
  /**
744
766
  * 当前数据集驻留的显存与内存总字节数(几何缓冲精确大小 + 纹理显存估计)
745
767
  */
@@ -1264,6 +1286,8 @@ export class S3MTileset {
1264
1286
  assembled,
1265
1287
  polygonOffset: this._polygonOffset,
1266
1288
  materialQuality: this._materialQuality,
1289
+ colorPipeline: this._colorPipeline,
1290
+ textureOptions: this._textureOptions,
1267
1291
  });
1268
1292
  const { group, stats } = buildRes;
1269
1293
  const texKeys = (buildRes as any).textureKeys ?? (group.userData?.textureKeys as string[] | undefined) ?? Object.keys(tile.textures ?? {});