s3m-three 0.3.6 → 0.3.8

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 (92) hide show
  1. package/dist/anim/Animator.d.ts +111 -0
  2. package/dist/anim/Animator.d.ts.map +1 -0
  3. package/dist/anim/Animator.js +382 -0
  4. package/dist/anim/Animator.js.map +1 -0
  5. package/dist/anim/Path.d.ts +30 -0
  6. package/dist/anim/Path.d.ts.map +1 -0
  7. package/dist/anim/Path.js +100 -0
  8. package/dist/anim/Path.js.map +1 -0
  9. package/dist/anim/easing.d.ts +70 -0
  10. package/dist/anim/easing.d.ts.map +1 -0
  11. package/dist/anim/easing.js +170 -0
  12. package/dist/anim/easing.js.map +1 -0
  13. package/dist/anim/index.d.ts +6 -0
  14. package/dist/anim/index.d.ts.map +1 -0
  15. package/dist/anim/index.js +4 -0
  16. package/dist/anim/index.js.map +1 -0
  17. package/dist/browser/assets/{s3mWorker-hZY5KqWF.js → s3mWorker-D7AV6O30.js} +1 -1
  18. package/dist/browser/s3m-three.iife.ext-three.js +60 -22
  19. package/dist/browser/s3m-three.iife.js +60 -22
  20. package/dist/core/TextureDecode.d.ts +22 -1
  21. package/dist/core/TextureDecode.d.ts.map +1 -1
  22. package/dist/core/TextureDecode.js +47 -3
  23. package/dist/core/TextureDecode.js.map +1 -1
  24. package/dist/core/TileGeometry.d.ts +92 -0
  25. package/dist/core/TileGeometry.d.ts.map +1 -0
  26. package/dist/core/TileGeometry.js +607 -0
  27. package/dist/core/TileGeometry.js.map +1 -0
  28. package/dist/core/types.d.ts +16 -0
  29. package/dist/core/types.d.ts.map +1 -1
  30. package/dist/index.d.ts +3 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/render/EdgeLines.d.ts +62 -14
  35. package/dist/render/EdgeLines.d.ts.map +1 -1
  36. package/dist/render/EdgeLines.js +97 -8
  37. package/dist/render/EdgeLines.js.map +1 -1
  38. package/dist/render/FeatureStyleManager.d.ts +17 -1
  39. package/dist/render/FeatureStyleManager.d.ts.map +1 -1
  40. package/dist/render/FeatureStyleManager.js +14 -4
  41. package/dist/render/FeatureStyleManager.js.map +1 -1
  42. package/dist/render/GeometryBuilder.d.ts +8 -10
  43. package/dist/render/GeometryBuilder.d.ts.map +1 -1
  44. package/dist/render/GeometryBuilder.js +77 -555
  45. package/dist/render/GeometryBuilder.js.map +1 -1
  46. package/dist/render/MergeVertices.d.ts +7 -0
  47. package/dist/render/MergeVertices.d.ts.map +1 -1
  48. package/dist/render/MergeVertices.js +10 -1
  49. package/dist/render/MergeVertices.js.map +1 -1
  50. package/dist/render/S3MViewer.d.ts +218 -9
  51. package/dist/render/S3MViewer.d.ts.map +1 -1
  52. package/dist/render/S3MViewer.js +772 -68
  53. package/dist/render/S3MViewer.js.map +1 -1
  54. package/dist/render/StyleMaterial.d.ts +19 -0
  55. package/dist/render/StyleMaterial.d.ts.map +1 -1
  56. package/dist/render/StyleMaterial.js +37 -0
  57. package/dist/render/StyleMaterial.js.map +1 -1
  58. package/dist/render/TextureFactory.d.ts +2 -1
  59. package/dist/render/TextureFactory.d.ts.map +1 -1
  60. package/dist/render/TextureFactory.js +30 -6
  61. package/dist/render/TextureFactory.js.map +1 -1
  62. package/dist/tileset/S3MTileset.d.ts +6 -2
  63. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  64. package/dist/tileset/S3MTileset.js +17 -7
  65. package/dist/tileset/S3MTileset.js.map +1 -1
  66. package/dist/worker/S3MWorkerClient.d.ts +12 -4
  67. package/dist/worker/S3MWorkerClient.d.ts.map +1 -1
  68. package/dist/worker/S3MWorkerClient.js +12 -8
  69. package/dist/worker/S3MWorkerClient.js.map +1 -1
  70. package/dist/worker/s3mWorker.js +11 -3
  71. package/dist/worker/s3mWorker.js.map +1 -1
  72. package/docs/ANIMATION.md +534 -0
  73. package/docs/API.md +4 -0
  74. package/package.json +1 -1
  75. package/src/anim/Animator.ts +460 -0
  76. package/src/anim/Path.ts +107 -0
  77. package/src/anim/easing.ts +195 -0
  78. package/src/anim/index.ts +10 -0
  79. package/src/core/TextureDecode.ts +69 -3
  80. package/src/core/TileGeometry.ts +745 -0
  81. package/src/core/types.ts +8 -0
  82. package/src/index.ts +10 -0
  83. package/src/render/EdgeLines.ts +131 -15
  84. package/src/render/FeatureStyleManager.ts +12 -4
  85. package/src/render/GeometryBuilder.ts +91 -617
  86. package/src/render/MergeVertices.ts +11 -1
  87. package/src/render/S3MViewer.ts +866 -67
  88. package/src/render/StyleMaterial.ts +54 -1
  89. package/src/render/TextureFactory.ts +42 -8
  90. package/src/tileset/S3MTileset.ts +25 -9
  91. package/src/worker/S3MWorkerClient.ts +18 -9
  92. package/src/worker/s3mWorker.ts +24 -6
