s3m-three 0.3.0 → 0.3.2

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 (110) hide show
  1. package/README.md +1 -1
  2. package/dist/browser/s3m-three.iife.ext-three.js +163 -15
  3. package/dist/browser/s3m-three.iife.js +190 -42
  4. package/dist/core/AtlasAlpha.d.ts +31 -0
  5. package/dist/core/AtlasAlpha.d.ts.map +1 -0
  6. package/dist/core/AtlasAlpha.js +82 -0
  7. package/dist/core/AtlasAlpha.js.map +1 -0
  8. package/dist/core/AtlasPack.d.ts +57 -0
  9. package/dist/core/AtlasPack.d.ts.map +1 -0
  10. package/dist/core/AtlasPack.js +81 -0
  11. package/dist/core/AtlasPack.js.map +1 -0
  12. package/dist/core/AtlasUpgrade.d.ts +68 -0
  13. package/dist/core/AtlasUpgrade.d.ts.map +1 -0
  14. package/dist/core/AtlasUpgrade.js +154 -0
  15. package/dist/core/AtlasUpgrade.js.map +1 -0
  16. package/dist/core/DXTTextureDecode.d.ts.map +1 -1
  17. package/dist/core/DXTTextureDecode.js +28 -6
  18. package/dist/core/DXTTextureDecode.js.map +1 -1
  19. package/dist/core/Draco.d.ts +52 -0
  20. package/dist/core/Draco.d.ts.map +1 -0
  21. package/dist/core/Draco.js +323 -0
  22. package/dist/core/Draco.js.map +1 -0
  23. package/dist/core/MeshOpt.d.ts +14 -3
  24. package/dist/core/MeshOpt.d.ts.map +1 -1
  25. package/dist/core/MeshOpt.js +101 -18
  26. package/dist/core/MeshOpt.js.map +1 -1
  27. package/dist/core/S3M1Parser.d.ts +16 -0
  28. package/dist/core/S3M1Parser.d.ts.map +1 -0
  29. package/dist/core/S3M1Parser.js +367 -0
  30. package/dist/core/S3M1Parser.js.map +1 -0
  31. package/dist/core/S3MBParser.d.ts +43 -1
  32. package/dist/core/S3MBParser.d.ts.map +1 -1
  33. package/dist/core/S3MBParser.js +101 -23
  34. package/dist/core/S3MBParser.js.map +1 -1
  35. package/dist/core/S3MBlockParser.d.ts +209 -0
  36. package/dist/core/S3MBlockParser.d.ts.map +1 -0
  37. package/dist/core/S3MBlockParser.js +789 -0
  38. package/dist/core/S3MBlockParser.js.map +1 -0
  39. package/dist/core/TextureFormat.d.ts +0 -1
  40. package/dist/core/TextureFormat.d.ts.map +1 -1
  41. package/dist/core/TextureFormat.js +19 -2
  42. package/dist/core/TextureFormat.js.map +1 -1
  43. package/dist/core/types.d.ts +65 -0
  44. package/dist/core/types.d.ts.map +1 -1
  45. package/dist/index.d.ts +3 -2
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +2 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/render/DatasetManager.d.ts +11 -0
  50. package/dist/render/DatasetManager.d.ts.map +1 -1
  51. package/dist/render/DatasetManager.js +68 -3
  52. package/dist/render/DatasetManager.js.map +1 -1
  53. package/dist/render/GeometryBuilder.d.ts +3 -0
  54. package/dist/render/GeometryBuilder.d.ts.map +1 -1
  55. package/dist/render/GeometryBuilder.js +189 -17
  56. package/dist/render/GeometryBuilder.js.map +1 -1
  57. package/dist/render/GroundManager.d.ts +57 -1
  58. package/dist/render/GroundManager.d.ts.map +1 -1
  59. package/dist/render/GroundManager.js +149 -21
  60. package/dist/render/GroundManager.js.map +1 -1
  61. package/dist/render/Picker.d.ts.map +1 -1
  62. package/dist/render/Picker.js +12 -4
  63. package/dist/render/Picker.js.map +1 -1
  64. package/dist/render/PointCloudMaterial.d.ts +36 -0
  65. package/dist/render/PointCloudMaterial.d.ts.map +1 -0
  66. package/dist/render/PointCloudMaterial.js +159 -0
  67. package/dist/render/PointCloudMaterial.js.map +1 -0
  68. package/dist/render/S3MViewer.d.ts +75 -3
  69. package/dist/render/S3MViewer.d.ts.map +1 -1
  70. package/dist/render/S3MViewer.js +284 -54
  71. package/dist/render/S3MViewer.js.map +1 -1
  72. package/dist/render/StyleMaterial.d.ts +6 -0
  73. package/dist/render/StyleMaterial.d.ts.map +1 -1
  74. package/dist/render/StyleMaterial.js +125 -2
  75. package/dist/render/StyleMaterial.js.map +1 -1
  76. package/dist/render/TextureFactory.d.ts.map +1 -1
  77. package/dist/render/TextureFactory.js +64 -10
  78. package/dist/render/TextureFactory.js.map +1 -1
  79. package/dist/tileset/S3MTileset.d.ts +177 -1
  80. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  81. package/dist/tileset/S3MTileset.js +754 -60
  82. package/dist/tileset/S3MTileset.js.map +1 -1
  83. package/dist/vendor/crunch_decoder.module.js +12 -18
  84. package/dist/vendor/draco_decoder.module.js +3178 -0
  85. package/docs/API.md +150 -11
  86. package/package.json +1 -1
  87. package/src/core/AtlasAlpha.ts +82 -0
  88. package/src/core/AtlasPack.ts +127 -0
  89. package/src/core/AtlasUpgrade.ts +182 -0
  90. package/src/core/DXTTextureDecode.ts +28 -6
  91. package/src/core/Draco.ts +409 -0
  92. package/src/core/MeshOpt.ts +124 -18
  93. package/src/core/S3M1Parser.ts +430 -0
  94. package/src/core/S3MBParser.ts +112 -21
  95. package/src/core/S3MBlockParser.ts +953 -0
  96. package/src/core/TextureFormat.ts +20 -2
  97. package/src/core/types.ts +63 -0
  98. package/src/index.ts +3 -1
  99. package/src/render/DatasetManager.ts +70 -5
  100. package/src/render/GeometryBuilder.ts +212 -16
  101. package/src/render/GroundManager.ts +174 -20
  102. package/src/render/Picker.ts +13 -4
  103. package/src/render/PointCloudMaterial.ts +174 -0
  104. package/src/render/S3MViewer.ts +322 -55
  105. package/src/render/StyleMaterial.ts +135 -2
  106. package/src/render/TextureFactory.ts +67 -11
  107. package/src/tileset/S3MTileset.ts +798 -65
  108. package/src/vendor/crunch_decoder.module.js +12 -18
  109. package/src/vendor/draco_decoder.module.d.ts +128 -0
  110. package/src/vendor/draco_decoder.module.js +3178 -0
