s3m-three 0.3.0 → 0.3.1

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 (109) 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 +2 -1
  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.map +1 -1
  50. package/dist/render/DatasetManager.js +5 -3
  51. package/dist/render/DatasetManager.js.map +1 -1
  52. package/dist/render/GeometryBuilder.d.ts +2 -0
  53. package/dist/render/GeometryBuilder.d.ts.map +1 -1
  54. package/dist/render/GeometryBuilder.js +185 -16
  55. package/dist/render/GeometryBuilder.js.map +1 -1
  56. package/dist/render/GroundManager.d.ts +57 -1
  57. package/dist/render/GroundManager.d.ts.map +1 -1
  58. package/dist/render/GroundManager.js +149 -21
  59. package/dist/render/GroundManager.js.map +1 -1
  60. package/dist/render/Picker.d.ts.map +1 -1
  61. package/dist/render/Picker.js +12 -4
  62. package/dist/render/Picker.js.map +1 -1
  63. package/dist/render/PointCloudMaterial.d.ts +36 -0
  64. package/dist/render/PointCloudMaterial.d.ts.map +1 -0
  65. package/dist/render/PointCloudMaterial.js +159 -0
  66. package/dist/render/PointCloudMaterial.js.map +1 -0
  67. package/dist/render/S3MViewer.d.ts +19 -0
  68. package/dist/render/S3MViewer.d.ts.map +1 -1
  69. package/dist/render/S3MViewer.js +143 -22
  70. package/dist/render/S3MViewer.js.map +1 -1
  71. package/dist/render/StyleMaterial.d.ts +6 -0
  72. package/dist/render/StyleMaterial.d.ts.map +1 -1
  73. package/dist/render/StyleMaterial.js +125 -2
  74. package/dist/render/StyleMaterial.js.map +1 -1
  75. package/dist/render/TextureFactory.d.ts.map +1 -1
  76. package/dist/render/TextureFactory.js +64 -10
  77. package/dist/render/TextureFactory.js.map +1 -1
  78. package/dist/tileset/S3MTileset.d.ts +152 -1
  79. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  80. package/dist/tileset/S3MTileset.js +669 -53
  81. package/dist/tileset/S3MTileset.js.map +1 -1
  82. package/dist/vendor/crunch_decoder.module.js +12 -18
  83. package/dist/vendor/draco_decoder.module.js +3178 -0
  84. package/docs/API.md +110 -9
  85. package/package.json +1 -1
  86. package/src/core/AtlasAlpha.ts +82 -0
  87. package/src/core/AtlasPack.ts +127 -0
  88. package/src/core/AtlasUpgrade.ts +182 -0
  89. package/src/core/DXTTextureDecode.ts +28 -6
  90. package/src/core/Draco.ts +409 -0
  91. package/src/core/MeshOpt.ts +124 -18
  92. package/src/core/S3M1Parser.ts +430 -0
  93. package/src/core/S3MBParser.ts +112 -21
  94. package/src/core/S3MBlockParser.ts +953 -0
  95. package/src/core/TextureFormat.ts +20 -2
  96. package/src/core/types.ts +63 -0
  97. package/src/index.ts +2 -1
  98. package/src/render/DatasetManager.ts +5 -5
  99. package/src/render/GeometryBuilder.ts +207 -15
  100. package/src/render/GroundManager.ts +174 -20
  101. package/src/render/Picker.ts +13 -4
  102. package/src/render/PointCloudMaterial.ts +174 -0
  103. package/src/render/S3MViewer.ts +140 -24
  104. package/src/render/StyleMaterial.ts +135 -2
  105. package/src/render/TextureFactory.ts +67 -11
  106. package/src/tileset/S3MTileset.ts +701 -58
  107. package/src/vendor/crunch_decoder.module.js +12 -18
  108. package/src/vendor/draco_decoder.module.d.ts +128 -0
  109. package/src/vendor/draco_decoder.module.js +3178 -0
@@ -21,7 +21,7 @@
21
21
  * MapLibre / Mapbox 的胶水层在 s3m-maplibre 包的 S3MLayer.ts。
22
22
  */
23
23
  import * as THREE from 'three';
24
- import { S3MTileset } from '../tileset/S3MTileset.js';
24
+ import { S3MTileset, usableGeoBounds } from '../tileset/S3MTileset.js';
25
25
  import type { BuildTileFn, TileNode } from '../tileset/S3MTileset.js';