@@ -115,8 +115,23 @@ export interface S3MStyle {
115
115
  roofThreshold?: number;
116
116
  /** 是否与模型原色贴图叠加混合(0 = 纯色覆盖,1 = 叠乘原色贴图) */
117
117
  texMix?: number;
118
+ /**
119
+ * 基础漫反射底色增益 / 直通自发光微调(0~1,默认 0.0,建议开启 TOP_SIDE 时可设 0.15~0.3)。
120
+ * 绕过光照夹角直接补偿表面暗部底色,让 TOP_SIDE / 自定义等模式下的背光阴影面保持通透、不至于过黑。
121
+ */
122
+ ambientGain?: number;
118
123
  /** 半透明时是否保持深度写入(默认 true,保持写入可彻底遮挡背面与内部棱线,避免背面透出) */
119
124
  depthWrite?: boolean;
125
+ /**
126
+ * 新瓦片首次出现时的淡入时长(毫秒,0 = 关闭)。
127
+ *
128
+ * 只影响「刚加载进来」的瓦片,已经显示的不会被重播。LOD 细化换瓦片时也会淡入,
129
+ * 视觉上比「啪」一下换掉柔和得多。
130
+ *
131
+ * 注意代价:淡入期间该瓦片会进透明渲染队列(混合 + 前后排序),首次加载
132
+ * 几百块瓦片同时淡入时会有一小段额外开销,所以默认关闭、按需开启。
133
+ */
134
+ fadeIn?: number;
120
135
  }
121
136
 
