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
@@ -65,6 +65,12 @@ function kindOfPixelFormat(pf: number): TextureKind | null {
65
65
 
66
66
  function kindOfCompress(ct: number): TextureKind | null {
67
67
  switch (ct) {
68
+ // 实测 iClient3D 数据里 compressType = 14 恒为 DXT5:
69
+ // · 块内嵌图集 ct=14 pf=21(DXT5),512×512 恰好 0.5 B/px 对应 DXT1、1 B/px 对应 DXT5
70
+ // · 独立 .texblock 里的图集 ct=14 pf=13,192×128 = 24576 B,正是 1 B/px 的 DXT5
71
+ // 后者 pf=13 会被 kindOfPixelFormat 判成 raw(ABGR 4 B/px)→ 按 1/4 长度读,整图碎掉。
72
+ case 14:
73
+ return 'dxt5';
68
74
  case TextureCompress.DXT1_RGB:
69
75
  case TextureCompress.DXT1_RGBA:
70
76
  return 'dxt1';
@@ -97,7 +103,19 @@ export function resolveTextureKind(compressType: number, pixelFormat: number): T
97
103
  return kindOfPixelFormat(pixelFormat) ?? 'unknown';
98
104
  }
99
105
 
100
- /** DXT 族对应的 pixelFormat(喂给 DXTTextureDecode,>11 即按 16 字节块解) */
106
+ /**
107
+ * DXT 族对应的 pixelFormat,喂给 DXTTextureDecode 选块大小 / alpha 编码。
108
+ *
109
+ * 必须逐格式给回真实值:以前除 dxt1 之外一律给 21,DXT3 就被按 DXT5 的插值 alpha 解了
110
+ * (块大小同为 16 B,所以不会越界,但 alpha 是错的)。
111
+ */
112
+ const DXT_PIXEL_FORMAT: Partial<Record<TextureKind, number>> = {
113
+ dxt1: 17,
114
+ dxt3: 19,
115
+ dxt4: 20,
116
+ dxt5: 21,
117
+ };
118
+
101
119
  export function dxtPixelFormatOf(kind: TextureKind): number {
102
- return kind === 'dxt1' ? 17 : 21;
120
+ return DXT_PIXEL_FORMAT[kind] ?? 21;
103
121
  }
package/src/core/types.ts CHANGED
@@ -47,11 +47,27 @@ export interface IndexPackage {
47
47
  passNames: string[];
48
48
  }
49
49
 
50
+ /** MeshOpt 块解码后的原始流清单(语义名 + 每顶点字节数),来自 parseMeshOptGeometry */
51
+ export interface MeshOptInfo {
52
+ vertexCount: number;
53
+ compressOptions: number;
54
+ minVertices: [number, number, number];
55
+ vertScale: number;
56
+ texCoordScale: number[][];
57
+ texCoordOffset: number[][];
58
+ streams: Array<{ semantic: string; bytes: number; stride: number }>;
59
+ }
60
+
50
61
  export interface VertexPackage {
51
62
  verticesCount: number;
52
63
  vertexAttributes: VertexAttribute[];
53
64
  /** semantic → 在 vertexAttributes 中的下标 */
54
65
  attrLocation: Record<string, number>;
66
+ /**
67
+ * MeshOpt 骨架的原始流清单(诊断用,也是接自定义属性 / 高斯泼溅属性的入口):
68
+ * 每条流解码后每个顶点占 stride 字节,压缩前的字节数在 bytes 里。
69
+ */
70
+ meshOptInfo?: MeshOptInfo;
55
71
  instanceCount: number;
56
72
  /** 16 = 只有矩阵,17/29 = 矩阵 + 颜色 */
57
73
  instanceMode: number;
@@ -89,6 +105,12 @@ export interface GeoPackageEntry {
89
105
  indexPackages: IndexPackage[];
90
106
  /** SMID → batchId + 顶点区间(单体化信息) */
91
107
  pickInfo?: Record<number, PickEntry>;
108
+ /**
109
+ * v3 骨架条目末尾自带的 OBB(局部 ENU 系,中心/三个半轴都是 float64)。
110
+ * 多骨架瓦片里每块 geode 的范围各不相同,pageLOD 的包围球只取自其中一块,
111
+ * 要做按块的剔除/贴地就得用它。v2 没有这个字段。
112
+ */
113
+ obb?: BoundingBox;
92
114
  }
93
115
 
94
116
  /* ------------------------------------------------------------------ LOD 树 */
@@ -123,10 +145,49 @@ export interface S3MTexture {
123
145
  compressType: number;
124
146
  pixelFormat: number;
125
147
  data: Uint8Array;
148
+ /**
149
+ * S3MBLOCK 图集为真:数据按 GL 纹理方向(v=0 在最后一行)存储,
150
+ * 解码成 RGBA 后要翻一次行再上传,rect 坐标(offsetY 从 v=0 起)才对得上。
151
+ */
152
+ flipY?: boolean;
153
+ /**
154
+ * 由 AtlasPack 拼合出来的 RGBA 大图(不是文件里的原始 DXT 块)。
155
+ * 这类纹理:① 已是自上而下,flipY 必须为 false;② 不能开 GPU mipmap,
156
+ * 层级由 StyleMaterial 的内联金字塔自己算;③ rect 是拼图坐标系。
157
+ */
158
+ packed?: boolean;
159
+ /**
160
+ * leaf 模式按 `subTexInfos` 把多张精灵贴出来的自上而下 RGBA 画布(可能没进拼图,
161
+ * 只有一张图集时 packAtlases 返回 null)。与 packed 同等对待:flipY=false、
162
+ * 不开 GPU mipmap,层级由 StyleMaterial 的内联金字塔自己算。
163
+ */
164
+ composed?: boolean;
165
+ /**
166
+ * 拼图集合签名(`408x192#id1,id2,...`)。拼图是逐块做的,同一个 atlas id 在不同块里
167
+ * 可能落在不同位置,所以跨瓦片共享纹理时 key 必须带上它,否则会复用别的块的排布。
168
+ */
169
+ packKey?: string;
126
170
  }
127
171
 
128
172
  /* ------------------------------------------------------------------ 瓦片 */
129
173
 
174
+ /**
175
+ * S3MBLOCK 图集:每个顶点所属子区域的归一化矩形 (x, y, w, h)。
176
+ * 未被绑定覆盖的顶点是 (0, 0, 1, 1),即整张图集。
177
+ * uv 对应材质 textureunit 0,uv2 对应 unit 1(官方叫 BaseTex / BakeTex)。
178
+ */
179
+ export interface S3MAtlasRects {
180
+ uv?: Float32Array;
181
+ uv2?: Float32Array;
182
+ /**
183
+ * 每个顶点所属的绑定组 id(= 块文件里「哪段顶点用哪张子纹理」)。
184
+ * 生成 UV 时按它分组,一组一面墙 → 一张精灵,跟官方 batch 表的语义一致。
185
+ */
186
+ group?: Float32Array;
187
+ /** 每个顶点所属子区域的像素尺寸 (w, h):决定这段 uv 该怎么平铺 */
188
+ groupDim?: Float32Array;
189
+ }
190
+
130
191
  export interface ParsedTile {
131
192
  version: number;
132
193
  compressedType: number;
@@ -134,6 +195,8 @@ export interface ParsedTile {
134
195
  groupNode: GroupNode;
135
196
  geoPackage: Record<string, GeoPackageEntry>;
136
197
  textures: Record<string, S3MTexture>;
198
+ /** 仅 S3MBLOCK:图集子区域矩形(按骨架名) */
199
+ atlasRects?: Record<string, S3MAtlasRects>;
137
200
  materials: unknown;
138
201
  pickInfo: unknown;
139
202
  extensions?: unknown;
package/src/index.ts CHANGED
@@ -10,7 +10,8 @@
10
10
  */
11
11
 
12
12
  // —— 解析层(Node 可用,不依赖 three / WebGL)
13
- export { parseS3MB, S3M_VERSION, VertexTag, VertexTagV3 } from './core/S3MBParser.js';
13
+ export { parseS3MB, S3MFormatError, S3M_VERSION, VertexTag, VertexTagV3 } from './core/S3MBParser.js';
14
+ export { parseS3M1, isS3M1Buffer } from './core/S3M1Parser.js';
14
15
  export type { ParseOptions, InflateFn } from './core/S3MBParser.js';
15
16
  export { BinaryReader } from './core/BinaryReader.js';
16
17
  export { resolveFeature, measureSegments, buildAttributes, DEFAULT_FLOOR_HEIGHT } from './core/FeatureInfo.js';
@@ -94,6 +95,7 @@ export type {
94
95
  S3MFilter,
95
96
  GroundMode,
96
97
  GroundOptions,
98
+ LightOptions,
97
99
  } from './render/S3MViewer.js';
98
100
 
99
101
  export { buildTileObjects } from './render/GeometryBuilder.js';
@@ -8,7 +8,7 @@
8
8
  * 4. 多数据集全局经纬度包围盒计算(getBounds)
9
9
  */
10
10
  import * as THREE from 'three';
11
- import { S3MTileset } from '../tileset/S3MTileset.js';
11
+ import { S3MTileset, usableGeoBounds } from '../tileset/S3MTileset.js';
12
12
  import type { Anchor } from '../core/types.js';
13
13
  import { worldFromLngLat, type GeoAdapter } from './GeoAdapter.js';
14
14
 
@@ -64,6 +64,68 @@ export interface DatasetManagerCallbacks {
64
64
  export class DatasetManager {
65
65
  readonly datasets = new Map<string, S3MDatasetEntry>();
66
66
  sceneAnchor: { x: number; y: number; z: number; meterInMercator: number; altitude?: number } | null = null;
67
+ totalMaxTiles: number = 0;
68
+
69
+ get(id: string): S3MDatasetEntry | undefined {
70
+ return this.datasets.get(id);
71
+ }
72
+
73
+ values(): IterableIterator<S3MDatasetEntry> {
74
+ return this.datasets.values();
75
+ }
76
+
77
+ /**
78
+ * 同步预注册数据集(用于提前建立条目与预算平衡)
79
+ */
80
+ registerDataset(options: S3MDatasetOptions): S3MDatasetEntry {
81
+ const existing = this.datasets.get(options.id);
82
+ if (existing) return existing;
83
+ const root = new THREE.Group();
84
+ root.name = `s3m-dataset:${options.id}`;
85
+ root.visible = options.visible !== false;
86
+ const entry: S3MDatasetEntry = {
87
+ id: options.id,
88
+ name: options.name ?? options.id,
89
+ options,
90
+ root,
91
+ tileset: null,
92
+ anchor: null,
93
+ baseAlignOffset: 0,
94
+ userOffset: options.heightOffset ?? 0,
95
+ heightOffset: options.heightOffset ?? 0,
96
+ offset: [0, 0, 0],
97
+ visible: options.visible !== false,
98
+ error: null,
99
+ };
100
+ this.datasets.set(options.id, entry);
101
+ this.rebalanceBudgets();
102
+ return entry;
103
+ }
104
+
105
+ /**
106
+ * 全局多数据集预算再平衡:在所有可见数据集之间均衡分配 maxTiles
107
+ */
108
+ rebalanceBudgets(totalMaxTiles?: number): void {
109
+ const budget = totalMaxTiles ?? this.totalMaxTiles;
110
+ if (!budget || budget <= 0) return;
111
+ this.totalMaxTiles = budget;
112
+
113
+ const visibleEntries = Array.from(this.datasets.values()).filter((d) => d.visible && d.tileset);
114
+ const count = Math.max(1, visibleEntries.length);
115
+ const perDataset = Math.max(8, Math.floor(budget / count));
116
+
117
+ for (const entry of this.datasets.values()) {
118
+ if (!entry.tileset) continue;
119
+ if (entry.visible) {
120
+ entry.tileset.maxTiles = perDataset;
121
+ entry.tileset.hardCap = perDataset + Math.max(4, Math.ceil(perDataset * 0.5));
122
+ } else {
123
+ // 隐藏数据集仅保留极低配额(防止占着显存)
124
+ entry.tileset.maxTiles = Math.min(4, entry.tileset.maxTiles);
125
+ entry.tileset.hardCap = Math.min(6, entry.tileset.hardCap);
126
+ }
127
+ }
128
+ }
67
129
 
68
130
  /** 根据语义对齐方式('top' | 'bottom' | 'center')和数据源高度范围计算基准高度偏移 */
69
131
  computeAlignOffset(tileset: S3MTileset | null, align?: DatasetAlignment): number {
@@ -144,6 +206,7 @@ export class DatasetManager {
144
206
  },
145
207
  });
146
208
  entry.tileset = tileset;
209
+ this.rebalanceBudgets();
147
210
 
148
211
  try {
149
212
  await tileset.init();
@@ -179,6 +242,7 @@ export class DatasetManager {
179
242
  entry.tileset?.dispose();
180
243
  scene.remove(entry.root);
181
244
  this.datasets.delete(id);
245
+ this.rebalanceBudgets();
182
246
 
183
247
  if (this.datasets.size > 0) {
184
248
  this.recomputeAnchors(geo);
@@ -277,6 +341,7 @@ export class DatasetManager {
277
341
  if (!entry) return;
278
342
  entry.visible = visible;
279
343
  entry.root.visible = visible;
344
+ this.rebalanceBudgets();
280
345
  }
281
346
 
282
347
  /**
@@ -291,10 +356,10 @@ export class DatasetManager {
291
356
 
292
357
  for (const entry of this.datasets.values()) {
293
358
  if (!entry.visible || !entry.tileset?.config) continue;
294
- const b = entry.tileset.config.geoBounds as
295
- | { left: number; bottom: number; right: number; top: number }
296
- | undefined;
297
- if (b && Number.isFinite(b.left)) {
359
+ // 只认「确实是一张经纬度范围」的 geoBounds:OSGB/SuperMapCache 那类配置只给米制
360
+ // 局部包围盒,全 0 的退化框拿去 fitBounds 会把视角甩到几内亚湾
361
+ const b = usableGeoBounds(entry.tileset.config.geoBounds);
362
+ if (b) {
298
363
  minLon = Math.min(minLon, b.left);
299
364
  minLat = Math.min(minLat, b.bottom);
300
365
  maxLon = Math.max(maxLon, b.right);
@@ -35,11 +35,55 @@ import type {
35
35
  */
36
36
  export const LINE_PRIMITIVE = 13;
37
37
 
38
+ /** primitiveType = 1:点云包。索引是点序号,画成三角面就是满屏彩色尖刺。 */
39
+ export const POINT_PRIMITIVE = 1;
40
+
38
41
  function isLinePrimitive(primitiveType: number): boolean {
39
42
  // 1 = 点,2 = 线段,3 = 折线,13 = 棱线包
40
43
  return primitiveType === LINE_PRIMITIVE || primitiveType === 2 || primitiveType === 3;
41
44
  }
42
45
 
46
+ function isPointPrimitive(primitiveType: number): boolean {
47
+ return primitiveType === POINT_PRIMITIVE;
48
+ }
49
+
50
+ /* ------------------------------------------------------------------ 图集 UV */
51
+
52
+ /**
53
+ * S3MBLOCK 图集的 UV 一律用几何自带的 uv0 原值,这里不再做任何重算。
54
+ *
55
+ * 走过三轮弯路才定下来(每一条都不要再试):
56
+ * 1. 「按世界尺度 fract」—— uv0 是建模工具留下的逐岛坐标,不是世界投影,
57
+ * 全局跨度 279 只是所有岛拼起来的包围盒,拿它除以瓦片尺寸算出的
58
+ * 「1 个 uv 周期 = 1.65 m」是错的;
59
+ * 2. 「按 binding 分组、逐面 0..1」—— 等于把作者排好的映射丢掉,
60
+ * 一面墙强行铺一整张精灵,长墙被横向拉糊;
61
+ * 3. 「按 uv 跨度 ÷ 精灵像素取整重复」—— 同上,而且依赖精灵像素尺寸这个
62
+ * 与几何无关的量。
63
+ *
64
+ * 官方引擎的做法(iClient3D 12.1.1,VS 里 getUVMatrixForBatch / FS 里
65
+ * czm_getTexColorForS3M)就是:fract(uv0) 直接乘子区域尺寸再平移到子区域,
66
+ * 一个 uv 周期 = 一张精灵,作者在 uv0 里已经把重复次数写死了。
67
+ * 唯一需要额外处理的是子区域右 1/3 那条内联 mip 链,见 StyleMaterial.s3mAtlasSample。
68
+ */
69
+
70
+
71
+ /**
72
+ * 点云共享材质:顶点色直接上屏,不受光照影响。
73
+ * 固定像素尺寸(sizeAttenuation: false),远近点大小一致,高空浏览时不会缩成一团。
74
+ */
75
+ let pointCloudMaterial: THREE.PointsMaterial | null = null;
76
+ function getPointCloudMaterial(): THREE.PointsMaterial {
77
+ if (!pointCloudMaterial) {
78
+ pointCloudMaterial = new THREE.PointsMaterial({
79
+ size: 2,
80
+ sizeAttenuation: false,
81
+ vertexColors: true,
82
+ });
83
+ }
84
+ return pointCloudMaterial;
85
+ }
86
+
43
87
  /* ------------------------------------------------------------------ 顶点流 */
44
88
 
45
89
  function bytesPerElement(dtype: AttrDType): number {
@@ -140,6 +184,9 @@ function toColorAttribute(attr: VertexAttribute | null, vertexCount: number): TH
140
184
  return new THREE.BufferAttribute(out, 3);
141
185
  }
142
186
 
187
+ /** SuperMap 内置水面(`103.water`)的基色:偏深的青蓝,配合低粗糙度出一点天光反射 */
188
+ const WATER_COLOR = new THREE.Color(0x1d4e63);
189
+
143
190
  /* ------------------------------------------------------------------ 材质描述 */
144
191
 
145
192
  interface RawMaterialUnit {
@@ -157,6 +204,7 @@ interface RawMaterialDef {
157
204
  diffuse?: { r?: number; g?: number; b?: number; a?: number };
158
205
  shininess?: number;
159
206
  alphaMode?: string;
207
+ alphaCutoff?: number;
160
208
  cullMode?: string;
161
209
  transparentsorting?: boolean;
162
210
  textureStates?: RawMaterialUnit[];
@@ -240,6 +288,7 @@ export interface BuildTileResult {
240
288
  group: THREE.Group;
241
289
  stats: TileBuildStats;
242
290
  objects: THREE.Object3D[];
291
+ textureKeys?: string[];
243
292
  }
244
293
 
245
294
  interface CachedGeometry {
@@ -260,6 +309,7 @@ export async function buildTileObjects(
260
309
  const textureByCode = options.textureCache ?? new Map<string, THREE.Texture>();
261
310
  const group = new THREE.Group();
262
311
  const objects: THREE.Object3D[] = [];
312
+ const usedTextureKeys: string[] = [];
263
313
  const stats: TileBuildStats = {
264
314
  meshes: 0,
265
315
  vertices: 0,
@@ -280,11 +330,31 @@ export async function buildTileObjects(
280
330
  group.userData.hasNativeEdges = false;
281
331
 
282
332
  // 1) 纹理
333
+ //
334
+ // 缓存 key 不能只用 atlas id:拼图(AtlasPack)是**逐块**做的,同一个 id 在 LOD 兄弟块里
335
+ // 参与的拼图集合可能不同、落位也不同。只按 id 复用会把别块的排布拿过来,
336
+ // 而 aAtlasRect 是本块的 —— 整块贴错图。带 packKey 的按「id#集合签名」隔离。
337
+ const texKeyOf = (code: string): string => {
338
+ const t = (tile.textures as Record<string, { packKey?: string }> | undefined)?.[code];
339
+ return t?.packKey ? `${code}#${t.packKey}` : code;
340
+ };
341
+
283
342
  for (const [code, tex] of Object.entries(tile.textures ?? {})) {
284
- if (textureByCode.has(code)) continue;
343
+ // 同一个图集 id 会被很多瓦片共用:先到的那块可能只拿到内嵌缩略图(120x184),
344
+ // 后面的块换成了高清版(240x304)。只按 id 判「缓存已有」会让高清永远进不来,
345
+ // 满屏都是马赛克。所以尺寸变大时必须重解码并覆盖。
346
+ const key = texKeyOf(code);
347
+ const prev = textureByCode.get(key);
348
+ if (prev) {
349
+ const img = prev.image as { width?: number; height?: number } | undefined;
350
+ const prevPixels = (img?.width ?? 0) * (img?.height ?? 0);
351
+ if (prevPixels >= (tex.width ?? 0) * (tex.height ?? 0)) continue;
352
+ textureByCode.delete(key);
353
+ }
285
354
  const { texture, decoded } = await createTexture(tex);
286
355
  if (texture) {
287
- textureByCode.set(code, texture);
356
+ textureByCode.set(key, texture);
357
+ usedTextureKeys.push(key);
288
358
  stats.textures++;
289
359
  } else {
290
360
  stats.skipped.push(`texture ${code} → ${decoded}`);
@@ -299,12 +369,19 @@ export async function buildTileObjects(
299
369
  // 材质对象本身很轻(program 由 customProgramCacheKey 复用),一栋一个不心疼。
300
370
  const materialList = resolveMaterialList(tile.materials);
301
371
 
302
- const makeMaterial = (code?: string): StyledMaterial => {
372
+ const makeMaterial = (code?: string, hasAtlasRect = false): StyledMaterial => {
303
373
  const entry = materialList.find((e) => materialName(unwrapMaterial(e)) === code);
304
374
  const m = unwrapMaterial(entry);
305
375
  const units = materialUnits(m);
306
376
  const unit0 = units[0];
307
- let map = unit0 ? (textureByCode.get(materialName(unit0) ?? unit0.id ?? '') ?? null) : null;
377
+ let map = unit0 ? (textureByCode.get(texKeyOf(materialName(unit0) ?? unit0.id ?? '')) ?? null) : null;
378
+ // S3MBLOCK 的烘焙图(unit1 / BakeTex)是光照烘焙,官方与 base 相乘;缺它建筑就是一片平白
379
+ const unit1 = units[1];
380
+ const noBake = (globalThis as { __S3M_NO_BAKE?: boolean }).__S3M_NO_BAKE === true;
381
+ const secMap =
382
+ hasAtlasRect && unit1 && !noBake
383
+ ? (textureByCode.get(texKeyOf(materialName(unit1) ?? unit1.id ?? '')) ?? null)
384
+ : null;
308
385
 
309
386
  // 检查并应用 texmodmatrix 纹理变换矩阵
310
387
  const texMod = unit0?.texmodmatrix ?? unit0?.matrix;
@@ -337,20 +414,36 @@ export async function buildTileObjects(
337
414
 
338
415
  const diffuse = m?.diffuse ?? { r: 1, g: 1, b: 1, a: 1 };
339
416
  const alphaMode = m?.alphaMode;
340
- const transparent = alphaMode === 'AM_BLEND' || (diffuse.a ?? 1) < 0.999 || m?.transparentsorting === true;
417
+ // SuperMap 的内置水面:纹理单元 id 是 `103.water`,块里**根本没有像素**
418
+ // (实测 Waters 的 s3mb 只有 1081 字节,textures = 0,材质 diffuse 还是纯白)。
419
+ // 以前当白色漫反射渲染,整片湖就是一块白板。这里按内置水面材质上色。
420
+ const isWater = [unit0, unit1].some((u) => /\.water$/i.test(String(materialName(u) ?? u?.id ?? '')));
421
+ const transparent =
422
+ alphaMode === 'AM_BLEND' || (diffuse.a ?? 1) < 0.999 || m?.transparentsorting === true || (isWater && !map);
341
423
 
342
424
  // S3M 的 shininess 是旧式 0~128 高光指数,粗略映射到粗糙度
343
425
  const roughness =
344
426
  typeof m?.shininess === 'number' && m.shininess > 0 ? Math.max(0.15, 1 - m.shininess / 128) : 0.95;
345
427
 
428
+ // 镂空图集(S3MBLOCK):建筑照片是异形 alpha 抠图,轮廓顶点会采到 alpha=0 的像素,
429
+ // 不丢弃的话就是成片的黑面。cutoff 取材质自带的 alphaCutoff(实测 0.01)。
430
+ // rect 是图集里的原始子区域(含右 1/3 的内联 mip 列),不做任何裁切,
431
+ // 这里保留 alphaTest 只为挡掉轮廓外的零星像素,不会再出现整面墙被抠空。
432
+ const alphaTest = (map as THREE.Texture | null)?.userData?.s3mHasAlpha
433
+ ? Math.max((m?.alphaCutoff as number) ?? 0, 0.01)
434
+ : 0;
435
+
346
436
  const mat = createStyledMaterial({
347
437
  uniforms,
348
438
  map,
349
- color: new THREE.Color(diffuse.r ?? 1, diffuse.g ?? 1, diffuse.b ?? 1),
439
+ color: isWater && !map ? WATER_COLOR.clone() : new THREE.Color(diffuse.r ?? 1, diffuse.g ?? 1, diffuse.b ?? 1),
350
440
  side: resolveSide(m),
351
441
  transparent,
352
- opacity: diffuse.a ?? 1,
353
- roughness,
442
+ opacity: isWater && !map ? 0.86 : (diffuse.a ?? 1),
443
+ roughness: isWater && !map ? 0.12 : roughness,
444
+ alphaTest,
445
+ atlas: hasAtlasRect && !!map,
446
+ secMap,
354
447
  });
355
448
  return mat;
356
449
  };
@@ -388,21 +481,61 @@ export async function buildTileObjects(
388
481
  ? toBufferAttribute(batchAttr)
389
482
  : new THREE.BufferAttribute(new Float32Array(vertexCount).fill(-1), 1);
390
483
 
484
+ // S3MBLOCK 图集:逐顶点带子区域矩形,平铺的 fract 交给片元做(顶点上做会画反)。
485
+ // 烘焙图(材质 unit1)用第二套原始 UV,这里另开一个 aSecUv 槽位,不跟 three 的 uv1 语义纠缠。
486
+ const rects = tile.atlasRects?.[name];
487
+ if (rects) {
488
+ attributes.aAtlasRect = new THREE.BufferAttribute(rects.uv ?? new Float32Array(vertexCount * 4), 4);
489
+ // 没有第二套 UV 的骨架不做烘焙:给一个退化矩形,片元里据此跳过
490
+ const uv1 = attributes.uv1 as THREE.BufferAttribute | undefined;
491
+ if (rects.uv2 && uv1) {
492
+ attributes.aAtlasRect1 = new THREE.BufferAttribute(rects.uv2, 4);
493
+ attributes.aSecUv = uv1;
494
+ } else {
495
+ attributes.aAtlasRect1 = new THREE.BufferAttribute(new Float32Array(vertexCount * 4), 4);
496
+ attributes.aSecUv = (attributes.uv as THREE.BufferAttribute) ?? new THREE.BufferAttribute(new Float32Array(vertexCount * 2), 2);
497
+ }
498
+ }
499
+
391
500
  const instanced = vp.instanceIndex !== -1 && (vp.instanceMode === 16 || vp.instanceMode === 17);
392
501
  // 注意:indexPackages 属于 Skeleton(geoPackage[name]),不在 vertexPackage 里
393
- skeletonInfo.set(name, { indexPackages: geo.indexPackages ?? [], instanced });
502
+ //
503
+ // 一个索引包都没有的骨架 = 纯点集:高斯泼溅(3DGaussianSplatting)和无索引点云
504
+ // 都是这种——顶点就是点,没有任何三角形可画。不补一个「顶点即点」的伪包,
505
+ // 下面就不会产出任何对象,整块瓦片最后落到「tile 无可用几何」被丢掉。
506
+ let packs = geo.indexPackages ?? [];
507
+ if (packs.length === 0 && vertexCount > 0) {
508
+ packs = [
509
+ {
510
+ indexCount: 0,
511
+ indexType: 0,
512
+ useIndex: 0,
513
+ primitiveType: POINT_PRIMITIVE,
514
+ indices: null,
515
+ passNames: [],
516
+ },
517
+ ];
518
+ }
519
+ skeletonInfo.set(name, { indexPackages: packs, instanced });
394
520
  stats.vertices += vertexCount;
395
521
 
396
- (geo.indexPackages ?? []).forEach((pack, pi) => {
397
- const geometry = new THREE.BufferGeometry();
522
+ packs.forEach((pack, pi) => {
523
+ let geometry = new THREE.BufferGeometry();
398
524
  for (const [k, v] of Object.entries(attributes)) geometry.setAttribute(k, v);
399
525
  const idx = toIndex(pack);
400
526
  if (idx) geometry.setIndex(idx);
401
527
  else geometry.setDrawRange(0, vertexCount);
402
528
 
529
+
530
+
403
531
  // 如果数据本身没有法线且是三角网格,必须在绑定了 index 之后计算法线
404
532
  // 绝不能在未索引的 attributes.position 上算,否则非连续顶点会导致法线朝向错乱甚至越界 NaN 导致死黑面
405
- if (!isLinePrimitive(pack.primitiveType) && !geometry.getAttribute('normal')) {
533
+ // 线 / 点包不需要法线
534
+ if (
535
+ !isLinePrimitive(pack.primitiveType) &&
536
+ !isPointPrimitive(pack.primitiveType) &&
537
+ !geometry.getAttribute('normal')
538
+ ) {
406
539
  geometry.computeVertexNormals();
407
540
  }
408
541
 
@@ -435,6 +568,7 @@ export async function buildTileObjects(
435
568
  let hasUv1 = false;
436
569
  let hasAColor = false;
437
570
  let hasABatchId = false;
571
+ let hasAtlas = false;
438
572
 
439
573
  for (const c of candidates) {
440
574
  const g = c.cached.geometry;
@@ -446,6 +580,7 @@ export async function buildTileObjects(
446
580
  if (g.attributes.uv1) hasUv1 = true;
447
581
  if (g.attributes.aColor) hasAColor = true;
448
582
  if (g.attributes.aBatchId) hasABatchId = true;
583
+ if (g.attributes.aAtlasRect) hasAtlas = true;
449
584
  }
450
585
 
451
586
  const mergedPos = new Float32Array(totalVertices * 3);
@@ -454,6 +589,31 @@ export async function buildTileObjects(
454
589
  const mergedUv1 = hasUv1 ? new Float32Array(totalVertices * 2) : null;
455
590
  const mergedColor = hasAColor ? new Float32Array(totalVertices * 3) : null;
456
591
  const mergedBatchId = hasABatchId ? new Float32Array(totalVertices) : null;
592
+ const mergedRect0 = hasAtlas ? new Float32Array(totalVertices * 4) : null;
593
+ const mergedRect1 = hasAtlas ? new Float32Array(totalVertices * 4) : null;
594
+ const mergedSecUv = hasAtlas ? new Float32Array(totalVertices * 2) : null;
595
+
596
+ /** 逐顶点拷一个 n 分量的浮点属性;缺失时用 fallbackRect / 0 兜底 */
597
+ const copyAttr = (
598
+ dst: Float32Array,
599
+ attr: THREE.BufferAttribute | THREE.InterleavedBufferAttribute | undefined,
600
+ comp: number,
601
+ vCount: number,
602
+ vertexOffset: number,
603
+ fallback?: (i: number, out: number[]) => void
604
+ ) => {
605
+ if (attr) {
606
+ const src = attr.array as ArrayLike<number>;
607
+ for (let i = 0; i < vCount; i++)
608
+ for (let k = 0; k < comp; k++) dst[(vertexOffset + i) * comp + k] = src[i * comp + k] ?? 0;
609
+ return;
610
+ }
611
+ for (let i = 0; i < vCount; i++) {
612
+ const out: number[] = [];
613
+ fallback ? fallback(i, out) : out.push(...new Array(comp).fill(0));
614
+ for (let k = 0; k < comp; k++) dst[(vertexOffset + i) * comp + k] = out[k] ?? 0;
615
+ }
616
+ };
457
617
 
458
618
  const IndexArrayType = totalVertices > 65535 ? Uint32Array : Uint16Array;
459
619
  const mergedIndices = new IndexArrayType(totalIndices);
@@ -582,7 +742,14 @@ export async function buildTileObjects(
582
742
  }
583
743
  }
584
744
 
585
- // 7. Indices
745
+ // 7. 图集矩形(S3MBLOCK)
746
+ if (mergedRect0) {
747
+ copyAttr(mergedRect0, g.attributes.aAtlasRect, 4, vCount, vertexOffset, (_i, out) => out.push(0, 0, 0, 0));
748
+ copyAttr(mergedRect1!, g.attributes.aAtlasRect1, 4, vCount, vertexOffset, (_i, out) => out.push(0, 0, 0, 0));
749
+ copyAttr(mergedSecUv!, g.attributes.aSecUv, 2, vCount, vertexOffset);
750
+ }
751
+
752
+ // 8. Indices
586
753
  if (g.index) {
587
754
  const idxArr = g.index.array as ArrayLike<number>;
588
755
  const iCount = g.index.count;
@@ -607,6 +774,11 @@ export async function buildTileObjects(
607
774
  if (mergedUv1) mergedGeo.setAttribute('uv1', new THREE.BufferAttribute(mergedUv1, 2));
608
775
  if (mergedColor) mergedGeo.setAttribute('aColor', new THREE.BufferAttribute(mergedColor, 3));
609
776
  if (mergedBatchId) mergedGeo.setAttribute('aBatchId', new THREE.BufferAttribute(mergedBatchId, 1));
777
+ if (mergedRect0) {
778
+ mergedGeo.setAttribute('aAtlasRect', new THREE.BufferAttribute(mergedRect0, 4));
779
+ mergedGeo.setAttribute('aAtlasRect1', new THREE.BufferAttribute(mergedRect1!, 4));
780
+ mergedGeo.setAttribute('aSecUv', new THREE.BufferAttribute(mergedSecUv!, 2));
781
+ }
610
782
  mergedGeo.setIndex(new THREE.BufferAttribute(mergedIndices, 1));
611
783
  mergedGeo.computeBoundingBox();
612
784
  mergedGeo.computeBoundingSphere();
@@ -632,7 +804,7 @@ export async function buildTileObjects(
632
804
  matrix: THREE.Matrix4 | null
633
805
  ): THREE.Object3D => {
634
806
  const { geometry, pack } = cached;
635
- const material = makeMaterial(pack.passNames?.[0]);
807
+ const material = makeMaterial(pack.passNames?.[0], !!geometry.getAttribute('aAtlasRect'));
636
808
  let object: THREE.Mesh | THREE.InstancedMesh;
637
809
 
638
810
  const info = skeletonInfo.get(name);
@@ -656,6 +828,28 @@ export async function buildTileObjects(
656
828
  return lines;
657
829
  }
658
830
 
831
+ // 点云包:顶点即点,按 THREE.Points 渲染。索引只是点序号,直接走 drawRange。
832
+ if (isPointPrimitive(pack.primitiveType)) {
833
+ // PointsMaterial 的顶点色读内置 'color' 槽位,与 aColor 共享同一份 buffer
834
+ if (!geometry.getAttribute('color')) {
835
+ geometry.setAttribute('color', geometry.getAttribute('aColor')!);
836
+ }
837
+ const points = new THREE.Points(geometry, getPointCloudMaterial());
838
+ // 点云不是 Mesh:viewer 的样式 / 拾取 / 卸载链路都靠这个标记放行
839
+ points.userData.isPointCloud = true;
840
+ points.userData.skeletonName = name;
841
+ points.userData.geoRef = geo;
842
+ points.userData.tileRef = tile;
843
+ points.userData.primitiveType = pack.primitiveType;
844
+ if (matrix && !matrix.equals(IDENTITY)) {
845
+ matrix.decompose(points.position, points.quaternion, points.scale);
846
+ }
847
+ group.add(points);
848
+ objects.push(points);
849
+ stats.meshes++;
850
+ return points;
851
+ }
852
+
659
853
  if (info?.instanced && vp.instanceBuffer && vp.instanceCount > 0) {
660
854
  // 逐实例的两条属性:
661
855
  // aInstanceBatchId —— 单体控制位,着色器靠它查状态纹理(改色 / 隐藏 / 抬高)
@@ -822,7 +1016,8 @@ export async function buildTileObjects(
822
1016
  if (list.length === 1) {
823
1017
  placeObject(list[0].cached, list[0].name, list[0].geo, list[0].matrix);
824
1018
  } else {
825
- const material = makeMaterial(matKey === '__default__' ? undefined : matKey);
1019
+ const hasAtlasRect = !!list[0].cached.geometry.getAttribute('aAtlasRect');
1020
+ const material = makeMaterial(matKey === '__default__' ? undefined : matKey, hasAtlasRect);
826
1021
  const batchedMesh = mergeBatchGeometries(list, material, tile);
827
1022
  group.add(batchedMesh);
828
1023
  objects.push(batchedMesh);
@@ -836,5 +1031,6 @@ export async function buildTileObjects(
836
1031
 
837
1032
  if (!group.children.length) stats.skipped.push('tile 无可用几何');
838
1033
 
839
- return { group, stats, objects };
1034
+ group.userData.textureKeys = usedTextureKeys;
1035
+ return { group, stats, objects, textureKeys: usedTextureKeys };
840
1036
  }