@@ -208,6 +208,16 @@ export function createSharedUniforms(): StyleUniforms {
208
208
  const VERT_PARS = /* glsl */ `
209
209
  attribute float aBatchId;
210
210
  attribute vec3 aColor;
211
+ #ifdef S3M_ATLAS
212
+ attribute vec4 aAtlasRect;
213
+ #ifdef S3M_ATLAS_SEC
214
+ attribute vec4 aAtlasRect1;
215
+ attribute vec2 aSecUv;
216
+ varying vec2 vSecUv;
217
+ varying vec4 vSecRect;
218
+ #endif
219
+ varying vec4 vAtlasRect;
220
+ #endif
211
221
  // 实例化几何没有逐顶点 batchId:顶点是 base mesh 的,单体粒度落在实例上
212
222
  #ifdef USE_INSTANCING
213
223
  attribute float aInstanceBatchId;
@@ -267,6 +277,14 @@ const VERT_WORLD = /* glsl */ `
267
277
  vBatchId = s3mBatchRaw();
268
278
  vVertColor = aColor;
269
279
 
280
+ #ifdef S3M_ATLAS
281
+ vAtlasRect = aAtlasRect;
282
+ #ifdef S3M_ATLAS_SEC
283
+ vSecUv = aSecUv;
284
+ vSecRect = aAtlasRect1;
285
+ #endif
286
+ #endif
287
+
270
288
  #ifdef USE_INSTANCING
271
289
  mat3 s3mNormMat = mat3( modelMatrix ) * mat3( instanceMatrix );
272
290
  #else
@@ -276,6 +294,75 @@ const VERT_WORLD = /* glsl */ `
276
294
  `;
277
295
 
278
296
  const FRAG_PARS = /* glsl */ `
297
+ #ifdef S3M_ATLAS
298
+ varying vec4 vAtlasRect;
299
+ uniform vec2 uAtlasDim;
300
+ #ifdef S3M_ATLAS_SEC
301
+ uniform sampler2D uSecMap;
302
+ uniform vec2 uAtlasDim1;
303
+ varying vec2 vSecUv;
304
+ varying vec4 vSecRect;
305
+ #endif
306
+ /**
307
+ * 官方 CalculateMipLevel(vec2 版),逐字照抄 iClient3D 12.1.1。
308
+ * 用**未 fract** 的 uv 求屏幕空间导数 —— 先 fract 再求导会在平铺接缝处炸出
309
+ * 一个巨大导数,GPU 直接把整块砖采成最糊的 mip,表现为每块砖边缘一条暗线。
310
+ */
311
+ void s3mMipLevel( vec2 uv, vec2 texSize, float maxMip, out float mip ) {
312
+ vec2 dx = dFdx( uv * texSize.x );
313
+ vec2 dy = dFdy( uv * texSize.y );
314
+ float dotX = dot( dx, dx );
315
+ float dotY = dot( dy, dy );
316
+ float dMax = max( dotX, dotY );
317
+ float dMin = min( dotX, dotY );
318
+ float off = clamp( ( dMax - dMin ) / max( dMax + dMin, 1e-8 ), 0.0, 1.0 );
319
+ float d = dMax * ( 1.0 - off ) + dMin * off;
320
+ mip = clamp( 0.5 * log2( max( d, 1e-8 ) ), 0.0, maxMip - 1.62 );
321
+ }
322
+
323
+ /**
324
+ * 按指定 mip 档采样子区域(官方 czm_getTexColorForS3M 的内层循环体)。
325
+ *
326
+ * ★ 子区域 rect 的**右 1/3 不是贴图内容,是内联 mip 金字塔**:
327
+ * mip0 只占左 2/3,右 1/3 从上往下依次堆 1/2、1/4、1/8 缩略图,总高恰好 = rect 高。
328
+ * 官方那句 width *= 2.0/3.0 就是为这个,不是「经验收缩系数」。
329
+ * 数值实证:14 个块 691 条子纹理里 96.7% 满足「右侧 1/3 ≈ 左 2/3 的 1/2 下采样」
330
+ * (平均误差 6.8 vs 27.3)。
331
+ * 所以 mip > 0 时采样点必须右移到 x + 2/3·w,并下移到 (2^(m-1)-1)/2^(m-1)。
332
+ */
333
+ vec4 s3mAtlasLod( vec2 uv, vec2 translate, vec2 scale, vec2 texSize, sampler2D tex, float mip ) {
334
+ vec2 t = translate;
335
+ if ( mip > 0.0 ) {
336
+ t.x = translate.x + scale.x;
337
+ float temp = pow( 2.0, mip - 1.0 );
338
+ t.y = translate.y + scale.y * ( temp - 1.0 ) / temp;
339
+ }
340
+ float inv = 1.0 / pow( 2.0, mip );
341
+ vec2 o = fract( uv );
342
+ // 半纹素内缩,避免双线性把邻格内容混进来
343
+ vec2 guard = vec2( pow( 2.0, mip ) / texSize.x, pow( 2.0, mip ) / texSize.y );
344
+ o = clamp( o, guard, vec2( 1.0 ) - guard );
345
+ return texture2D( tex, o * inv * scale + t );
346
+ }
347
+
348
+ /**
349
+ * 图集采样入口:层级由 ceil/floor 两档各采一次再平均(官方同样做法),
350
+ * 因为内联金字塔只有 2 的整数次幂,中间档必须靠混合。
351
+ */
352
+ vec4 s3mAtlasUnit( vec2 uv, vec4 rect, vec2 atlasDim, sampler2D tex ) {
353
+ // 退化矩形 = 官方 batchId < 0:该纹理单元不参与合成
354
+ if ( rect.z <= 0.0 || rect.w <= 0.0 ) return vec4( 1.0 );
355
+ vec2 dimPx = vec2( rect.z * atlasDim.x, rect.w * atlasDim.y );
356
+ vec2 texSize = vec2( dimPx.x * ( 2.0 / 3.0 ), dimPx.y );
357
+ float maxMip = log2( max( min( texSize.x, texSize.y ), 1.0 ) );
358
+ vec2 scale = texSize / atlasDim;
359
+ float mip = 0.0;
360
+ s3mMipLevel( uv, texSize, maxMip, mip );
361
+ vec4 hi = s3mAtlasLod( uv, rect.xy, scale, texSize, tex, ceil( mip ) );
362
+ vec4 lo = s3mAtlasLod( uv, rect.xy, scale, texSize, tex, floor( mip ) );
363
+ return hi * 0.5 + lo * 0.5;
364
+ }
365
+ #endif
279
366
  uniform int uMode;
280
367
  uniform vec3 uColorA;
281
368
  uniform vec3 uColorB;
@@ -445,6 +532,14 @@ const FRAG_EMISSIVE = /* glsl */ `
445
532
  }