122
137
  export const DEFAULT_STYLE: S3MStyle = {
@@ -147,7 +162,9 @@ export const DEFAULT_STYLE: S3MStyle = {
147
162
  edgeThreshold: 55,
148
163
  roofThreshold: 0.65,
149
164
  texMix: 0,
165
+ ambientGain: 0,
150
166
  depthWrite: true,
167
+ fadeIn: 0,
151
168
  };
152
169
 
153
170
  export interface StyledMaterial extends THREE.MeshStandardMaterial {
@@ -157,6 +174,8 @@ export interface StyledMaterial extends THREE.MeshStandardMaterial {
157
174
  uBatchTex: { value: THREE.Texture };
158
175
  uBatchTexWidth: { value: number };
159
176
  uBatchTexActive: { value: number };
177
+ /** 单块瓦片的淡入系数(1 = 完全显示) */
178
+ uTileFade: { value: number };
160
179
  isStyled: true;
161
180
  [key: string]: unknown;
162
181
  };
@@ -205,9 +224,25 @@ export function createSharedUniforms(): StyleUniforms {
205
224
  uRoofThreshold: { value: 0.65 },
206
225
  /** 与模型原色贴图混合强度:0 = 纯色覆盖,1 = 叠乘原色贴图 */
207
226
  uTexMix: { value: 0 },
227
+ /** 基础底色增益 / 暗部补偿(0~1,默认 0.0) */
228
+ uAmbientGain: { value: 0 },
229
+ /**
230
+ * 图层整体透明度:在片元里乘进 diffuseColor.a。
231
+ *
232
+ * 为什么不做成 `material.opacity`:那是逐材质写入,几百块瓦片就是 O(meshes),
233
+ * 而且跨 0.999 改 `material.transparent` 会换 program(`opaque` 参与缓存键)。
234
+ * 放到共享 uniform 里,透明度动画就退化成一次 float 赋值。
235
+ */
236
+ uOpacity: { value: 1 },
237
+ /** 棱线淡入淡出乘子:setEdges 过渡期间 0→1,新插入的瓦片自动继承 */
238
+ uEdgeFade: { value: 1 },
239
+ /**
240
+ * 整体压暗乘子:`mode` 这类不可插值的切换用它做 fade-through
241
+ * (压暗 → 换模式 → 亮回来),比硬切自然得多。
242
+ */
243
+ uFade: { value: 1 },
208
244
  };
209
245
  }
210
-
211
246
  const VERT_PARS = /* glsl */ `
212
247
  attribute float aBatchId;
213
248
  attribute vec3 aColor;
@@ -391,6 +426,10 @@ const FRAG_PARS = /* glsl */ `
391
426
  uniform float uSelWhole;
392
427
  uniform float uRoofThreshold;
393
428
  uniform float uTexMix;
429
+ uniform float uAmbientGain;
430
+ uniform float uOpacity;
431
+ uniform float uTileFade;
432
+ uniform float uFade;
394
433
  varying float vBatchId;
395
434
  varying vec3 vVertColor;
396
435
  varying float vWorldZ;
@@ -430,6 +469,8 @@ const FRAG_PARS = /* glsl */ `
430
469
  const FRAG_COLOR = /* glsl */ `
431
470
  float s3mH = clamp( ( vWorldZ - uHeightMin ) / max( uHeightMax - uHeightMin, 0.001 ), 0.0, 1.0 );
432
471
  vec3 s3mBase = diffuseColor.rgb; // 已含 diffuse × map
472
+ // 图层整体透明度(共享 uniform)+ 单瓦片淡入系数(逐材质),逐帧改都不产生重编译
473
+ diffuseColor.a *= uOpacity * uTileFade;
433
474
  // 所有按单体取值(配色 / 状态纹理 / 选中判定)都走取整后的 id,避免插值噪声
434
475
  float s3mBid = s3mBatchId();
435
476
 
@@ -531,6 +572,10 @@ const FRAG_COLOR = /* glsl */ `
531
572
  } else {
532
573
  diffuseColor.rgb *= mix( 1.0, uDimFactor, uDim );
533
574
  }
575
+
576
+ // 压暗乘子放在最后:mode 分支会整体覆写 diffuseColor.rgb,
577
+ // 放在前面会被覆盖掉。setMode 的 fade-through 就靠它。
578
+ diffuseColor.rgb *= uFade;
534
579
  `;
535
580
 
536
581
  const FRAG_EMISSIVE = /* glsl */ `
@@ -539,6 +584,10 @@ const FRAG_EMISSIVE = /* glsl */ `
539
584
  if ( uSelMode != 3 ) {
540
585
  totalEmissiveRadiance += uSelColor * s3mSelE * uSelMix * ( uSelEmissive * 0.4 );
541
586
  }
587
+ // 直通自发光微调 / 环境光底色提升:补偿背光阴影面(尤其 TOP_SIDE 立面),避免光照阴影面过暗
588
+ if ( uAmbientGain > 0.001 ) {
589
+ totalEmissiveRadiance += diffuseColor.rgb * uAmbientGain;
590
+ }
542
591
  `;
543
592
 
544
593
  const FRAG_BAKE = /* glsl */ `
@@ -596,6 +645,8 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
596
645
  const uBatchTex = { value: DUMMY_BATCH_TEX };
597
646
  const uBatchTexWidth = { value: 1.0 };
598
647
  const uBatchTexActive = { value: 0 };
648
+ /** 单块瓦片的淡入系数:逐材质独享(每块网格一份材质),默认 1 = 完全显示 */
649
+ const uTileFade = { value: 1 };
599
650
 
600
651
  // 图集材质:只有几何确实带了 aAtlasRect 才开这个宏,否则缺属性会采到 (0,0) 的默认值
601
652
  const atlas = !!opts.atlas && !!opts.map;
@@ -613,6 +664,7 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
613
664
  uBatchTex,
614
665
  uBatchTexWidth,
615
666
  uBatchTexActive,
667
+ uTileFade,
616
668
  uSecMap: { value: secMap },
617
669
  uAtlasDim,
618
670
  uAtlasDim1,
@@ -656,6 +708,7 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
656
708
  mat.userData.uBatchTex = uBatchTex;
657
709
  mat.userData.uBatchTexWidth = uBatchTexWidth;
658
710
  mat.userData.uBatchTexActive = uBatchTexActive;
711
+ mat.userData.uTileFade = uTileFade;
659
712
  mat.userData.isStyled = true;
660
713
  return mat;
661
714
  }