26
26
  import { setMaxAnisotropy } from './TextureFactory.js';
27
27
  import {
@@ -33,6 +33,7 @@ import {
33
33
  createOutlineMaterial,
34
34
  } from './StyleMaterial.js';
35
35
  import type { OutlineMaterial, StyledMaterial, StyleMode, SelectMode, S3MStyle } from './StyleMaterial.js';
36
+ import { applyPointCloudStyle, isOwnedPointCloudMaterial } from './PointCloudMaterial.js';
36
37
  import { Picker } from './Picker.js';
37
38
  import {
38
39
  buildFeatureEdgeSegments,
@@ -191,6 +192,8 @@ interface MeshUserData {
191
192
  geoRef?: GeoPackageEntry;
192
193
  tileRef?: ParsedTile;
193
194
  primitiveType?: number;
195
+ /** 点云对象(THREE.Points):不吃面着色,也没有棱可描 */
196
+ isPointCloud?: boolean;
194
197
  /** CPU 端现算的棱线对象(等宽线 Mesh) */
195
198
  edges?: THREE.Object3D | null;
196
199
  /** 数据自带棱线包(此时不再用 EdgesGeometry 现算) */
@@ -211,6 +214,11 @@ function styledMaterialOf(mesh: THREE.Mesh): StyledMaterial | null {
211
214
  return styled?.userData?.isStyled ? styled : null;
212
215
  }
213
216
 
217
+ /** 点云是 THREE.Points 不是 Mesh,样式 / 拾取 / 卸载链路都靠这个判定放行 */
218
+ function isPointCloudObject(o: THREE.Object3D): o is THREE.Points {
219
+ return (o as unknown as THREE.Points).isPoints === true;
220
+ }
221
+
214
222
  /* ------------------------------------------------------------------ 图层 */
215
223
 
216
224
  export class S3MViewer {
@@ -272,7 +280,7 @@ export class S3MViewer {
272
280
  private _heightMax = -Infinity;
273
281
  private readonly _boxScratch = new THREE.Box3();
274
282
  private _selection: PickResult | null = null;
275
- private _selectedMeshes: THREE.Mesh[] = [];
283
+ private _selectedMeshes: THREE.Object3D[] = [];
276
284
  private _outlineMesh: THREE.Mesh | null = null;
277
285
  private _outlineMeshes: THREE.Mesh[] = [];
278
286
  private _selectionEdges: THREE.Mesh | null = null;
@@ -540,8 +548,11 @@ export class S3MViewer {
540
548
  /**
541
549
  * 物理地表网格的高度跟随「主数据集的应用层抬升量」,
542
550
  * 保证贴地后地表覆盖 / 地下透视的物理关系与被抬升的模型一致。
551
+ * `ground.trackHeight = false` 时不跟随:地表钉在当前高度,
552
+ * 高程调整只动模型不动切割面。
543
553
  */
544
554
  private _syncGroundHeight(): void {
555
+ if (!this.groundManager.trackHeight) return;
545
556
  const primaryId = this.datasets.keys().next().value;
546
557
  if (!primaryId) return;
547
558
  const userOffset = this._datasetManager.getDatasetHeight(primaryId);
@@ -704,6 +715,15 @@ export class S3MViewer {
704
715
  this._requestRepaint();
705
716
  }
706
717
 
718
+ /**
719
+ * 开关「物理地表作为深度遮挡源」:开启后地表以下的几何被真正切掉。
720
+ * 不依赖宿主是否开启 MapLibre terrain——深度缓冲是共用的,谁更近谁遮挡。
721
+ */
722
+ setGroundOcclude(enabled: boolean): void {
723
+ this.groundManager.setOcclude(enabled);
724
+ this._requestRepaint();
725
+ }
726
+
707
727
  /* ---------------------------------------------------------------- 状态纹理同步 */
708
728
 
709
729
  private _isFeatureHidden(smid: number | null, _batchId: number): boolean {
@@ -773,6 +793,7 @@ export class S3MViewer {
773
793
  mesh.geometry?.dispose?.();
774
794
  const styled = styledMaterialOf(mesh);
775
795
  if (styled) styled.dispose();
796
+ if (isOwnedPointCloudMaterial(mesh.material)) (mesh.material as THREE.Material).dispose();
776
797
  this._featureStyleManager.disposeMesh(mesh);
777
798
  });
778
799
  });
@@ -808,8 +829,10 @@ export class S3MViewer {
808
829
  continue;
809
830
  }
810
831
 
832
+ // 点云是 THREE.Points 不是 Mesh,但同样要进拾取表、吃不透明度、跟着着色模式走
833
+ const isPoints = isPointCloudObject(child);
811
834
  const mesh = child as THREE.Mesh;
812
- if (!mesh.isMesh) continue;
835
+ if (!mesh.isMesh && !isPoints) continue;
813
836
  const ud = mesh.userData as MeshUserData;
814
837
  ud.pickId = this._allocPickId();
815
838
  ud.datasetId = entry.id;
@@ -822,29 +845,88 @@ export class S3MViewer {
822
845
  });
823
846
  this._indexMeshSmids(mesh);
824
847
  this._addMeshHeight(mesh);
825
- if (this._edgesEnabled && !hasNativeEdges) this._ensureEdges(mesh);
848
+ // 点云没有「棱」:给一堆孤立点建 EdgesGeometry 又慢又没意义
849
+ if (this._edgesEnabled && !hasNativeEdges && !isPoints) this._ensureEdges(mesh);
826
850
 
827
851
  // 新瓦片要继承当前的不透明度与着色方式
828
- const mat = styledMaterialOf(mesh);
829
- if (mat) {
830
- const o = Math.min(1, Math.max(0.05, this.style.opacity ?? 1));
831
- mat.opacity = o;
832
- mat.transparent = o < 0.999;
833
- mat.depthWrite = o >= 0.999;
834
- mat.flatShading = this.style.shading !== 'smooth';
835
- mat.needsUpdate = true;
836
- this._featureStyleManager.applyOverridesToMesh(mesh);
852
+ if (isPoints) {
853
+ this._applyPointCloudStyle(child as THREE.Points);
854
+ } else {
855
+ const mat = styledMaterialOf(mesh);
856
+ if (mat) {
857
+ const o = Math.min(1, Math.max(0.05, this.style.opacity ?? 1));
858
+ mat.opacity = o;
859
+ mat.transparent = o < 0.999;
860
+ mat.depthWrite = o >= 0.999;
861
+ mat.flatShading = this.style.shading !== 'smooth';
862
+ mat.needsUpdate = true;
863
+ this._featureStyleManager.applyOverridesToMesh(mesh);
864
+ }
837
865
  }
866
+
867
+ // 选中状态也要跟着继承:LOD 细化 / 瓦片重载会换掉 mesh,
868
+ // 不重放就出现「点中的那栋楼一拉近就没高亮了」
869
+ this._replaySelectionOnto(mesh);
838
870
  }
839
871
  }
840
872
 
873
+ /**
874
+ * 把「当前选中」重放到一块新登记的 mesh 上。
875
+ *
876
+ * 为什么需要:选中高亮是直接写在各 mesh 材质的 uSelBatch / uSelWhole 上的,
877
+ * 而 LOD 细化、瓦片重载、淘汰回补都会换掉 mesh —— 不重放的话,用户点中的那栋楼
878
+ * 一细化就失去高亮(状态还在 _selection.smid 里,只是没人往新 mesh 上刷)。
879
+ */
880
+ private _replaySelectionOnto(mesh: THREE.Mesh): void {
881
+ const sel = this._selection;
882
+ if (!sel) return;
883
+ const mat = styledMaterialOf(mesh);
884
+ if (!mat) return;
885
+
886
+ const ud = mesh.userData as MeshUserData;
887
+ let batchId = -1;
888
+ if (sel.smid != null) {
889
+ const pickInfo = ud.geoRef?.pickInfo as Record<string | number, unknown> | undefined;
890
+ const entry = pickInfo?.[sel.smid] ?? pickInfo?.[String(sel.smid)];
891
+ batchId = entry ? pickEntryId(entry as any) : -1;
892
+ }
893
+ // 拾取命中的那块 mesh 用原始 batchId;同一 smid 散在别的几何里时按各自的区间算
894
+ if (mesh === sel.mesh) batchId = sel.batchId;
895
+ // 这块几何里根本没有这个单体 → 不动它(否则会把无关瓦片一起点亮)
896
+ if (batchId < 0 && mesh !== sel.mesh) return;
897
+
898
+ mat.userData.uSelBatch.value = batchId;
899
+ mat.userData.uSelWhole.value = batchId < 0 ? 1 : 0;
900
+ if (!this._selectedMeshes.includes(mesh)) this._selectedMeshes.push(mesh);
901
+ if (isPointCloudObject(mesh)) this._applyPointCloudStyle(mesh, true);
902
+ this.uniforms.uSelMix.value = 1;
903
+ this.uniforms.uDim.value = this.style.dimOthers ? 1 : 0;
904
+ }
905
+
841
906
  /** 瓦片被淘汰:从拾取表和场景里摘掉,并释放它自己的 GPU 资源 */
842
907
  private _unregisterTile(node: TileNode, entry: S3MDatasetEntry): void {
843
908
  for (const child of [...node.group.children]) {
844
909
  const mesh = child as THREE.Mesh;
845
910
  if (!mesh.isMesh) continue;
846
911
  const ud = mesh.userData as MeshUserData;
847
- if (this._selection?.mesh === mesh || this._selectedMeshes.includes(mesh)) this.select(null);
912
+ // 「这块 mesh 没了」≠「用户不再选中」:LOD 细化 / 淘汰回补都会换掉 mesh。
913
+ // 以前这里一律 select(null),于是点中的那栋楼一拉近,选中状态就整个被清掉。
914
+ const wasSelected = this._selectedMeshes.includes(mesh);
915
+ if (wasSelected) {
916
+ this._selectedMeshes = this._selectedMeshes.filter((m) => m !== mesh);
917
+ }
918
+ if (this._selection?.mesh === mesh) {
919
+ const smid = this._selection.smid;
920
+ const carrier =
921
+ smid != null ? [...(this._smidMeshes.get(smid) ?? [])].find((m) => m !== mesh && m.parent) : undefined;
922
+ if (carrier) {
923
+ // 选中身份交给承载同一 smid 的另一块 mesh(通常是接替显示的子瓦)
924
+ this._selection = { ...this._selection, mesh: carrier };
925
+ this._replaySelectionOnto(carrier);
926
+ } else {
927
+ this.select(null);
928
+ }
929
+ }
848
930
  if (ud.pickId != null) {
849
931
  this._pickables.delete(ud.pickId);
850
932
  this._releasePickId(ud.pickId);
@@ -855,6 +937,8 @@ export class S3MViewer {
855
937
  this._featureStyleManager.disposeMesh(mesh);
856
938
  const styled = styledMaterialOf(mesh);
857
939
  styled?.dispose?.();
940
+ // 点云材质是本模块按模式建的,不走 styled 那条链
941
+ if (isOwnedPointCloudMaterial(mesh.material)) (mesh.material as THREE.Material).dispose();
858
942
  mesh.geometry?.dispose?.();
859
943
  }
860
944
  entry.root.remove(node.group);
@@ -890,6 +974,8 @@ export class S3MViewer {
890
974
  this.style = { ...this.style, ...patch };
891
975
  this._applyStyleUniforms();
892
976
  if ('opacity' in patch || 'depthWrite' in patch) this._applyOpacity();
977
+ // 点云不走 StyleMaterial 的 uMode,模式切换要单独同步它的材质
978
+ if ('mode' in patch) this._applyPointCloudStyles();
893
979
  if ('shading' in patch) this._applyShading();
894
980
  if ('edges' in patch) this.setEdges(!!this.style.edges);
895
981
  else if ('edgeColor' in patch || 'edgeWidth' in patch || 'opacity' in patch || 'edgeOpacity' in patch) {
@@ -959,6 +1045,10 @@ export class S3MViewer {
959
1045
  // 关键:若 depthWrite 开启(默认开启),即使半透明也保持深度写入,完美遮挡背面棱线,防止背面线条透出
960
1046
  const keepDepth = this.style.depthWrite !== false;
961
1047
  for (const p of this._pickables.values()) {
1048
+ if (isPointCloudObject(p.mesh)) {
1049
+ this._applyPointCloudStyle(p.mesh);
1050
+ continue;
1051
+ }
962
1052
  const m = styledMaterialOf(p.mesh);
963
1053
  if (!m) continue;
964
1054
  m.opacity = o;
@@ -968,6 +1058,19 @@ export class S3MViewer {
968
1058
  this._requestRepaint();
969
1059
  }
970
1060
 
1061
+ /** 单个点云对象同步当前样式(着色模式 + 不透明度 + 选中高亮) */
1062
+ private _applyPointCloudStyle(points: THREE.Points, selected = this._selectedMeshes.includes(points)): void {
1063
+ applyPointCloudStyle(points, this.style, this.uniforms, selected);
1064
+ }
1065
+
1066
+ /** 全部点云刷新:着色模式切换时用(uniform 是共享引用,改色不用走这里) */
1067
+ private _applyPointCloudStyles(): void {
1068
+ for (const p of this._pickables.values()) {
1069
+ if (isPointCloudObject(p.mesh)) this._applyPointCloudStyle(p.mesh);
1070
+ }
1071
+ this._requestRepaint();
1072
+ }
1073
+
971
1074
  /** 平面/平滑着色:FLAT_SHADED 是编译期宏,改它必须重编译 */
972
1075
  private _applyShading(): void {
973
1076
  const flat = this.style.shading !== 'smooth';
@@ -1104,7 +1207,8 @@ export class S3MViewer {
1104
1207
  .map((p) => p.mesh)
1105
1208
  .filter((m) => {
1106
1209
  const ud = m.userData as MeshUserData;
1107
- return !ud.edges && !ud.nativeEdges;
1210
+ // 点云没有棱可描
1211
+ return !ud.edges && !ud.nativeEdges && !isPointCloudObject(m);
1108
1212
  });
1109
1213
  let i = 0;
1110
1214
  const step = (): void => {
@@ -1614,8 +1718,13 @@ export class S3MViewer {
1614
1718
  select(result: PickResult | null, { silent = false }: { silent?: boolean } = {}): void {
1615
1719
  // 清掉上一次
1616
1720
  if (this._selection) {
1617
- for (const mesh of this._selectedMeshes) {
1618
- const mat = styledMaterialOf(mesh);
1721
+ for (const obj of this._selectedMeshes) {
1722
+ // 点云:恢复成当前着色模式(去掉选中混色)
1723
+ if (isPointCloudObject(obj)) {
1724
+ this._applyPointCloudStyle(obj, false);
1725
+ continue;
1726
+ }
1727
+ const mat = styledMaterialOf(obj as THREE.Mesh);
1619
1728
  if (mat) {
1620
1729
  mat.userData.uSelBatch.value = -1;
1621
1730
  mat.userData.uSelWhole.value = 0;
@@ -1666,14 +1775,22 @@ export class S3MViewer {
1666
1775
  mat.userData.uSelWhole.value = batchId < 0 ? 1 : 0;
1667
1776
  this._selectedMeshes.push(mesh);
1668
1777
  }
1778
+ // 点云没有单体粒度:整块混成 selectColor,让选中至少看得见
1779
+ for (const mesh of this._selectedMeshes) {
1780
+ if (isPointCloudObject(mesh)) this._applyPointCloudStyle(mesh, true);
1781
+ }
1669
1782
  this.uniforms.uSelMix.value = 1;
1670
1783
  this.uniforms.uDim.value = this.style.dimOthers ? 1 : 0;
1671
1784
 
1672
1785
  if (this.style.outline) {
1673
- if ((this.style.outlineStyle ?? 'edges') === 'edges') {
1674
- this._addSelectionEdges(result, targetMeshes);
1675
- } else {
1676
- this._addSelectionHull(result, targetMeshes);
1786
+ // 棱线 / 外扩轮廓都是按「面」设计的,点云画不出来也不该画
1787
+ const decoMeshes = [...targetMeshes].filter((m) => !isPointCloudObject(m));
1788
+ if (decoMeshes.length) {
1789
+ if ((this.style.outlineStyle ?? 'edges') === 'edges') {
1790
+ this._addSelectionEdges(result, decoMeshes);
1791
+ } else {
1792
+ this._addSelectionHull(result, decoMeshes);
1793
+ }
1677
1794
  }
1678
1795
  }
1679
1796
  this._requestRepaint();
@@ -1973,9 +2090,8 @@ export class S3MViewer {
1973
2090
  let maxLon = -Infinity;
1974
2091
  let maxLat = -Infinity;
1975
2092
  for (const entry of this.datasets.values()) {
1976
- const b = entry.tileset?.config?.geoBounds as
1977
- | { left: number; bottom: number; right: number; top: number }
1978
- | undefined;
2093
+ // 同 DatasetManager.getBounds:退化 / 越界的 geoBounds 不参与,避免视角甩到 (0,0)
2094
+ const b = usableGeoBounds(entry.tileset?.config?.geoBounds);
1979
2095
  if (!b) continue;
1980
2096
  minLon = Math.min(minLon, b.left);
1981
2097
  maxLon = Math.max(maxLon, b.right);
@@ -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 需要额外解码器,先如实上报而不是静默出错