446
533
  `;
447
534
 
535
+ const FRAG_BAKE = /* glsl */ `
536
+ #ifdef S3M_ATLAS_SEC
537
+ // 官方 getTextureColorForS3M = firstColor(base) * secColor(bake),
538
+ // 两套独立的 rect / 尺寸(实测 base 11 条、bake 1 条,条数并不相同)
539
+ diffuseColor *= s3mAtlasUnit( vSecUv, vSecRect, uAtlasDim1, uSecMap );
540
+ #endif
541
+ `;
542
+
448
543
  export interface StyledMaterialOptions {
449
544
  uniforms: StyleUniforms;
450
545
  map?: THREE.Texture | null;
@@ -454,6 +549,12 @@ export interface StyledMaterialOptions {
454
549
  opacity?: number;
455
550
  roughness?: number;
456
551
  flatShading?: boolean;
552
+ /** >0 时开启 alphaTest:镂空贴图(S3MBLOCK 图集)的透明片元直接丢弃,避免渲染成黑面 */
553
+ alphaTest?: number;
554
+ /** 贴图为 S3MBLOCK 图集:几何需带 aAtlasRect,平铺改由片元按子区域做 */
555
+ atlas?: boolean;
556
+ /** 图集材质的第二套纹理(官方 BakeTex,光照烘焙),与 base 相乘 */
557
+ secMap?: THREE.Texture | null;
457
558
  }
458
559
 
459
560
  /**
@@ -476,6 +577,7 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
476
577
  flatShading: opts.flatShading !== false,
477
578
  // 顶点色、贴图全部在自定义 chunk 里处理,不让 three 插手
478
579
  vertexColors: false,
580
+ alphaTest: opts.alphaTest ?? 0,
479
581
  }) as StyledMaterial;
480
582
 
481
583
  // 选中单体编号:每个 Mesh 独立,默认 -1 表示无选中
@@ -486,6 +588,15 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
486
588
  const uBatchTexWidth = { value: 1.0 };
487
589
  const uBatchTexActive = { value: 0 };
488
590
 
591
+ // 图集材质:只有几何确实带了 aAtlasRect 才开这个宏,否则缺属性会采到 (0,0) 的默认值
592
+ const atlas = !!opts.atlas && !!opts.map;
593
+ const secMap = atlas ? (opts.secMap ?? null) : null;
594
+ if (atlas) mat.defines = { ...(mat.defines as Record<string, string>), S3M_ATLAS: '' };
595
+ if (secMap) mat.defines = { ...mat.defines, S3M_ATLAS_SEC: '' };
596
+ // 官方 uTextureDim = (base宽, base高, bake宽, bake高),取实际绑定纹理的像素尺寸
597
+ const uAtlasDim = { value: new THREE.Vector2(opts.map?.image?.width || 1, opts.map?.image?.height || 1) };
598
+ const uAtlasDim1 = { value: new THREE.Vector2(secMap?.image?.width || 1, secMap?.image?.height || 1) };
599
+
489
600
  mat.onBeforeCompile = (shader) => {
490
601
  Object.assign(shader.uniforms, uniforms, {
491
602
  uSelBatch,
@@ -493,6 +604,9 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
493
604
  uBatchTex,
494
605
  uBatchTexWidth,
495
606
  uBatchTexActive,
607
+ uSecMap: { value: secMap },
608
+ uAtlasDim,
609
+ uAtlasDim1,
496
610
  });
497
611
 
498
612
  shader.vertexShader = shader.vertexShader
@@ -504,10 +618,29 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
504
618
  .replace('#include <common>', `#include <common>\n${FRAG_PARS}`)