@@ -21,6 +21,18 @@ export function setMaxAnisotropy(n: number): void {
21
21
  maxAnisotropy = Math.max(1, n | 0);
22
22
  }
23
23
 
24
+ /**
25
+ * DXT 压缩直传开关(S3MViewer 在 mount / 上下文恢复时按 GL 能力注入):
26
+ * 需要 WEBGL_compressed_texture_s3tc 与 s3tc_srgb 两个扩展同时可用 ——
27
+ * 纹理是 SRGBColorSpace,缺 srgb 变体会出现贴图发白的颜色失配。
28
+ * 移动端大多没有 s3tc,自动回退 Worker 解码成 RGBA 上传,行为同旧版。
29
+ */
30
+ let compressedTexturesSupported = false;
31
+
32
+ export function setCompressedTexturesSupported(v: boolean): void {
33
+ compressedTexturesSupported = v;
34
+ }
35
+
24
36
  export interface DecodedTexture {
25
37
  texture: THREE.Texture | null;
26
38
  /** 实际走的解码路径,`unsupported(26)` 这类值会进 stats.skipped */
@@ -31,20 +43,22 @@ export interface DecodedTexture {
31
43
  * 注入「离线解码」实现(S3MViewer 装配:走共享解析 Worker)。
32
44
  * 返回 null = Worker 不可用 / 崩溃,createTexture 自动退回主线程 decodeTextureData。
33
45
  */
34
- let offThreadDecode: ((tex: S3MTexture) => Promise<DecodedTextureData | null>) | null = null;
46
+ let offThreadDecode:
47
+ | ((tex: S3MTexture, compressed: boolean) => Promise<DecodedTextureData | null>)
48
+ | null = null;
35
49
 
36
50
  export function setTextureOffThreadDecode(
37
- fn: ((tex: S3MTexture) => Promise<DecodedTextureData | null>) | null
51
+ fn: ((tex: S3MTexture, compressed: boolean) => Promise<DecodedTextureData | null>) | null
38
52
  ): void {
39
53
  offThreadDecode = fn;
40
54
  }
41
55
 
42
- async function decodeTexture(tex: S3MTexture): Promise<DecodedTextureData> {
56
+ async function decodeTexture(tex: S3MTexture, compressed: boolean): Promise<DecodedTextureData> {
43
57
  if (offThreadDecode) {
44
- const viaWorker = await offThreadDecode(tex);
58
+ const viaWorker = await offThreadDecode(tex, compressed);
45
59
  if (viaWorker) return viaWorker;
46
60
  }
47
- return decodeTextureData(tex);
61
+ return decodeTextureData(tex, { compressed });
48
62
  }
49
63
 
50
64
  function configure<T extends THREE.Texture>(tex: T, srgb = true): T {
@@ -87,9 +101,11 @@ export async function createTexture(tex: S3MTexture | null | undefined): Promise
87
101
  // 关 GPU mip,层级由 StyleMaterial.s3mAtlasUnit 的内联金字塔自己算。
88
102
  const isAtlas = tex.packed === true || tex.flipY === true || tex.composed === true;
89
103
 
90
- const data = await decodeTexture(tex);
104
+ // 压缩直传只在「宿主 GL 支持 + 非图集」时开:图集要 flipY / 无 mip,压缩上传做不了翻行
105
+ const wantCompressed = compressedTexturesSupported && !isAtlas;
106
+ const data = await decodeTexture(tex, wantCompressed);
91
107
 
92
- if (!data.rgba && !data.bitmap) {
108
+ if (!data.rgba && !data.blocks && !data.bitmap) {
93
109
  if (data.error) {
94
110
  // 一张纹理坏了不该带崩整个瓦片,如实上报并降级成无贴图
95
111
  console.warn('[s3m] CRN 转码失败:', data.error);
@@ -100,12 +116,30 @@ export async function createTexture(tex: S3MTexture | null | undefined): Promise
100
116
  let texture: THREE.Texture;
101
117
  if (data.bitmap) {
102
118
  texture = new THREE.Texture(data.bitmap);
119
+ } else if (data.blocks && data.blockFormat) {
120
+ // DXT 块直传:GPU 硬件解压,省 4~8 倍上传带宽与显存。压缩纹理不能 generateMipmaps
121
+ // (WebGL 限制),远处靠 LinearFilter 兜底;s3mHasAlpha 走 alphaTest 的 DXT1 1bit
122
+ // alpha 历史上没开过,DXT5 的 hasAlpha 由 decodeTextureData 判定
123
+ const format =
124
+ data.blockFormat === 'dxt1' ? THREE.RGB_S3TC_DXT1_Format : THREE.RGBA_S3TC_DXT5_Format;
125
+ texture = new THREE.CompressedTexture(
126
+ [{ data: data.blocks, width: data.width, height: data.height }],
127
+ data.width,
128
+ data.height,
129
+ format,
130
+ THREE.UnsignedByteType
131
+ );
103
132
  } else {
104
133
  const dt = new THREE.DataTexture(data.rgba!, data.width, data.height, THREE.RGBAFormat, THREE.UnsignedByteType);
105
134
  if (data.hasAlpha) dt.userData.s3mHasAlpha = true;
106
135
  texture = dt;
107
136
  }
108
137
  texture.needsUpdate = true;
138
+ const configured = isAtlas ? configureAtlas(texture) : configure(texture);
139
+ if (data.blocks) {
140
+ configured.generateMipmaps = false;
141
+ configured.minFilter = THREE.LinearFilter;
142
+ }
109
143
  // flipY 为真即 S3MBLOCK 图集(见 core/types.ts),必须无 mip
110
- return { texture: isAtlas ? configureAtlas(texture) : configure(texture), decoded: data.kind };
144
+ return { texture: configured, decoded: data.kind };
111
145
  }
@@ -27,6 +27,7 @@ import type {
27
27
  TexblockRequestPlan,
28
28
  } from '../core/S3MBlockParser.js';
29
29
  import type { Anchor, ParsedTile, Stats, StyleUniforms } from '../core/types.js';
30
+ import type { TileGeometryData } from '../core/TileGeometry.js';
30
31
  import type { Group, Texture } from 'three';
31
32
 
32
33
  function dirOf(url: string): string {
@@ -226,7 +227,12 @@ export interface TileNode {
226
227
  /** 瓦片构建器可注入,用于测试 / 服务端渲染(此时不需要 three) */
227
228
  export type BuildTileFn = (
228
229
  tile: ParsedTile,
229
- options: { textureCache: Map<string, Texture>; uniforms: StyleUniforms | null }
230
+ options: {
231
+ textureCache: Map<string, Texture>;
232
+ uniforms: StyleUniforms | null;
233
+ /** Worker 侧已完成的纯数据几何组装(经典 .s3mb 路径),构建器直接包装成 three 对象 */
234
+ assembled?: TileGeometryData;
235
+ }
230
236
  ) => Promise<{ group: Group; stats: Partial<Stats> }>;
231
237
 
232
238
  export interface S3MTilesetOptions {
@@ -691,17 +697,18 @@ export class S3MTileset {
691
697
  }
692
698
 
693
699
  /**
694
- * 经典 `.s3mb` 瓦片解析:优先走 Worker(把 inflate / Draco / MeshOpt 的 CPU 密集
695
- * 解码搬离主线程),Worker 不可用或已崩溃时主线程兜底,两条路径输出完全一致。
700
+ * 经典 `.s3mb` 瓦片解析:优先走 Worker(inflate / Draco / MeshOpt / 几何组装
701
+ * 全在 Worker 做,一次往返带回 tile + 纯数据几何),Worker 不可用或已崩溃时
702
+ * 主线程兜底,两条路径输出完全一致。
696
703
  */
697
- private async _parseClassicTile(buffer: ArrayBuffer): Promise<ParsedTile> {
704
+ private async _parseClassicTile(buffer: ArrayBuffer): Promise<{ tile: ParsedTile; geometry?: TileGeometryData }> {
698
705
  if (this._useWorker && this._workerClient === undefined) {
699
706
  // 与纹理解码共用进程级单例,避免每个数据集各起一个 Worker
700
707
  this._workerClient = getSharedS3MWorker();
701
708
  }
702
709
  if (this._workerClient) {
703
710
  try {
704
- return await this._workerClient.parseTile(buffer);
711
+ return await this._workerClient.parseTile(buffer, true);
705
712
  } catch (e) {
706
713
  if (e instanceof WorkerDeadError) {
707
714
  // 脚本加载失败 / 崩溃属于环境问题,永久退回主线程;瓦片本身的问题照常抛
@@ -711,7 +718,7 @@ export class S3MTileset {
711
718
  }
712
719
  }
713
720
  }
714
- return parseS3MB(buffer);
721
+ return { tile: await parseS3MB(buffer) };
715
722
  }
716
723
 
717
724
  /**
@@ -985,6 +992,7 @@ export class S3MTileset {
985
992
  const isBlock = /\.s3mblock($|\?)/i.test(baseUrl);
986
993
 
987
994
  let tile: ParsedTile;
995
+ let assembled: TileGeometryData | undefined;
988
996
  let groupNames: string[] = [];
989
997
  const cachedSet = this._groupCache.get(baseUrl);
990
998
  if (cachedSet) {
@@ -1007,7 +1015,9 @@ export class S3MTileset {
1007
1015
  groupNames = set.names;
1008
1016
  tile = set.tiles[groupIndex] ?? set.tiles[0]!;
1009
1017
  } else {
1010
- tile = await this._parseClassicTile(buffer);
1018
+ const parsed = await this._parseClassicTile(buffer);
1019
+ tile = parsed.tile;
1020
+ assembled = parsed.geometry;
1011
1021
  }
1012
1022
  }
1013
1023
  if (isBlock) {
@@ -1017,7 +1027,11 @@ export class S3MTileset {
1017
1027
  }
1018
1028
 
1019
1029
  const build = await this._builder();
1020
- const buildRes = await build(tile, { textureCache: this.textureCache, uniforms: this._uniforms });
1030
+ const buildRes = await build(tile, {
1031
+ textureCache: this.textureCache,
1032
+ uniforms: this._uniforms,
1033
+ assembled,
1034
+ });
1021
1035
  const { group, stats } = buildRes;
1022
1036
  const texKeys = (buildRes as any).textureKeys ?? (group.userData?.textureKeys as string[] | undefined) ?? Object.keys(tile.textures ?? {});
1023
1037
  this.registerTileTextures(url, texKeys);
@@ -1164,7 +1178,9 @@ export class S3MTileset {
1164
1178
  if (inRange) node.lastSeen = this._frame;
1165
1179
 
1166
1180
  const childUrls = node.childUrls ?? (node.childUrl ? [node.childUrl] : []);
1167
- const needRefine = childUrls.length > 0 && (byDistance ? metric < threshold : metric > threshold);
1181
+ // 加入 8% 迟滞区间(Hysteresis Band),防止相机在 LOD 临界距离处发生父子瓦片逐帧来回横跳闪烁
1182
+ const hFactor = node.needRefine ? (byDistance ? 1.08 : 0.92) : 1.0;
1183
+ const needRefine = childUrls.length > 0 && (byDistance ? metric < threshold * hFactor : metric > threshold * hFactor);
1168
1184
  node.needRefine = needRefine;
1169
1185
 
1170
1186
  const dbgSeen = ((globalThis as { __S3M_LOD_SEEN?: Set<string> }).__S3M_LOD_SEEN ??= new Set<string>());
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import type { ParsedTile, S3MTexture } from '../core/types.js';
12
12
  import type { DecodedTextureData } from '../core/TextureDecode.js';
13
+ import type { TileGeometryData } from '../core/TileGeometry.js';
13
14
 
14
15
  /** Worker 崩溃 / 不可用 —— 与「瓦片数据本身有问题」区分开,调用方据此决定降级还是重试 */
15
16
  export class WorkerDeadError extends Error {
@@ -23,6 +24,7 @@ interface ParseOkResponse {
23
24
  id: number;
24
25
  ok: true;
25
26
  tile: ParsedTile;
27
+ geometry?: TileGeometryData;
26
28
  }
27
29
 
28
30
  interface TexOkResponse {
@@ -73,7 +75,7 @@ export class S3MWorkerClient {
73
75
  (err as { permanent?: boolean }).permanent = msg.permanent;
74
76
  waiter.reject(err);
75
77
  } else if ('tile' in msg) {
76
- waiter.resolve(msg.tile);
78
+ waiter.resolve({ tile: msg.tile, geometry: msg.geometry });
77
79
  } else {
78
80
  waiter.resolve(msg.data);
79
81
  }
@@ -82,23 +84,26 @@ export class S3MWorkerClient {
82
84
  worker.onmessageerror = () => this._die('s3m 解析 Worker 消息无法反序列化');
83
85
  }
84
86
 
85
- /** 解析一块瓦片。buffer 会被 structured clone(不 transfer),主线程保留原 buffer 以便崩溃后降级重解 */
86
- parseTile(buffer: ArrayBuffer): Promise<ParsedTile> {
87
+ /**
88
+ * 解析一块瓦片。buffer 会被 structured clone(不 transfer),主线程保留原 buffer 以便崩溃后降级重解。
89
+ * assemble = true 时顺带在 Worker 里做纯数据几何组装,返回 { tile, geometry } 一次带回。
90
+ */
91
+ parseTile(buffer: ArrayBuffer, assemble = false, batching = true): Promise<{ tile: ParsedTile; geometry?: TileGeometryData }> {
87
92
  if (this.dead) return Promise.reject(new WorkerDeadError('s3m 解析 Worker 已终止'));
88
93
  const id = ++this._seq;
89
- return new Promise<ParsedTile>((resolve, reject) => {
94
+ return new Promise<{ tile: ParsedTile; geometry?: TileGeometryData }>((resolve, reject) => {
90
95
  this._pending.set(id, { resolve: resolve as (v: unknown) => void, reject });
91
- this._worker.postMessage({ type: 'parse', id, buffer });
96
+ this._worker.postMessage({ type: 'parse', id, buffer, assemble, batching });
92
97
  });
93
98
  }
94
99
 
95
100
  /** 解码一张纹理(DXT / CRN / WEBP / RGB 系)。tex.data 走 structured clone,主线程保留原数据 */
96
- decodeTexture(tex: S3MTexture): Promise<DecodedTextureData> {
101
+ decodeTexture(tex: S3MTexture, compressed = false): Promise<DecodedTextureData> {
97
102
  if (this.dead) return Promise.reject(new WorkerDeadError('s3m 解析 Worker 已终止'));
98
103
  const id = ++this._seq;
99
104
  return new Promise<DecodedTextureData>((resolve, reject) => {
100
105
  this._pending.set(id, { resolve: resolve as (v: unknown) => void, reject });
101
- this._worker.postMessage({ type: 'tex', id, tex });
106
+ this._worker.postMessage({ type: 'tex', id, tex, compressed });
102
107
  });
103
108
  }
104
109
 
@@ -138,12 +143,16 @@ export function getSharedS3MWorker(): S3MWorkerClient | null {
138
143
  * 纹理解码的「Worker 优先」入口,供 TextureFactory.setTextureOffThreadDecode 装配。
139
144
  * 永不 reject:Worker 不可用 / 崩溃 / 单张解码失败都返回 null,调用方主线程兜底
140
145
  * (解码失败本身在 decodeTextureData 里也不抛错,走 crn(failed) 之类的标记)。
146
+ * compressed 由 TextureFactory 按宿主 GL 能力传入,Worker 端据此跳过 DXT 解码。
141
147
  */
142
- export async function decodeTextureWithWorker(tex: S3MTexture): Promise<DecodedTextureData | null> {
148
+ export async function decodeTextureWithWorker(
149
+ tex: S3MTexture,
150
+ compressed: boolean
151
+ ): Promise<DecodedTextureData | null> {
143
152
  const client = getSharedS3MWorker();
144
153
  if (!client) return null;
145
154
  try {
146
- return await client.decodeTexture(tex);
155
+ return await client.decodeTexture(tex, compressed);
147
156
  } catch {
148
157
  return null;
149
158
  }
@@ -16,15 +16,24 @@
16
16
  */
17
17
  import { parseS3MB } from '../core/S3MBParser.js';
18
18
  import { decodeTextureData } from '../core/TextureDecode.js';
19
+ import { assembleTileGeometry } from '../core/TileGeometry.js';
19
20
  import type { ParsedTile, S3MTexture } from '../core/types.js';
21
+ import type { TileGeometryData } from '../core/TileGeometry.js';
20
22
  import { collectTransferables } from './transferables.js';
21
23
 
22
- type ParseRequest = { type: 'parse'; id: number; buffer: ArrayBuffer };
23
- type TexRequest = { type: 'tex'; id: number; tex: S3MTexture };
24
+ type ParseRequest = {
25
+ type: 'parse';
26
+ id: number;
27
+ buffer: ArrayBuffer;
28
+ /** 解析后顺带做纯数据几何组装(解交错 / 法线 / 合批),一次往返带回 */
29
+ assemble?: boolean;
30
+ batching?: boolean;
31
+ };
32
+ type TexRequest = { type: 'tex'; id: number; tex: S3MTexture; compressed?: boolean };
24
33
  type Request = ParseRequest | TexRequest;
25
34
 
26
35
  type ParseResponse =
27
- | { id: number; ok: true; tile: ParsedTile }
36
+ | { id: number; ok: true; tile: ParsedTile; geometry?: TileGeometryData }
28
37
  | { id: number; ok: false; message: string; permanent: boolean };
29
38
 
30
39
  type TexResponse = { id: number; ok: true; data: Awaited<ReturnType<typeof decodeTextureData>> };
@@ -44,10 +53,19 @@ ctx.onmessage = async (e: MessageEvent<Request>) => {
44
53
  try {
45
54
  if (msg.type === 'parse') {
46
55
  const tile = await parseS3MB(msg.buffer);
47
- // 响应整体过线(零拷贝);请求 buffer 是主线程 structured clone 过来的副本,无需回传
48
- ctx.postMessage({ id, ok: true, tile } satisfies ParseResponse, collectTransferables(tile));
56
+ if (msg.assemble) {
57
+ // 解析 + 组装一起做,避免 tile 回主线程后再整块克隆回来一次
58
+ const geometry = assembleTileGeometry(tile, { batching: msg.batching });
59
+ ctx.postMessage(
60
+ { id, ok: true, tile, geometry } satisfies ParseResponse,
61
+ [...collectTransferables(tile), ...collectTransferables(geometry)]
62
+ );
63
+ } else {
64
+ // 响应整体过线(零拷贝);请求 buffer 是主线程 structured clone 过来的副本,无需回传
65
+ ctx.postMessage({ id, ok: true, tile } satisfies ParseResponse, collectTransferables(tile));
66
+ }
49
67
  } else {
50
- const data = await decodeTextureData(msg.tex);
68
+ const data = await decodeTextureData(msg.tex, { compressed: msg.compressed });
51
69
  ctx.postMessage({ id, ok: true, data } satisfies TexResponse, texTransferList(data));
52
70
  }
53
71
  } catch (err) {