s3m-three 0.4.18 → 0.4.20

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 (169) hide show
  1. package/dist/browser/assets/s3mWorker-DvAUA8NJ.js +1 -0
  2. package/dist/browser/s3m-three.iife.ext-three.js +619 -304
  3. package/dist/browser/s3m-three.iife.js +647 -332
  4. package/dist/core/Draco.d.ts.map +1 -1
  5. package/dist/core/Draco.js +0 -1
  6. package/dist/core/Draco.js.map +1 -1
  7. package/dist/core/GaussianSplat.d.ts +64 -0
  8. package/dist/core/GaussianSplat.d.ts.map +1 -0
  9. package/dist/core/GaussianSplat.js +139 -0
  10. package/dist/core/GaussianSplat.js.map +1 -0
  11. package/dist/core/S3M1Parser.d.ts +1 -1
  12. package/dist/core/S3M1Parser.d.ts.map +1 -1
  13. package/dist/core/S3M1Parser.js +320 -25
  14. package/dist/core/S3M1Parser.js.map +1 -1
  15. package/dist/core/S3MBParser.d.ts.map +1 -1
  16. package/dist/core/S3MBParser.js +13 -0
  17. package/dist/core/S3MBParser.js.map +1 -1
  18. package/dist/core/S3MBlockParser.d.ts.map +1 -1
  19. package/dist/core/S3MBlockParser.js +83 -28
  20. package/dist/core/S3MBlockParser.js.map +1 -1
  21. package/dist/core/SplatLod.d.ts +36 -0
  22. package/dist/core/SplatLod.d.ts.map +1 -0
  23. package/dist/core/SplatLod.js +54 -0
  24. package/dist/core/SplatLod.js.map +1 -0
  25. package/dist/core/SplatProjection.d.ts +75 -0
  26. package/dist/core/SplatProjection.d.ts.map +1 -0
  27. package/dist/core/SplatProjection.js +162 -0
  28. package/dist/core/SplatProjection.js.map +1 -0
  29. package/dist/core/SplatSort.d.ts +26 -0
  30. package/dist/core/SplatSort.d.ts.map +1 -0
  31. package/dist/core/SplatSort.js +61 -0
  32. package/dist/core/SplatSort.js.map +1 -0
  33. package/dist/core/TexCoordZ.d.ts +94 -0
  34. package/dist/core/TexCoordZ.d.ts.map +1 -0
  35. package/dist/core/TexCoordZ.js +125 -0
  36. package/dist/core/TexCoordZ.js.map +1 -0
  37. package/dist/core/TextureDecode.d.ts +19 -0
  38. package/dist/core/TextureDecode.d.ts.map +1 -1
  39. package/dist/core/TextureDecode.js +53 -11
  40. package/dist/core/TextureDecode.js.map +1 -1
  41. package/dist/core/TileGeometry.d.ts +39 -0
  42. package/dist/core/TileGeometry.d.ts.map +1 -1
  43. package/dist/core/TileGeometry.js +133 -14
  44. package/dist/core/TileGeometry.js.map +1 -1
  45. package/dist/core/degradations.d.ts +37 -0
  46. package/dist/core/degradations.d.ts.map +1 -0
  47. package/dist/core/degradations.js +32 -0
  48. package/dist/core/degradations.js.map +1 -0
  49. package/dist/core/types.d.ts +36 -0
  50. package/dist/core/types.d.ts.map +1 -1
  51. package/dist/index.d.ts +7 -1
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +4 -1
  54. package/dist/index.js.map +1 -1
  55. package/dist/render/DatasetManager.d.ts +11 -0
  56. package/dist/render/DatasetManager.d.ts.map +1 -1
  57. package/dist/render/DatasetManager.js +2 -0
  58. package/dist/render/DatasetManager.js.map +1 -1
  59. package/dist/render/FeatureStyleManager.d.ts.map +1 -1
  60. package/dist/render/FeatureStyleManager.js +7 -5
  61. package/dist/render/FeatureStyleManager.js.map +1 -1
  62. package/dist/render/GeometryBuilder.d.ts +31 -0
  63. package/dist/render/GeometryBuilder.d.ts.map +1 -1
  64. package/dist/render/GeometryBuilder.js +229 -6
  65. package/dist/render/GeometryBuilder.js.map +1 -1
  66. package/dist/render/Picker.d.ts.map +1 -1
  67. package/dist/render/Picker.js +19 -5
  68. package/dist/render/Picker.js.map +1 -1
  69. package/dist/render/S3MViewer.d.ts +106 -379
  70. package/dist/render/S3MViewer.d.ts.map +1 -1
  71. package/dist/render/S3MViewer.js +324 -1494
  72. package/dist/render/S3MViewer.js.map +1 -1
  73. package/dist/render/SplatMaterial.d.ts +103 -0
  74. package/dist/render/SplatMaterial.d.ts.map +1 -0
  75. package/dist/render/SplatMaterial.js +388 -0
  76. package/dist/render/SplatMaterial.js.map +1 -0
  77. package/dist/render/SplatSorter.d.ts +14 -0
  78. package/dist/render/SplatSorter.d.ts.map +1 -0
  79. package/dist/render/SplatSorter.js +66 -0
  80. package/dist/render/SplatSorter.js.map +1 -0
  81. package/dist/render/StyleMaterial.d.ts +48 -0
  82. package/dist/render/StyleMaterial.d.ts.map +1 -1
  83. package/dist/render/StyleMaterial.js +292 -69
  84. package/dist/render/StyleMaterial.js.map +1 -1
  85. package/dist/render/TextureFactory.d.ts +10 -0
  86. package/dist/render/TextureFactory.d.ts.map +1 -1
  87. package/dist/render/TextureFactory.js +35 -5
  88. package/dist/render/TextureFactory.js.map +1 -1
  89. package/dist/render/lightingPreset.d.ts +53 -0
  90. package/dist/render/lightingPreset.d.ts.map +1 -0
  91. package/dist/render/lightingPreset.js +36 -0
  92. package/dist/render/lightingPreset.js.map +1 -0
  93. package/dist/render/materialAlpha.d.ts +51 -0
  94. package/dist/render/materialAlpha.d.ts.map +1 -0
  95. package/dist/render/materialAlpha.js +68 -0
  96. package/dist/render/materialAlpha.js.map +1 -0
  97. package/dist/render/viewer/EdgeDecorator.d.ts +206 -0
  98. package/dist/render/viewer/EdgeDecorator.d.ts.map +1 -0
  99. package/dist/render/viewer/EdgeDecorator.js +628 -0
  100. package/dist/render/viewer/EdgeDecorator.js.map +1 -0
  101. package/dist/render/viewer/HeightIndex.d.ts +36 -0
  102. package/dist/render/viewer/HeightIndex.d.ts.map +1 -0
  103. package/dist/render/viewer/HeightIndex.js +98 -0
  104. package/dist/render/viewer/HeightIndex.js.map +1 -0
  105. package/dist/render/viewer/LightRig.d.ts +88 -0
  106. package/dist/render/viewer/LightRig.d.ts.map +1 -0
  107. package/dist/render/viewer/LightRig.js +229 -0
  108. package/dist/render/viewer/LightRig.js.map +1 -0
  109. package/dist/render/viewer/PickService.d.ts +109 -0
  110. package/dist/render/viewer/PickService.d.ts.map +1 -0
  111. package/dist/render/viewer/PickService.js +591 -0
  112. package/dist/render/viewer/PickService.js.map +1 -0
  113. package/dist/render/viewer/SelectionDecorator.d.ts +84 -0
  114. package/dist/render/viewer/SelectionDecorator.d.ts.map +1 -0
  115. package/dist/render/viewer/SelectionDecorator.js +339 -0
  116. package/dist/render/viewer/SelectionDecorator.js.map +1 -0
  117. package/dist/render/viewer/SplatLayer.d.ts +26 -0
  118. package/dist/render/viewer/SplatLayer.d.ts.map +1 -0
  119. package/dist/render/viewer/SplatLayer.js +44 -0
  120. package/dist/render/viewer/SplatLayer.js.map +1 -0
  121. package/dist/render/viewer/types.d.ts +250 -0
  122. package/dist/render/viewer/types.d.ts.map +1 -0
  123. package/dist/render/viewer/types.js +3 -0
  124. package/dist/render/viewer/types.js.map +1 -0
  125. package/dist/tileset/S3MTileset.d.ts +42 -1
  126. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  127. package/dist/tileset/S3MTileset.js +52 -2
  128. package/dist/tileset/S3MTileset.js.map +1 -1
  129. package/dist/vendor/browser-node-stub.js +10 -0
  130. package/dist/worker/s3mWorker.js +1166 -979
  131. package/dist/worker/s3mWorker.js.map +1 -1
  132. package/docs/API.md +55 -1
  133. package/docs/FORMAT-NOTES.md +362 -0
  134. package/package.json +1 -1
  135. package/src/core/Draco.ts +1 -2
  136. package/src/core/GaussianSplat.ts +175 -0
  137. package/src/core/S3M1Parser.ts +330 -24
  138. package/src/core/S3MBParser.ts +18 -0
  139. package/src/core/S3MBlockParser.ts +83 -27
  140. package/src/core/SplatLod.ts +83 -0
  141. package/src/core/SplatProjection.ts +231 -0
  142. package/src/core/SplatSort.ts +92 -0
  143. package/src/core/TexCoordZ.ts +160 -0
  144. package/src/core/TextureDecode.ts +63 -11
  145. package/src/core/TileGeometry.ts +151 -14
  146. package/src/core/degradations.ts +74 -0
  147. package/src/core/types.ts +32 -0
  148. package/src/index.ts +8 -1
  149. package/src/render/DatasetManager.ts +10 -0
  150. package/src/render/FeatureStyleManager.ts +8 -6
  151. package/src/render/GeometryBuilder.ts +271 -7
  152. package/src/render/Picker.ts +21 -5
  153. package/src/render/S3MViewer.ts +358 -1727
  154. package/src/render/SplatMaterial.ts +433 -0
  155. package/src/render/SplatSorter.ts +66 -0
  156. package/src/render/StyleMaterial.ts +339 -69
  157. package/src/render/TextureFactory.ts +39 -5
  158. package/src/render/lightingPreset.ts +76 -0
  159. package/src/render/materialAlpha.ts +75 -0
  160. package/src/render/viewer/EdgeDecorator.ts +722 -0
  161. package/src/render/viewer/HeightIndex.ts +94 -0
  162. package/src/render/viewer/LightRig.ts +267 -0
  163. package/src/render/viewer/PickService.ts +651 -0
  164. package/src/render/viewer/SelectionDecorator.ts +377 -0
  165. package/src/render/viewer/SplatLayer.ts +44 -0
  166. package/src/render/viewer/types.ts +258 -0
  167. package/src/tileset/S3MTileset.ts +78 -2
  168. package/src/vendor/browser-node-stub.js +10 -0
  169. package/dist/browser/assets/s3mWorker-BjPOUgjg.js +0 -1