505
619
  .replace('#include <color_fragment>', `#include <color_fragment>\n${FRAG_COLOR}`)
506
620
  .replace('#include <emissivemap_fragment>', `#include <emissivemap_fragment>\n${FRAG_EMISSIVE}`);
621
+
622
+ if (atlas) {
623
+ // onBeforeCompile 收到的还是未展开的 `#include <map_fragment>`,不是 include 里的
624
+ // `texture2D( map, vMapUv )`。只替换后者会静默 no-op,最终仍按整张图集普通采样。
625
+ const atlasMapFragment = /* glsl */ `
626
+ #ifdef USE_MAP
627
+ vec4 sampledDiffuseColor = s3mAtlasUnit( vMapUv, vAtlasRect, uAtlasDim, map );
628
+ diffuseColor *= sampledDiffuseColor;
629
+ #endif
630
+ `;
631
+ shader.fragmentShader = shader.fragmentShader.replace(
632
+ '#include <map_fragment>',
633
+ `${atlasMapFragment}${secMap ? `\n${FRAG_BAKE}` : ''}`
634
+ );
635
+ } else if (secMap) {
636
+ shader.fragmentShader = shader.fragmentShader.replace('#include <map_fragment>', `#include <map_fragment>\n${FRAG_BAKE}`);
637
+ }
507
638
  };
508
639
 
509
- // 不同材质共用同一段注入代码,必须让 three 认为它们的 program 可复用
510
- mat.customProgramCacheKey = () => 's3m-styled';
640
+ // 不同材质共用同一段注入代码,必须让 three 认为它们的 program 可复用;
641
+ // 但图集 / 烘焙两套宏会改注入结果,必须进 cacheKey,否则会复用错 program。
642
+ mat.customProgramCacheKey = () =>
643
+ `s3m-styled${atlas ? '-atlas' : ''}${secMap ? '-bake' : ''}`;
511
644
 
512
645
  mat.userData.uSelBatch = uSelBatch;
513
646
  mat.userData.uSelWhole = uSelWhole;
@@ -47,7 +47,7 @@ function configure<T extends THREE.Texture>(tex: T, srgb = true): T {
47
47
  tex.magFilter = THREE.LinearFilter;
48
48
  tex.minFilter = THREE.LinearMipmapLinearFilter;
49
49
  tex.generateMipmaps = true;
50
- // 建筑立面大多是斜视角,各向异性过滤对清晰度的提升比提高分辨率更直接
50
+ // 各向异性过滤对斜视角立面清晰度的提升比提高分辨率更直接
51
51
  tex.anisotropy = maxAnisotropy;
52
52
  if (srgb) tex.colorSpace = THREE.SRGBColorSpace;
53
53
  tex.flipY = false;
@@ -55,13 +55,61 @@ function configure<T extends THREE.Texture>(tex: T, srgb = true): T {
55
55
  return tex;
56
56
  }
57
57
 
58
+ /**
59
+ * S3MBLOCK 图集专用:关掉 GPU mip。
60
+ *
61
+ * 官方 `sampleLod` 是显式 `textureLod(tex, uv, 0.0)`,层级靠「把子区域 rect 平移 +
62
+ * 内缩」自己算(见 StyleMaterial 的 s3mAtlasUnit)。若让 GPU 对整张图集生成 mip,
63
+ * 自动选的层级会把相邻子区域混进来,远处就是一糊串色 —— 实测照抄公式但留着 GPU mip
64
+ * 时,画面反而比不改更糟,就是因为这两个变量叠在一起了。
65
+ */
66
+ function configureAtlas<T extends THREE.Texture>(tex: T): T {
67
+ configure(tex);
68
+ tex.wrapS = THREE.ClampToEdgeWrapping;
69
+ tex.wrapT = THREE.ClampToEdgeWrapping;
70
+ tex.minFilter = THREE.LinearFilter;
71
+ tex.generateMipmaps = false;
72
+ tex.anisotropy = 1;
73
+ tex.needsUpdate = true;
74
+ return tex;
75
+ }
76
+
77
+ /** 行序翻转(原地)。S3MBLOCK 图集按 GL 方向存储(v=0 在最后一行),解码后要翻一次行 */
78
+ function flipRows(rgba: Uint8Array, width: number, height: number): void {
79
+ const row = width * 4;
80
+ const tmp = new Uint8Array(row);
81
+ for (let y = 0; y < height >> 1; y++) {
82
+ const a = y * row;
83
+ const b = (height - 1 - y) * row;
84
+ tmp.set(rgba.subarray(a, a + row));
85
+ rgba.copyWithin(a, b, b + row);
86
+ rgba.set(tmp, b);
87
+ }
88
+ }
89
+
90
+ /** 该像素格式自带 alpha 通道(DXT1 的 1bit alpha 不算,历史上没开过 alphaTest) */
91
+ function hasAlphaChannel(pixelFormat: number): boolean {
92
+ return (
93
+ pixelFormat === PixelFormat.DXT3 ||
94
+ pixelFormat === PixelFormat.DXT4 ||
95
+ pixelFormat === PixelFormat.DXT5 ||
96
+ pixelFormat === PixelFormat.ARGB ||
97
+ pixelFormat === PixelFormat.ABGR ||
98
+ pixelFormat === PixelFormat.BGRA ||
99
+ pixelFormat === PixelFormat.RGBA
100
+ );
101
+ }
102
+
58
103
  /** 解 DXT 到 RGBA8。DXT5 是 1 字节/像素,DXT1 是 0.5 字节/像素。 */
59
104
  /** DXT 解码只需要尺寸 / 格式 / 字节,CRN 转码出来的结果也走这条路 */
60
- function decodeDXT(tex: Pick<S3MTexture, 'width' | 'height' | 'pixelFormat' | 'data'>): THREE.DataTexture {
105
+ function decodeDXT(tex: Pick<S3MTexture, 'width' | 'height' | 'pixelFormat' | 'data' | 'flipY'>): THREE.DataTexture {
61
106
  const { width, height, pixelFormat, data } = tex;
62
107
  const out = new Uint8Array(width * height * 4);
63
108
  DXTTextureDecode.decode(out, width, height, data, pixelFormat);
64
- return new THREE.DataTexture(out, width, height, THREE.RGBAFormat, THREE.UnsignedByteType);
109
+ if (tex.flipY) flipRows(out, width, height);
110
+ const dt = new THREE.DataTexture(out, width, height, THREE.RGBAFormat, THREE.UnsignedByteType);
111
+ if (hasAlphaChannel(pixelFormat)) dt.userData.s3mHasAlpha = true;
112
+ return dt;
65
113
  }
66
114
 
67
115
  function fromRawRGB(tex: S3MTexture): THREE.DataTexture {
@@ -83,12 +131,20 @@ function fromRawRGB(tex: S3MTexture): THREE.DataTexture {
83
131
  out[d + 2] = swapRB ? data[p] : data[p + 2];
84
132
  out[d + 3] = hasAlpha ? data[alphaFirst ? s : s + 3] : 255;
85
133
  }
86
- return new THREE.DataTexture(out, width, height, THREE.RGBAFormat, THREE.UnsignedByteType);
134
+ if (tex.flipY) flipRows(out, width, height);
135
+ const dt = new THREE.DataTexture(out, width, height, THREE.RGBAFormat, THREE.UnsignedByteType);
136
+ if (hasAlphaChannel(pixelFormat)) dt.userData.s3mHasAlpha = true;
137
+ return dt;
87
138
  }
88
139
 
89
140
  export async function createTexture(tex: S3MTexture | null | undefined): Promise<DecodedTexture> {
90
141
  if (!tex || !tex.width || !tex.height) return { texture: null, decoded: 'empty' };
91
142
 
143
+ // 图集纹理(flipY 的原始 DXT 图集 / AtlasPack 拼出来的 RGBA 大图 / leaf 组合画布)都必须
144
+ // 关 GPU mip,层级由 StyleMaterial.s3mAtlasUnit 的内联金字塔自己算。
145
+ const isAtlas = tex.packed === true || tex.flipY === true || tex.composed === true;
146
+ const asAtlas = <T extends THREE.Texture>(t: T): T => (isAtlas ? configureAtlas(t) : configure(t));
147
+
92
148
  // 判定只能看 compressType:v3.00 的 CRN 纹理 pixelFormat 是 13(ABGR)、
93
149
  // 行标的 CRN 是 32856(GL_RGBA8),都不是 26 / 27(见 core/TextureFormat.ts)
94
150
  const kind = resolveTextureKind(tex.compressType ?? 0, tex.pixelFormat);
@@ -102,7 +158,7 @@ export async function createTexture(tex: S3MTexture | null | undefined): Promise
102
158
  pixelFormat: crn.dxtFormat === 1 ? 17 : 21,
103
159
  data: crn.data,
104
160
  });
105
- return { texture: configure(decoded), decoded: `crn->dxt${crn.dxtFormat}` };
161
+ return { texture: asAtlas(decoded), decoded: `crn->dxt${crn.dxtFormat}` };
106
162
  } catch (e) {
107
163
  // 一张纹理坏了不该带崩整个瓦片,如实上报并降级成无贴图
108
164
  console.warn('[s3m] CRN 转码失败:', e);
@@ -111,10 +167,9 @@ export async function createTexture(tex: S3MTexture | null | undefined): Promise
111
167
  }