@@ -207,6 +207,17 @@ export interface S3MStyle {
207
207
  * 绕过光照夹角直接补偿表面暗部底色,让 TOP_SIDE / 自定义等模式下的背光阴影面保持通透、不至于过黑。
208
208
  */
209
209
  ambientGain?: number;
210
+ /**
211
+ * 金属度(0~1)。不传 = 保持数据/材质自身的值(实测 S3M 数据恒为 0)。
212
+ *
213
+ * 只对 pbr 光照模型有效(`materialQuality: 'lambert'` 时忽略)。
214
+ * ⚠️ 金属度越高,漫反射越少、几乎全靠环境反射成像:场景没有 `scene.environment`
215
+ * 时会明显发黑(只剩两盏平行光的高光点)。要用先把环境贴图挂上
216
+ * (`viewer.setEnvironment(url)` / `layer.setEnvironment(url)`)。
217
+ */
218
+ metalness?: number;
219
+ /** 粗糙度(0~1)。不传 = 保持数据自身的值(S3M 的 shininess 映射,常见 0.95) */
220
+ roughness?: number;
210
221
  /** 半透明时是否保持深度写入(默认 true,保持写入可彻底遮挡背面与内部棱线,避免背面透出) */
211
222
  depthWrite?: boolean;
212
223
  /**
@@ -283,6 +294,26 @@ export function getMaterialQuality(): MaterialQuality {
283
294
  return materialQuality;
284
295
  }
285
296
 
297
+ /**
298
+ * 颜色管线:
299
+ * - `'linear'`(默认):纹理 sRGB 解码 → 线性 PBR 光照 → linear→sRGB 输出,three 的标准姿势;
300
+ * - `'direct'`:官方 iClient3D 的做法 —— 字节值不解码、sRGB 数值空间直乘 Lambert
301
+ * (0.8·NdotL + 0.7)、输出前抵消 renderer 的 OETF。与 `setSrgbTextureDecode(false)`
302
+ * 配套使用,`lightingPreset: 'supermap'` 会同时打开两者。
303
+ *
304
+ * 与 materialQuality 同一套全局约定:只影响之后创建的材质,已加载瓦片要重载才换。
305
+ */
306
+ export type ColorPipeline = 'linear' | 'direct';
307
+ let colorPipeline: ColorPipeline = 'linear';
308
+
309
+ export function setColorPipeline(p: ColorPipeline): void {
310
+ colorPipeline = p === 'direct' ? 'direct' : 'linear';
311
+ }
312
+
313
+ export function getColorPipeline(): ColorPipeline {
314
+ return colorPipeline;
315
+ }
316
+
286
317
  export type StyledMaterial = (THREE.MeshStandardMaterial | THREE.MeshLambertMaterial) & {
287
318
  userData: {
288
319
  uSelBatch: { value: number };
@@ -315,6 +346,8 @@ DUMMY_BATCH_TEX.needsUpdate = true;
315
346
  export function createSharedUniforms(): StyleUniforms {
316
347
  return {
317
348
  uMode: { value: STYLE.TEXTURE },
349
+ /** 主光世界方向(direct 管线的官方 Lambert 用;headlamp/fixed 都随 _updateLights 更新) */
350
+ uSunDirection: { value: new THREE.Vector3(0.5, 0.5, 0.7071).normalize() },
318
351
  uColorA: { value: new THREE.Color('#dfe6ee') },
319
352
  uColorB: { value: new THREE.Color('#1d4ed8') },
320
353
  uColorC: { value: new THREE.Color('#fbbf24') },
@@ -350,6 +383,10 @@ export function createSharedUniforms(): StyleUniforms {
350
383
  uTexMix: { value: 0 },
351
384
  /** 基础底色增益 / 暗部补偿(0~1,默认 0.0) */
352
385
  uAmbientGain: { value: 0 },
386
+ /** 金属度覆盖(<0 = 不覆盖,用材质自身的值) */
387
+ uMetalness: { value: -1 },
388
+ /** 粗糙度覆盖(<0 = 不覆盖,用材质自身的值) */
389
+ uRoughness: { value: -1 },
353
390
  /** 双纹理交叉淡化插值系数(0 = 纯 uBatchTex, 1 = 纯 uBatchTex2) */
354
391
  uBatchMix: { value: 0 },
355
392
  /** GPU 路径数据采样纹理 */
@@ -385,7 +422,8 @@ export function createSharedUniforms(): StyleUniforms {
385
422
  }
386
423
  const VERT_PARS = /* glsl */ `
387
424
  attribute float aBatchId;
388
- attribute vec3 aColor;
425
+ // vec4:alpha 是「材质不透明、顶点半透明」这类数据的唯一透明度来源(官方 baseColorWithAlpha = vColor)
426
+ attribute vec4 aColor;
389
427
  #ifdef S3M_ATLAS
390
428
  attribute vec4 aAtlasRect;
391
429
  #ifdef S3M_ATLAS_SEC
@@ -400,6 +438,15 @@ const VERT_PARS = /* glsl */ `
400
438
  #ifdef USE_INSTANCING
401
439
  attribute float aInstanceBatchId;
402
440
  #endif
441
+ // uv 第三分量:官方 aTextureCoordMatrix,决定该顶点采不采样纹理 / 用图集哪一块。
442
+ // 解码必须在**顶点着色器**做:z 的量级是 65536~327680,片元里若落到 mediump(fp16
443
+ // 上限 65504)会直接溢出成 inf,官方也是在 VS 解完再用 varying 传。
444
+ #ifdef S3M_TEXCOORD_Z
445
+ attribute float aTexCoordZ;
446
+ varying vec4 vTexMatrix; // x=xTran y=yTran z=scale w=log2(scale*texWidth)
447
+ varying float vTexCoordTransform; // 原始 z,-90000 以下表示不采样
448
+ uniform vec2 uAtlasDim; // 与片元共用:纹理像素尺寸
449
+ #endif
403
450
  uniform float uSelBatch;
404
451
  uniform float uSelWhole;
405
452
  uniform float uSelElevation;
@@ -413,7 +460,7 @@ const VERT_PARS = /* glsl */ `
413
460
  uniform float uPathTime;
414
461
  uniform int uPathTexActive;
415
462
  varying float vBatchId;
416
- varying vec3 vVertColor;
463
+ varying vec4 vVertColor;
417
464
  varying float vWorldZ;
418
465
  varying vec3 vWorldNormal;
419
466
  varying vec3 vWorldPos;
@@ -443,29 +490,37 @@ const VERT_PARS = /* glsl */ `
443
490
 
444
491
  vec4 s3mSampleBatchRow0( sampler2D texA, sampler2D texB, vec2 uv, float mixVal ) {
445
492
  vec4 vA = texture2D( texA, uv );
446
- if ( mixVal <= 0.0001 ) return vA;
447
- vec4 vB = texture2D( texB, uv );
448
- vec4 res = mix( vA, vB, mixVal );
449
- if ( vA.r < 0.0 && vB.r >= 0.0 ) {
450
- res.rgb = vB.rgb;
451
- } else if ( vA.r >= 0.0 && vB.r < 0.0 ) {
452
- res.rgb = vA.rgb;
453
- }
454
- if ( vA.a < -0.5 || vB.a < -0.5 ) {
455
- res.a = ( mixVal >= 0.5 ) ? vB.a : vA.a;
493
+ vec4 res;
494
+ if ( mixVal <= 0.0001 ) {
495
+ res = vA;
496
+ } else {
497
+ vec4 vB = texture2D( texB, uv );
498
+ res = mix( vA, vB, mixVal );
499
+ if ( vA.r < 0.0 && vB.r >= 0.0 ) {
500
+ res.rgb = vB.rgb;
501
+ } else if ( vA.r >= 0.0 && vB.r < 0.0 ) {
502
+ res.rgb = vA.rgb;
503
+ }
504
+ if ( vA.a < -0.5 || vB.a < -0.5 ) {
505
+ res.a = ( mixVal >= 0.5 ) ? vB.a : vA.a;
506
+ }
456
507
  }
457
508
  return res;
458
509
  }
459
510
 
460
511
  vec4 s3mSampleBatchRow1( sampler2D texA, sampler2D texB, vec2 uv, float mixVal ) {
461
512
  vec4 vA = texture2D( texA, uv );
462
- if ( mixVal <= 0.0001 ) return vA;
463
- vec4 vB = texture2D( texB, uv );
464
- vec4 res = mix( vA, vB, mixVal );
465
- if ( vA.a == 0.0 && vB.a > 0.0 ) {
466
- res.a = mix( 1.0, vB.a, mixVal );
467
- } else if ( vA.a > 0.0 && vB.a == 0.0 ) {
468
- res.a = mix( vA.a, 1.0, mixVal );
513
+ vec4 res;
514
+ if ( mixVal <= 0.0001 ) {
515
+ res = vA;
516
+ } else {
517
+ vec4 vB = texture2D( texB, uv );
518
+ res = mix( vA, vB, mixVal );
519
+ if ( vA.a == 0.0 && vB.a > 0.0 ) {
520
+ res.a = mix( 1.0, vB.a, mixVal );
521
+ } else if ( vA.a > 0.0 && vB.a == 0.0 ) {
522
+ res.a = mix( vA.a, 1.0, mixVal );
523
+ }
469
524
  }
470
525
  return res;
471
526
  }
@@ -569,6 +624,31 @@ const VERT_WORLD = /* glsl */ `
569
624
  vWorldPos = s3mWorld.xyz;
570
625
  vBatchId = s3mBatchRaw();
571
626
  vVertColor = aColor;
627
+ #ifdef S3M_TEXCOORD_Z
628
+ // 官方 S3MTilesVS.js:192-198(getTextureMatrixFromZValue)+ bundle getTextureColor
629
+ // 的三档哨兵,档位编码在 vTexMatrix.z 的符号上:
630
+ // -2 → 白(z < -99900 档:官方 texCoordZ < -99900 返回 vec4(1.0),平涂顶点色面)
631
+ // -1 → 平铺(-99900 ≤ z < -9000:uv 原样采样)
632
+ // >0 → 图集子区域公式
633
+ vTexCoordTransform = aTexCoordZ;
634
+ vTexMatrix = vec4( 0.0, 0.0, 1.0, 0.0 );
635
+ if ( vTexCoordTransform < -99900.0 ) {
636
+ vTexMatrix.z = -2.0;
637
+ } else if ( vTexCoordTransform < -9000.0 ) {
638
+ vTexMatrix.z = -1.0;
639
+ } else {
640
+ float s3mNz = floor( vTexCoordTransform );
641
+ if ( s3mNz > 0.0 ) {
642
+ float nDel8 = floor( s3mNz / 256.0 );
643
+ float nDel16 = floor( nDel8 / 256.0 );
644
+ float nDel20 = floor( nDel16 / 16.0 );
645
+ vTexMatrix.x = nDel8 - nDel16 * 256.0;
646
+ vTexMatrix.y = s3mNz - nDel8 * 256.0;
647
+ vTexMatrix.z = 1.0 / pow( 2.0, nDel16 - nDel20 * 16.0 );
648
+ }
649
+ }
650
+ vTexMatrix.w = vTexMatrix.z > 0.0 ? log2( uAtlasDim.x * vTexMatrix.z ) : 0.0;
651
+ #endif
572
652
 
573
653
  #ifdef S3M_ATLAS
574
654
  vAtlasRect = aAtlasRect;
@@ -590,31 +670,85 @@ const VERT_WORLD = /* glsl */ `
590
670
  `;
591
671
 
592
672
  const FRAG_PARS = /* glsl */ `
673
+ // 纹理像素尺寸:图集分支用它把 rect 换算成像素宽,uv-z 分支用它算 uv 周期。
674
+ // 两个分支都需要,所以无条件声明(值恒为 map 的实际像素尺寸)。
675
+ uniform vec2 uAtlasDim;
676
+
677
+ /**
678
+ * 官方 CalculateMipLevel(vec2 版),逐字照抄 iClient3D 12.1.1。
679
+ * 用**未 fract** 的 uv 求屏幕空间导数 —— 先 fract 再求导会在平铺接缝处炸出
680
+ * 一个巨大导数,GPU 直接把整块砖采成最糊的 mip,表现为每块砖边缘一条暗线。
681
+ */
682
+ void s3mMipLevel( vec2 uv, vec2 texSize, float maxMip, out float mip ) {
683
+ vec2 dx = dFdx( uv * texSize.x );
684
+ vec2 dy = dFdy( uv * texSize.y );
685
+ float dotX = dot( dx, dx );
686
+ float dotY = dot( dy, dy );
687
+ float dMax = max( dotX, dotY );
688
+ float dMin = min( dotX, dotY );
689
+ float off = clamp( ( dMax - dMin ) / max( dMax + dMin, 1e-8 ), 0.0, 1.0 );
690
+ float d = dMax * ( 1.0 - off ) + dMin * off;
691
+ mip = clamp( 0.5 * log2( max( d, 1e-8 ) ), 0.0, maxMip - 1.62 );
692
+ }
693
+
694
+ #ifdef S3M_TEXCOORD_Z
695
+ // 矩阵在顶点着色器解好(vTexMatrix),这里只做 mip 收缩 + 采样。
696
+ varying vec4 vTexMatrix;
697
+
698
+ /**
699
+ * 官方 S3MTilesFS.js 的 getTextureColor → getTexColorForS3M → calculateTexCoord
700
+ * 逐行移植,数学等价物见 core/TexCoordZ.ts(单测钉住两边一致):
701
+ *
702
+ * vTexMatrix.z < 0 → 不采样,返回白(官方 if ( vTexMatrix.z < 0.0 ))
703
+ * fTile = scale * texWidth
704
+ * inner = 2^mip / fTile; f = clamp( fract(uv), inner, 1 - inner )
705
+ * out = ( f + T ) * scale
706
+ *
707
+ * ★ 采样必须用**显式 LOD**(官方 textureLod / texture2DLodEXT)。
708
+ * 交给硬件自动求导时,导数从 (fract(uv)+T)*scale 上取,而 fract 每个周期
709
+ * 都有一次跳变,接缝像素的导数会炸掉 → GPU 采到最糊的 mip,画面上每隔一个
710
+ * 周期就出现一条暗带 / 摩尔纹。近处周期密,这些带子最明显。
711
+ */
712
+ vec4 s3mTexCoordZUnit( vec2 uv, vec4 m, sampler2D tex, float texWidth ) {
713
+ // 官方 bundle getTextureColor 三档语义(逐行核对 SuperMap3D.js):
714
+ // texCoordZ < -99900 → vec4(1.0) 纯白,不采样(平涂顶点色面,如 SMID 3464)
715
+ // texCoordZ < -9000 → uv 原样平铺(自动 mip 平均成贴图色)
716
+ // 其余 → 图集子区域公式
717
+ // 之前误读成「-99999 也平铺」,3464 的米制 uv 就把整张纹理平铺上百次 = 「uv 整图」。
718
+ // 单出口结构:ANGLE 把返回值翻译成 out 参数,函数内提前 return
719
+ // 会让 D3D 编译器报 X4000「use of potentially uninitialized variable」。
720
+ vec4 s3mResult;
721
+ if ( m.z < -1.5 ) {
722
+ s3mResult = vec4( 1.0 );
723
+ } else if ( m.z < 0.0 ) {
724
+ s3mResult = texture2D( tex, uv );
725
+ } else {
726
+ float fTile = max( m.z * texWidth, 1.0 );
727
+ float mip = 0.0;
728
+ s3mMipLevel( uv, vec2( fTile ), m.w, mip );
729
+
730
+ vec2 f = fract( uv );
731
+ float inner = pow( 2.0, mip ) / fTile;
732
+ f = clamp( f, vec2( inner ), vec2( 1.0 - inner ) );
733
+ vec2 c = ( f + m.xy ) * m.z;
734
+
735
+ #if __VERSION__ >= 300
736
+ s3mResult = textureLod( tex, c, mip );
737
+ #else
738
+ s3mResult = texture2D( tex, c );
739
+ #endif
740
+ }
741
+ return s3mResult;
742
+ }
743
+ #endif
593
744
  #ifdef S3M_ATLAS
594
745
  varying vec4 vAtlasRect;
595
- uniform vec2 uAtlasDim;
596
746
  #ifdef S3M_ATLAS_SEC
597
747
  uniform sampler2D uSecMap;
598
748
  uniform vec2 uAtlasDim1;
599
749
  varying vec2 vSecUv;
600
750
  varying vec4 vSecRect;
601
751
  #endif
602
- /**
603
- * 官方 CalculateMipLevel(vec2 版),逐字照抄 iClient3D 12.1.1。
604
- * 用**未 fract** 的 uv 求屏幕空间导数 —— 先 fract 再求导会在平铺接缝处炸出
605
- * 一个巨大导数,GPU 直接把整块砖采成最糊的 mip,表现为每块砖边缘一条暗线。
606
- */
607
- void s3mMipLevel( vec2 uv, vec2 texSize, float maxMip, out float mip ) {
608
- vec2 dx = dFdx( uv * texSize.x );
609
- vec2 dy = dFdy( uv * texSize.y );
610
- float dotX = dot( dx, dx );
611
- float dotY = dot( dy, dy );
612
- float dMax = max( dotX, dotY );
613
- float dMin = min( dotX, dotY );
614
- float off = clamp( ( dMax - dMin ) / max( dMax + dMin, 1e-8 ), 0.0, 1.0 );
615
- float d = dMax * ( 1.0 - off ) + dMin * off;
616
- mip = clamp( 0.5 * log2( max( d, 1e-8 ) ), 0.0, maxMip - 1.62 );
617
- }
618
752
 
619
753
  /**
620
754
  * 按指定 mip 档采样子区域(官方 czm_getTexColorForS3M 的内层循环体)。
@@ -646,17 +780,22 @@ const FRAG_PARS = /* glsl */ `
646
780
  * 因为内联金字塔只有 2 的整数次幂,中间档必须靠混合。
647
781
  */
648
782
  vec4 s3mAtlasUnit( vec2 uv, vec4 rect, vec2 atlasDim, sampler2D tex ) {
649
- // 退化矩形 = 官方 batchId < 0:该纹理单元不参与合成
650
- if ( rect.z <= 0.0 || rect.w <= 0.0 ) return vec4( 1.0 );
651
- vec2 dimPx = vec2( rect.z * atlasDim.x, rect.w * atlasDim.y );
652
- vec2 texSize = vec2( dimPx.x * ( 2.0 / 3.0 ), dimPx.y );
653
- float maxMip = log2( max( min( texSize.x, texSize.y ), 1.0 ) );
654
- vec2 scale = texSize / atlasDim;
655
- float mip = 0.0;
656
- s3mMipLevel( uv, texSize, maxMip, mip );
657
- vec4 hi = s3mAtlasLod( uv, rect.xy, scale, texSize, tex, ceil( mip ) );
658
- vec4 lo = s3mAtlasLod( uv, rect.xy, scale, texSize, tex, floor( mip ) );
659
- return hi * 0.5 + lo * 0.5;
783
+ // 退化矩形 = 官方 batchId < 0:该纹理单元不参与合成(单出口,同 X4000 规避)
784
+ vec4 s3mResult;
785
+ if ( rect.z <= 0.0 || rect.w <= 0.0 ) {
786
+ s3mResult = vec4( 1.0 );
787
+ } else {
788
+ vec2 dimPx = vec2( rect.z * atlasDim.x, rect.w * atlasDim.y );
789
+ vec2 texSize = vec2( dimPx.x * ( 2.0 / 3.0 ), dimPx.y );
790
+ float maxMip = log2( max( min( texSize.x, texSize.y ), 1.0 ) );
791
+ vec2 scale = texSize / atlasDim;
792
+ float mip = 0.0;
793
+ s3mMipLevel( uv, texSize, maxMip, mip );
794
+ vec4 hi = s3mAtlasLod( uv, rect.xy, scale, texSize, tex, ceil( mip ) );
795
+ vec4 lo = s3mAtlasLod( uv, rect.xy, scale, texSize, tex, floor( mip ) );
796
+ s3mResult = hi * 0.5 + lo * 0.5;
797
+ }
798
+ return s3mResult;
660
799
  }
661
800
  #endif
662
801
  uniform int uMode;
@@ -687,11 +826,13 @@ const FRAG_PARS = /* glsl */ `
687
826
  uniform float uRoofThreshold;
688
827
  uniform float uTexMix;
689
828
  uniform float uAmbientGain;
829
+ uniform float uMetalness;
830
+ uniform float uRoughness;
690
831
  uniform float uOpacity;
691
832
  uniform float uTileFade;
692
833
  uniform float uFade;
693
834
  varying float vBatchId;
694
- varying vec3 vVertColor;
835
+ varying vec4 vVertColor;
695
836
  varying float vWorldZ;
696
837
  varying vec3 vWorldNormal;
697
838
  varying vec3 vWorldPos;
@@ -700,29 +841,37 @@ const FRAG_PARS = /* glsl */ `
700
841
 
701
842
  vec4 s3mSampleBatchRow0( sampler2D texA, sampler2D texB, vec2 uv, float mixVal ) {
702
843
  vec4 vA = texture2D( texA, uv );
703
- if ( mixVal <= 0.0001 ) return vA;
704
- vec4 vB = texture2D( texB, uv );
705
- vec4 res = mix( vA, vB, mixVal );
706
- if ( vA.r < 0.0 && vB.r >= 0.0 ) {
707
- res.rgb = vB.rgb;
708
- } else if ( vA.r >= 0.0 && vB.r < 0.0 ) {
709
- res.rgb = vA.rgb;
710
- }
711
- if ( vA.a < -0.5 || vB.a < -0.5 ) {
712
- res.a = ( mixVal >= 0.5 ) ? vB.a : vA.a;
844
+ vec4 res;
845
+ if ( mixVal <= 0.0001 ) {
846
+ res = vA;
847
+ } else {
848
+ vec4 vB = texture2D( texB, uv );
849
+ res = mix( vA, vB, mixVal );
850
+ if ( vA.r < 0.0 && vB.r >= 0.0 ) {
851
+ res.rgb = vB.rgb;
852
+ } else if ( vA.r >= 0.0 && vB.r < 0.0 ) {
853
+ res.rgb = vA.rgb;
854
+ }
855
+ if ( vA.a < -0.5 || vB.a < -0.5 ) {
856
+ res.a = ( mixVal >= 0.5 ) ? vB.a : vA.a;
857
+ }
713
858
  }
714
859
  return res;
715
860
  }
716
861
 
717
862
  vec4 s3mSampleBatchRow1( sampler2D texA, sampler2D texB, vec2 uv, float mixVal ) {
718
863
  vec4 vA = texture2D( texA, uv );
719
- if ( mixVal <= 0.0001 ) return vA;
720
- vec4 vB = texture2D( texB, uv );
721
- vec4 res = mix( vA, vB, mixVal );
722
- if ( vA.a == 0.0 && vB.a > 0.0 ) {
723
- res.a = mix( 1.0, vB.a, mixVal );
724
- } else if ( vA.a > 0.0 && vB.a == 0.0 ) {
725
- res.a = mix( vA.a, 1.0, mixVal );
864
+ vec4 res;
865
+ if ( mixVal <= 0.0001 ) {
866
+ res = vA;
867
+ } else {
868
+ vec4 vB = texture2D( texB, uv );
869
+ res = mix( vA, vB, mixVal );
870
+ if ( vA.a == 0.0 && vB.a > 0.0 ) {
871
+ res.a = mix( 1.0, vB.a, mixVal );
872
+ } else if ( vA.a > 0.0 && vB.a == 0.0 ) {
873
+ res.a = mix( vA.a, 1.0, mixVal );
874
+ }
726
875
  }
727
876
  return res;
728
877
  }
@@ -762,6 +911,10 @@ const FRAG_COLOR = /* glsl */ `
762
911
  vec3 s3mBase = diffuseColor.rgb; // 已含 diffuse × map
763
912
  // 图层整体透明度(共享 uniform)+ 单瓦片淡入系数(逐材质),逐帧改都不产生重编译
764
913
  diffuseColor.a *= uOpacity * uTileFade;
914
+ // 顶点色 alpha:官方 outColor = baseColorWithAlpha * uDiffuseColor,alpha 走的就是这条路。
915
+ // 实测 3D-BIMbuilding-2 / SMID 4421 的材质无纹理、diffuse.a = 1,透明度只存在于
916
+ // 顶点色 alpha(u8 63/255 ≈ 0.247)。不乘进来,这一片就是实心色。
917
+ diffuseColor.a *= vVertColor.a;
765
918
  // 所有按单体取值(配色 / 状态纹理 / 选中判定)都走取整后的 id,避免插值噪声
766
919
  float s3mBid = s3mBatchId();
767
920
 
@@ -773,7 +926,7 @@ const FRAG_COLOR = /* glsl */ `
773
926
  diffuseColor.rgb = uColorA;
774
927
  } else if ( uMode == 1 ) {
775
928
  // 原色:贴图 × 顶点色
776
- diffuseColor.rgb = s3mBase * vVertColor;
929
+ diffuseColor.rgb = s3mBase * vVertColor.rgb;
777
930
  } else if ( uMode == 2 ) {
778
931
  diffuseColor.rgb = uColorA;
779
932
  } else if ( uMode == 3 ) {
@@ -808,7 +961,7 @@ const FRAG_COLOR = /* glsl */ `
808
961
 
809
962
  // 叠加在原色贴图上:保留窗框、玻璃幕墙、材质纹理与顶点色
810
963
  if ( uMode != 1 && uTexMix > 0.001 ) {
811
- vec3 origTex = s3mBase * vVertColor;
964
+ vec3 origTex = s3mBase * vVertColor.rgb;
812
965
  vec3 overlay = origTex * diffuseColor.rgb * 1.35;
813
966
  diffuseColor.rgb = mix( diffuseColor.rgb, overlay, uTexMix );
814
967
  }
@@ -870,6 +1023,36 @@ const FRAG_COLOR = /* glsl */ `
870
1023
  diffuseColor.rgb *= uFade;
871
1024
  `;
872
1025
 
1026
+ /**
1027
+ * direct 管线(lightingPreset: 'supermap' 完整版)的着色器片段。
1028
+ *
1029
+ * 官方整条管线(S3MTilesFS.js):贴图字节值不解码 → sRGB 数值空间直乘
1030
+ * Lambert(Sun 0.8 / Ambient 0.7,无高光无 tonemap)→ 直出。
1031
+ * 我们保留 three 的 sRGB 输出 OETF(不动共享 renderer,底图不受影响),
1032
+ * 在片元末尾把颜色先压回 linear,OETF 再把它还原成官方直乘的原始数值。
1033
+ */
1034
+ const FRAG_DIRECT_PARS = /* glsl */ `
1035
+ #ifdef S3M_DIRECT_COLOR
1036
+ uniform vec3 uSunDirection;
1037
+
1038
+ // 精确 sRGB EOTF 逆(与 renderer 输出端的 OETF 互逆),不是 pow 2.2 近似
1039
+ vec3 s3mSRGBToLinear( vec3 c ) {
1040
+ return mix( c / 12.92, pow( ( c + 0.055 ) / 1.055, vec3( 2.4 ) ), step( vec3( 0.04045 ), c ) );
1041
+ }
1042
+ #endif
1043
+ `;
1044
+
1045
+ const FRAG_DIRECT_MAIN = /* glsl */ `
1046
+ #ifdef S3M_DIRECT_COLOR
1047
+ // 官方 S3MTilesFS.js:255-263:uDiffuseColor.rgb * (SunLightColor * NdotL + AmbientLightColor)
1048
+ // Sun=0.8、Ambient=0.7;uAmbientColor 数据里恒为 (1,1,1),并入常数
1049
+ float s3mNL = max( dot( s3mNorm, normalize( uSunDirection ) ), 0.0 );
1050
+ vec3 s3mDirectOut = diffuseColor.rgb * ( 0.8 * s3mNL + vec3( 0.7 ) );
1051
+ s3mDirectOut = s3mSRGBToLinear( max( s3mDirectOut, vec3( 0.0 ) ) );
1052
+ outgoingLight = s3mDirectOut;
1053
+ #endif
1054
+ `;
1055
+
873
1056
  const FRAG_EMISSIVE = /* glsl */ `
874
1057
  float s3mSelE = max( step( 0.0, uSelBatch ) * step( abs( s3mBatchId() - uSelBatch ), 0.5 ), uSelWhole );
875
1058
  // glowOnly 模式下表面不自发光,只保留描边边框;其他模式下适度发光
@@ -909,6 +1092,17 @@ export interface StyledMaterialOptions {
909
1092
  alphaTest?: number;
910
1093
  /** 贴图为 S3MBLOCK 图集:几何需带 aAtlasRect,平铺改由片元按子区域做 */
911
1094
  atlas?: boolean;
1095
+ /**
1096
+ * 几何带 uv 第三分量(`aTexCoordZ`,来自文件里的 `custom0`)。
1097
+ *
1098
+ * 官方 iClient3D 用它决定采样方式:`z < -9000` 时 uv 原样平铺整图(非白)
1099
+ * `vec4(1.0)`(纯色),否则把 uv 按 `czm_getTextureMatrixFromZValue` 解出的
1100
+ * 平移/缩放送进图集子区域。BIM / OSGB 转出来的数据大量依赖它——忽略就会把
1101
+ * 本该纯色的墙面贴上整张材质图并高频重复。
1102
+ *
1103
+ * 不传 = 与改动前完全一致(走 three 原生的 `texture2D( map, vMapUv )`)。
1104
+ */
1105
+ texCoordZ?: boolean;
912
1106
  /** 图集材质的第二套纹理(官方 BakeTex,光照烘焙),与 base 相乘 */
913
1107
  secMap?: THREE.Texture | null;
914
1108
  }
@@ -938,6 +1132,24 @@ const FRAG_NORMAL = /* glsl */ `
938
1132
  }
939
1133
  `;
940
1134
 
1135
+ /**
1136
+ * 金属度 / 粗糙度覆盖。
1137
+ *
1138
+ * 注入点必须是 `#include <metalnessmap_fragment>` 之后 —— 两个 chunk 各自
1139
+ * `float roughnessFactor = roughness;` / `float metalnessFactor = metalness;`,
1140
+ * 之前引用会「变量未声明」,之后就落在 `lights_physical_fragment` 之前,
1141
+ * 改完立刻参与 GGX 计算。
1142
+ *
1143
+ * 用共享 uniform 而不是改 `material.metalness`:材质是一 mesh 一份,逐 mesh 改是
1144
+ * O(meshes);uniform 改一次全场景生效,也能走样式动画插值。
1145
+ *
1146
+ * 注意:`lambert` 光照模型里没有这两个 include,注入是 no-op,金属度自然无效。
1147
+ */
1148
+ const FRAG_SURFACE = /* glsl */ `
1149
+ if ( uMetalness >= 0.0 ) metalnessFactor = clamp( uMetalness, 0.0, 1.0 );
1150
+ if ( uRoughness >= 0.0 ) roughnessFactor = clamp( uRoughness, 0.04, 1.0 );
1151
+ `;
1152
+
941
1153
  export function createStyledMaterial(opts: StyledMaterialOptions): StyledMaterial {
942
1154
  const uniforms = opts.uniforms;
943
1155
  // 两种光照模型共用的参数。注入的 chunk 只动 diffuseColor / normal / transformed,
@@ -984,9 +1196,16 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
984
1196
 
985
1197
  // 图集材质:只有几何确实带了 aAtlasRect 才开这个宏,否则缺属性会采到 (0,0) 的默认值
986
1198
  const atlas = !!opts.atlas && !!opts.map;
1199
+ // uv 第三分量:同样要求几何真的带 aTexCoordZ,且没有贴图时没有意义。
1200
+ // atlas 优先——S3MBLOCK 图集的 aAtlasRect 已经表达了子区域,两者不会同时出现。
1201
+ const texCoordZ = !atlas && !!opts.texCoordZ && !!opts.map;
987
1202
  const secMap = atlas ? (opts.secMap ?? null) : null;
988
1203
  if (atlas) mat.defines = { ...(mat.defines as Record<string, string>), S3M_ATLAS: '' };
1204
+ if (texCoordZ) mat.defines = { ...(mat.defines as Record<string, string>), S3M_TEXCOORD_Z: '' };
989
1205
  if (secMap) mat.defines = { ...mat.defines, S3M_ATLAS_SEC: '' };
1206
+ // supermap 完整版:官方 sRGB 直乘管线(配合 setSrgbTextureDecode(false))
1207
+ const direct = colorPipeline === 'direct';
1208
+ if (direct) mat.defines = { ...mat.defines, S3M_DIRECT_COLOR: '' };
990
1209
  // 官方 uTextureDim = (base宽, base高, bake宽, bake高),取实际绑定纹理的像素尺寸
991
1210
  const uAtlasDim = { value: new THREE.Vector2(opts.map?.image?.width || 1, opts.map?.image?.height || 1) };
992
1211
  const uAtlasDim1 = { value: new THREE.Vector2(secMap?.image?.width || 1, secMap?.image?.height || 1) };
@@ -1002,6 +1221,8 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
1002
1221
  uBatchTexActive,
1003
1222
  uTileFade,
1004
1223
  uSecMap: { value: secMap },
1224
+ // direct 管线需要主光方向;没接共享 uniforms 时(单测/独立构建)给个默认值保证能编译
1225
+ ...(direct ? { uSunDirection: uniforms?.uSunDirection ?? { value: new THREE.Vector3(0.5, 0.5, 0.7071).normalize() } } : {}),
1005
1226
  uAtlasDim,
1006
1227
  uAtlasDim1,
1007
1228
  });
@@ -1013,9 +1234,17 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
1013
1234
  .replace('#include <worldpos_vertex>', VERT_WORLD);
1014
1235
 
1015
1236
  shader.fragmentShader = shader.fragmentShader
1016
- .replace('#include <common>', `#include <common>\n${FRAG_PARS}`)
1237
+ .replace('#include <common>', `#include <common>\n${FRAG_PARS}${direct ? FRAG_DIRECT_PARS : ''}`)
1017
1238
  .replace('#include <color_fragment>', `#include <color_fragment>\n${FRAG_COLOR}`)
1018
1239
  .replace('#include <emissivemap_fragment>', `#include <emissivemap_fragment>\n${FRAG_EMISSIVE}`);
1240
+ if (direct) {
1241
+ // s3mNorm(FRAG_COLOR 里定义)之后、three 组装 outgoingLight 之前整体接管:
1242
+ // PBR 的 specular / 环境项全被丢弃 —— 官方模型里 totalSpecularLight 恒为 0
1243
+ shader.fragmentShader = shader.fragmentShader.replace(
1244
+ '#include <opaque_fragment>',
1245
+ `${FRAG_DIRECT_MAIN}\n#include <opaque_fragment>`
1246
+ );
1247
+ }
1019
1248
 
1020
1249
  if (atlas) {
1021
1250
  // onBeforeCompile 收到的还是未展开的 `#include <map_fragment>`,不是 include 里的
@@ -1030,6 +1259,16 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
1030
1259
  '#include <map_fragment>',
1031
1260
  `${atlasMapFragment}${secMap ? `\n${FRAG_BAKE}` : ''}`
1032
1261
  );
1262
+ } else if (texCoordZ) {
1263
+ // 普通 S3M + uv 第三分量:逐顶点决定「用不用贴图 / 用哪块子区域」。
1264
+ // 官方就是这个分支让 BIM 的墙体保持纯色(custom0 = -99999 的顶点直接返回白),
1265
+ // 而我们过去一律整张平铺,才会出现「一个面重复铺满整张 uv 贴片」。
1266
+ const texCoordZFragment = /* glsl */ `
1267
+ #ifdef USE_MAP
1268
+ diffuseColor *= s3mTexCoordZUnit( vMapUv, vTexMatrix, map, uAtlasDim.x );
1269
+ #endif
1270
+ `;
1271
+ shader.fragmentShader = shader.fragmentShader.replace('#include <map_fragment>', texCoordZFragment);
1033
1272
  } else if (secMap) {
1034
1273
  shader.fragmentShader = shader.fragmentShader.replace('#include <map_fragment>', `#include <map_fragment>\n${FRAG_BAKE}`);
1035
1274
  }
@@ -1039,12 +1278,19 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
1039
1278
  '#include <normal_fragment_begin>',
1040
1279
  `#include <normal_fragment_begin>\n${FRAG_NORMAL}`
1041
1280
  );
1281
+
1282
+ // 金属度 / 粗糙度覆盖:紧跟在 metalnessmap_fragment 之后
1283
+ // (lambert 模板里没有这个 include,replace 会静默 no-op,符合预期)
1284
+ shader.fragmentShader = shader.fragmentShader.replace(
1285
+ '#include <metalnessmap_fragment>',
1286
+ `#include <metalnessmap_fragment>\n${FRAG_SURFACE}`
1287
+ );
1042
1288
  };
1043
1289
 
1044
1290
  // 不同材质共用同一段注入代码,必须让 three 认为它们的 program 可复用;
1045
1291
  // 但图集 / 烘焙两套宏会改注入结果,必须进 cacheKey,否则会复用错 program。
1046
1292
  mat.customProgramCacheKey = () =>
1047
- `s3m-styled${atlas ? '-atlas' : ''}${secMap ? '-bake' : ''}`;
1293
+ `s3m-styled${atlas ? '-atlas' : ''}${texCoordZ ? '-texz' : ''}${secMap ? '-bake' : ''}${direct ? '-direct' : ''}`;
1048
1294
 
1049
1295
  mat.userData.uSelBatch = uSelBatch;
1050
1296
  mat.userData.uSelWhole = uSelWhole;
@@ -1069,6 +1315,30 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
1069
1315
  return mat;
1070
1316
  }
1071
1317
 
1318
+ /**
1319
+ * 恢复材质的「基础」混合状态(淡入结束 / 清除单体淡化 / 全局透明度滑回 1 时调用)。
1320
+ *
1321
+ * 材质的最终透明 = **自身语义透明**(alphaMode blend / 顶点色 alpha < 1 / 水面,
1322
+ * 创建时算好记在 `userData.s3mOpts.transparent`)|| 图层全局半透明。
1323
+ *
1324
+ * 以前各处恢复逻辑只看图层透明度(`transparent && !layerTransparent → false`),
1325
+ * 会把自身就是透明的材质(如 3D-BIMbuilding-2 的 SMID 4421)误关成不透明——
1326
+ * 症状是「刚进场景没透明度,切换材质质感重建后又有了」。
1327
+ *
1328
+ * depthWrite 跟随:自身 blend 材质保持创建时的 false;临时透明(淡入 / 全局淡出)
1329
+ * 则尊重调用方的 keepDepthWrite(默认 true,保持深度写入防漏背面)。
1330
+ */
1331
+ export function restoreBaseBlendState(
1332
+ mat: THREE.Material,
1333
+ layerTransparent: boolean,
1334
+ keepDepthWrite = true
1335
+ ): void {
1336
+ const opts = (mat as StyledMaterial).userData?.s3mOpts as StyledMaterialOptions | undefined;
1337
+ const baseTransparent = opts?.transparent === true;
1338
+ mat.transparent = baseTransparent || layerTransparent;
1339
+ mat.depthWrite = baseTransparent ? false : mat.transparent ? keepDepthWrite : true;
1340
+ }
1341
+
1072
1342
  /* ------------------------------------------------------------------ 选中描边 */
1073
1343
 
1074
1344
  const OUTLINE_VERT = /* glsl */ `