112
168
 
113
169
  if (kind === 'dxt1' || kind === 'dxt3' || kind === 'dxt4' || kind === 'dxt5') {
114
- return {
115
- texture: configure(decodeDXT({ ...tex, pixelFormat: dxtPixelFormatOf(kind) })),
116
- decoded: kind,
117
- };
170
+ const decoded = decodeDXT({ ...tex, pixelFormat: dxtPixelFormatOf(kind) });
171
+ // flipY 为真即 S3MBLOCK 图集(见 core/types.ts),必须无 mip
172
+ return { texture: asAtlas(decoded), decoded: kind };
118
173
  }
119
174
 
120
175
  if (kind === 'webp') {
@@ -122,11 +177,12 @@ export async function createTexture(tex: S3MTexture | null | undefined): Promise
122
177
  const bmp = await createImageBitmap(blob);
123
178
  const t = new THREE.Texture(bmp);
124
179
  t.needsUpdate = true;
125
- return { texture: configure(t), decoded: 'webp' };
180
+ return { texture: asAtlas(t), decoded: 'webp' };
126
181
  }
127
182
 
128
183
  if (kind === 'raw') {
129
- return { texture: configure(fromRawRGB(tex)), decoded: 'raw' };
184
+ const decoded = fromRawRGB(tex);
185
+ return { texture: asAtlas(decoded), decoded: 'raw' };
130
186
  }
131
187
 
132
188
  // KTX2 / ETC 需要额外解码器,先如实上报而不是静默出错