s3m-three 0.2.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 (114) hide show
  1. package/README.md +2 -2
  2. package/dist/browser/s3m-three.iife.ext-three.js +585 -0
  3. package/dist/browser/s3m-three.iife.js +4419 -0
  4. package/dist/browser.d.ts +3 -0
  5. package/dist/browser.d.ts.map +1 -0
  6. package/dist/browser.js +21 -0
  7. package/dist/browser.js.map +1 -0
  8. package/dist/core/AtlasAlpha.d.ts +31 -0
  9. package/dist/core/AtlasAlpha.d.ts.map +1 -0
  10. package/dist/core/AtlasAlpha.js +82 -0
  11. package/dist/core/AtlasAlpha.js.map +1 -0
  12. package/dist/core/AtlasPack.d.ts +57 -0
  13. package/dist/core/AtlasPack.d.ts.map +1 -0
  14. package/dist/core/AtlasPack.js +81 -0
  15. package/dist/core/AtlasPack.js.map +1 -0
  16. package/dist/core/AtlasUpgrade.d.ts +68 -0
  17. package/dist/core/AtlasUpgrade.d.ts.map +1 -0
  18. package/dist/core/AtlasUpgrade.js +154 -0
  19. package/dist/core/AtlasUpgrade.js.map +1 -0
  20. package/dist/core/DXTTextureDecode.d.ts.map +1 -1
  21. package/dist/core/DXTTextureDecode.js +28 -6
  22. package/dist/core/DXTTextureDecode.js.map +1 -1
  23. package/dist/core/Draco.d.ts +52 -0
  24. package/dist/core/Draco.d.ts.map +1 -0
  25. package/dist/core/Draco.js +323 -0
  26. package/dist/core/Draco.js.map +1 -0
  27. package/dist/core/MeshOpt.d.ts +14 -3
  28. package/dist/core/MeshOpt.d.ts.map +1 -1
  29. package/dist/core/MeshOpt.js +101 -18
  30. package/dist/core/MeshOpt.js.map +1 -1
  31. package/dist/core/S3M1Parser.d.ts +16 -0
  32. package/dist/core/S3M1Parser.d.ts.map +1 -0
  33. package/dist/core/S3M1Parser.js +367 -0
  34. package/dist/core/S3M1Parser.js.map +1 -0
  35. package/dist/core/S3MBParser.d.ts +43 -1
  36. package/dist/core/S3MBParser.d.ts.map +1 -1
  37. package/dist/core/S3MBParser.js +101 -23
  38. package/dist/core/S3MBParser.js.map +1 -1
  39. package/dist/core/S3MBlockParser.d.ts +209 -0
  40. package/dist/core/S3MBlockParser.d.ts.map +1 -0
  41. package/dist/core/S3MBlockParser.js +789 -0
  42. package/dist/core/S3MBlockParser.js.map +1 -0
  43. package/dist/core/TextureFormat.d.ts +0 -1
  44. package/dist/core/TextureFormat.d.ts.map +1 -1
  45. package/dist/core/TextureFormat.js +19 -2
  46. package/dist/core/TextureFormat.js.map +1 -1
  47. package/dist/core/types.d.ts +65 -0
  48. package/dist/core/types.d.ts.map +1 -1
  49. package/dist/index.d.ts +2 -1
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +2 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/render/DatasetManager.d.ts.map +1 -1
  54. package/dist/render/DatasetManager.js +5 -3
  55. package/dist/render/DatasetManager.js.map +1 -1
  56. package/dist/render/GeometryBuilder.d.ts +2 -0
  57. package/dist/render/GeometryBuilder.d.ts.map +1 -1
  58. package/dist/render/GeometryBuilder.js +185 -16
  59. package/dist/render/GeometryBuilder.js.map +1 -1
  60. package/dist/render/GroundManager.d.ts +57 -1
  61. package/dist/render/GroundManager.d.ts.map +1 -1
  62. package/dist/render/GroundManager.js +149 -21
  63. package/dist/render/GroundManager.js.map +1 -1
  64. package/dist/render/Picker.d.ts.map +1 -1
  65. package/dist/render/Picker.js +12 -4
  66. package/dist/render/Picker.js.map +1 -1
  67. package/dist/render/PointCloudMaterial.d.ts +36 -0
  68. package/dist/render/PointCloudMaterial.d.ts.map +1 -0
  69. package/dist/render/PointCloudMaterial.js +159 -0
  70. package/dist/render/PointCloudMaterial.js.map +1 -0
  71. package/dist/render/S3MViewer.d.ts +19 -0
  72. package/dist/render/S3MViewer.d.ts.map +1 -1
  73. package/dist/render/S3MViewer.js +143 -22
  74. package/dist/render/S3MViewer.js.map +1 -1
  75. package/dist/render/StyleMaterial.d.ts +6 -0
  76. package/dist/render/StyleMaterial.d.ts.map +1 -1
  77. package/dist/render/StyleMaterial.js +125 -2
  78. package/dist/render/StyleMaterial.js.map +1 -1
  79. package/dist/render/TextureFactory.d.ts.map +1 -1
  80. package/dist/render/TextureFactory.js +64 -10
  81. package/dist/render/TextureFactory.js.map +1 -1
  82. package/dist/tileset/S3MTileset.d.ts +152 -1
  83. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  84. package/dist/tileset/S3MTileset.js +672 -54
  85. package/dist/tileset/S3MTileset.js.map +1 -1
  86. package/dist/vendor/crunch_decoder.module.js +12 -18
  87. package/dist/vendor/draco_decoder.module.js +3178 -0
  88. package/docs/API.md +112 -11
  89. package/package.json +5 -3
  90. package/src/browser.ts +21 -0
  91. package/src/core/AtlasAlpha.ts +82 -0
  92. package/src/core/AtlasPack.ts +127 -0
  93. package/src/core/AtlasUpgrade.ts +182 -0
  94. package/src/core/DXTTextureDecode.ts +28 -6
  95. package/src/core/Draco.ts +409 -0
  96. package/src/core/MeshOpt.ts +124 -18
  97. package/src/core/S3M1Parser.ts +430 -0
  98. package/src/core/S3MBParser.ts +112 -21
  99. package/src/core/S3MBlockParser.ts +953 -0
  100. package/src/core/TextureFormat.ts +20 -2
  101. package/src/core/types.ts +63 -0
  102. package/src/index.ts +2 -1
  103. package/src/render/DatasetManager.ts +5 -5
  104. package/src/render/GeometryBuilder.ts +207 -15
  105. package/src/render/GroundManager.ts +174 -20
  106. package/src/render/Picker.ts +13 -4
  107. package/src/render/PointCloudMaterial.ts +174 -0
  108. package/src/render/S3MViewer.ts +140 -24
  109. package/src/render/StyleMaterial.ts +135 -2
  110. package/src/render/TextureFactory.ts +67 -11
  111. package/src/tileset/S3MTileset.ts +704 -59
  112. package/src/vendor/crunch_decoder.module.js +12 -18
  113. package/src/vendor/draco_decoder.module.d.ts +128 -0
  114. 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';
@@ -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
 
@@ -291,10 +291,10 @@ export class DatasetManager {
291
291
 
292
292
  for (const entry of this.datasets.values()) {
293
293
  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)) {
294
+ // 只认「确实是一张经纬度范围」的 geoBounds:OSGB/SuperMapCache 那类配置只给米制
295
+ // 局部包围盒,全 0 的退化框拿去 fitBounds 会把视角甩到几内亚湾
296
+ const b = usableGeoBounds(entry.tileset.config.geoBounds);
297
+ if (b) {
298
298
  minLon = Math.min(minLon, b.left);
299
299
  minLat = Math.min(minLat, b.bottom);
300
300
  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[];
@@ -280,11 +328,30 @@ export async function buildTileObjects(
280
328
  group.userData.hasNativeEdges = false;
281
329
 
282
330
  // 1) 纹理
331
+ //
332
+ // 缓存 key 不能只用 atlas id:拼图(AtlasPack)是**逐块**做的,同一个 id 在 LOD 兄弟块里
333
+ // 参与的拼图集合可能不同、落位也不同。只按 id 复用会把别块的排布拿过来,
334
+ // 而 aAtlasRect 是本块的 —— 整块贴错图。带 packKey 的按「id#集合签名」隔离。
335
+ const texKeyOf = (code: string): string => {
336
+ const t = (tile.textures as Record<string, { packKey?: string }> | undefined)?.[code];
337
+ return t?.packKey ? `${code}#${t.packKey}` : code;
338
+ };
339
+
283
340
  for (const [code, tex] of Object.entries(tile.textures ?? {})) {
284
- if (textureByCode.has(code)) continue;
341
+ // 同一个图集 id 会被很多瓦片共用:先到的那块可能只拿到内嵌缩略图(120x184),
342
+ // 后面的块换成了高清版(240x304)。只按 id 判「缓存已有」会让高清永远进不来,
343
+ // 满屏都是马赛克。所以尺寸变大时必须重解码并覆盖。
344
+ const key = texKeyOf(code);
345
+ const prev = textureByCode.get(key);
346
+ if (prev) {
347
+ const img = prev.image as { width?: number; height?: number } | undefined;
348
+ const prevPixels = (img?.width ?? 0) * (img?.height ?? 0);
349
+ if (prevPixels >= (tex.width ?? 0) * (tex.height ?? 0)) continue;
350
+ textureByCode.delete(key);
351
+ }
285
352
  const { texture, decoded } = await createTexture(tex);
286
353
  if (texture) {
287
- textureByCode.set(code, texture);
354
+ textureByCode.set(key, texture);
288
355
  stats.textures++;
289
356
  } else {
290
357
  stats.skipped.push(`texture ${code} → ${decoded}`);
@@ -299,12 +366,19 @@ export async function buildTileObjects(
299
366
  // 材质对象本身很轻(program 由 customProgramCacheKey 复用),一栋一个不心疼。
300
367
  const materialList = resolveMaterialList(tile.materials);
301
368
 
302
- const makeMaterial = (code?: string): StyledMaterial => {
369
+ const makeMaterial = (code?: string, hasAtlasRect = false): StyledMaterial => {
303
370
  const entry = materialList.find((e) => materialName(unwrapMaterial(e)) === code);
304
371
  const m = unwrapMaterial(entry);
305
372
  const units = materialUnits(m);
306
373
  const unit0 = units[0];
307
- let map = unit0 ? (textureByCode.get(materialName(unit0) ?? unit0.id ?? '') ?? null) : null;
374
+ let map = unit0 ? (textureByCode.get(texKeyOf(materialName(unit0) ?? unit0.id ?? '')) ?? null) : null;
375
+ // S3MBLOCK 的烘焙图(unit1 / BakeTex)是光照烘焙,官方与 base 相乘;缺它建筑就是一片平白
376
+ const unit1 = units[1];
377
+ const noBake = (globalThis as { __S3M_NO_BAKE?: boolean }).__S3M_NO_BAKE === true;
378
+ const secMap =
379
+ hasAtlasRect && unit1 && !noBake
380
+ ? (textureByCode.get(texKeyOf(materialName(unit1) ?? unit1.id ?? '')) ?? null)
381
+ : null;
308
382
 
309
383
  // 检查并应用 texmodmatrix 纹理变换矩阵
310
384
  const texMod = unit0?.texmodmatrix ?? unit0?.matrix;
@@ -337,20 +411,36 @@ export async function buildTileObjects(
337
411
 
338
412
  const diffuse = m?.diffuse ?? { r: 1, g: 1, b: 1, a: 1 };
339
413
  const alphaMode = m?.alphaMode;
340
- const transparent = alphaMode === 'AM_BLEND' || (diffuse.a ?? 1) < 0.999 || m?.transparentsorting === true;
414
+ // SuperMap 的内置水面:纹理单元 id 是 `103.water`,块里**根本没有像素**
415
+ // (实测 Waters 的 s3mb 只有 1081 字节,textures = 0,材质 diffuse 还是纯白)。
416
+ // 以前当白色漫反射渲染,整片湖就是一块白板。这里按内置水面材质上色。
417
+ const isWater = [unit0, unit1].some((u) => /\.water$/i.test(String(materialName(u) ?? u?.id ?? '')));
418
+ const transparent =
419
+ alphaMode === 'AM_BLEND' || (diffuse.a ?? 1) < 0.999 || m?.transparentsorting === true || (isWater && !map);
341
420
 
342
421
  // S3M 的 shininess 是旧式 0~128 高光指数,粗略映射到粗糙度
343
422
  const roughness =
344
423
  typeof m?.shininess === 'number' && m.shininess > 0 ? Math.max(0.15, 1 - m.shininess / 128) : 0.95;
345
424
 
425
+ // 镂空图集(S3MBLOCK):建筑照片是异形 alpha 抠图,轮廓顶点会采到 alpha=0 的像素,
426
+ // 不丢弃的话就是成片的黑面。cutoff 取材质自带的 alphaCutoff(实测 0.01)。
427
+ // rect 是图集里的原始子区域(含右 1/3 的内联 mip 列),不做任何裁切,
428
+ // 这里保留 alphaTest 只为挡掉轮廓外的零星像素,不会再出现整面墙被抠空。
429
+ const alphaTest = (map as THREE.Texture | null)?.userData?.s3mHasAlpha
430
+ ? Math.max((m?.alphaCutoff as number) ?? 0, 0.01)
431
+ : 0;
432
+
346
433
  const mat = createStyledMaterial({
347
434
  uniforms,
348
435
  map,
349
- color: new THREE.Color(diffuse.r ?? 1, diffuse.g ?? 1, diffuse.b ?? 1),
436
+ color: isWater && !map ? WATER_COLOR.clone() : new THREE.Color(diffuse.r ?? 1, diffuse.g ?? 1, diffuse.b ?? 1),
350
437
  side: resolveSide(m),
351
438
  transparent,
352
- opacity: diffuse.a ?? 1,
353
- roughness,
439
+ opacity: isWater && !map ? 0.86 : (diffuse.a ?? 1),
440
+ roughness: isWater && !map ? 0.12 : roughness,
441
+ alphaTest,
442
+ atlas: hasAtlasRect && !!map,
443
+ secMap,
354
444
  });
355
445
  return mat;
356
446
  };
@@ -388,21 +478,61 @@ export async function buildTileObjects(
388
478
  ? toBufferAttribute(batchAttr)
389
479
  : new THREE.BufferAttribute(new Float32Array(vertexCount).fill(-1), 1);
390
480
 
481
+ // S3MBLOCK 图集:逐顶点带子区域矩形,平铺的 fract 交给片元做(顶点上做会画反)。
482
+ // 烘焙图(材质 unit1)用第二套原始 UV,这里另开一个 aSecUv 槽位,不跟 three 的 uv1 语义纠缠。
483
+ const rects = tile.atlasRects?.[name];
484
+ if (rects) {
485
+ attributes.aAtlasRect = new THREE.BufferAttribute(rects.uv ?? new Float32Array(vertexCount * 4), 4);
486
+ // 没有第二套 UV 的骨架不做烘焙:给一个退化矩形,片元里据此跳过
487
+ const uv1 = attributes.uv1 as THREE.BufferAttribute | undefined;
488
+ if (rects.uv2 && uv1) {
489
+ attributes.aAtlasRect1 = new THREE.BufferAttribute(rects.uv2, 4);
490
+ attributes.aSecUv = uv1;
491
+ } else {
492
+ attributes.aAtlasRect1 = new THREE.BufferAttribute(new Float32Array(vertexCount * 4), 4);
493
+ attributes.aSecUv = (attributes.uv as THREE.BufferAttribute) ?? new THREE.BufferAttribute(new Float32Array(vertexCount * 2), 2);
494
+ }
495
+ }
496
+
391
497
  const instanced = vp.instanceIndex !== -1 && (vp.instanceMode === 16 || vp.instanceMode === 17);
392
498
  // 注意:indexPackages 属于 Skeleton(geoPackage[name]),不在 vertexPackage 里
393
- skeletonInfo.set(name, { indexPackages: geo.indexPackages ?? [], instanced });
499
+ //
500
+ // 一个索引包都没有的骨架 = 纯点集:高斯泼溅(3DGaussianSplatting)和无索引点云
501
+ // 都是这种——顶点就是点,没有任何三角形可画。不补一个「顶点即点」的伪包,
502
+ // 下面就不会产出任何对象,整块瓦片最后落到「tile 无可用几何」被丢掉。
503
+ let packs = geo.indexPackages ?? [];
504
+ if (packs.length === 0 && vertexCount > 0) {
505
+ packs = [
506
+ {
507
+ indexCount: 0,
508
+ indexType: 0,
509
+ useIndex: 0,
510
+ primitiveType: POINT_PRIMITIVE,
511
+ indices: null,
512
+ passNames: [],
513
+ },
514
+ ];
515
+ }
516
+ skeletonInfo.set(name, { indexPackages: packs, instanced });
394
517
  stats.vertices += vertexCount;
395
518
 
396
- (geo.indexPackages ?? []).forEach((pack, pi) => {
397
- const geometry = new THREE.BufferGeometry();
519
+ packs.forEach((pack, pi) => {
520
+ let geometry = new THREE.BufferGeometry();
398
521
  for (const [k, v] of Object.entries(attributes)) geometry.setAttribute(k, v);
399
522
  const idx = toIndex(pack);
400
523
  if (idx) geometry.setIndex(idx);
401
524
  else geometry.setDrawRange(0, vertexCount);
402
525
 
526
+
527
+
403
528
  // 如果数据本身没有法线且是三角网格,必须在绑定了 index 之后计算法线
404
529
  // 绝不能在未索引的 attributes.position 上算,否则非连续顶点会导致法线朝向错乱甚至越界 NaN 导致死黑面
405
- if (!isLinePrimitive(pack.primitiveType) && !geometry.getAttribute('normal')) {
530
+ // 线 / 点包不需要法线
531
+ if (
532
+ !isLinePrimitive(pack.primitiveType) &&
533
+ !isPointPrimitive(pack.primitiveType) &&
534
+ !geometry.getAttribute('normal')
535
+ ) {
406
536
  geometry.computeVertexNormals();
407
537
  }
408
538
 
@@ -435,6 +565,7 @@ export async function buildTileObjects(
435
565
  let hasUv1 = false;
436
566
  let hasAColor = false;
437
567
  let hasABatchId = false;
568
+ let hasAtlas = false;
438
569
 
439
570
  for (const c of candidates) {
440
571
  const g = c.cached.geometry;
@@ -446,6 +577,7 @@ export async function buildTileObjects(
446
577
  if (g.attributes.uv1) hasUv1 = true;
447
578
  if (g.attributes.aColor) hasAColor = true;
448
579
  if (g.attributes.aBatchId) hasABatchId = true;
580
+ if (g.attributes.aAtlasRect) hasAtlas = true;
449
581
  }
450
582
 
451
583
  const mergedPos = new Float32Array(totalVertices * 3);
@@ -454,6 +586,31 @@ export async function buildTileObjects(
454
586
  const mergedUv1 = hasUv1 ? new Float32Array(totalVertices * 2) : null;
455
587
  const mergedColor = hasAColor ? new Float32Array(totalVertices * 3) : null;
456
588
  const mergedBatchId = hasABatchId ? new Float32Array(totalVertices) : null;
589
+ const mergedRect0 = hasAtlas ? new Float32Array(totalVertices * 4) : null;
590
+ const mergedRect1 = hasAtlas ? new Float32Array(totalVertices * 4) : null;
591
+ const mergedSecUv = hasAtlas ? new Float32Array(totalVertices * 2) : null;
592
+
593
+ /** 逐顶点拷一个 n 分量的浮点属性;缺失时用 fallbackRect / 0 兜底 */
594
+ const copyAttr = (
595
+ dst: Float32Array,
596
+ attr: THREE.BufferAttribute | THREE.InterleavedBufferAttribute | undefined,
597
+ comp: number,
598
+ vCount: number,
599
+ vertexOffset: number,
600
+ fallback?: (i: number, out: number[]) => void
601
+ ) => {
602
+ if (attr) {
603
+ const src = attr.array as ArrayLike<number>;
604
+ for (let i = 0; i < vCount; i++)
605
+ for (let k = 0; k < comp; k++) dst[(vertexOffset + i) * comp + k] = src[i * comp + k] ?? 0;
606
+ return;
607
+ }
608
+ for (let i = 0; i < vCount; i++) {
609
+ const out: number[] = [];
610
+ fallback ? fallback(i, out) : out.push(...new Array(comp).fill(0));
611
+ for (let k = 0; k < comp; k++) dst[(vertexOffset + i) * comp + k] = out[k] ?? 0;
612
+ }
613
+ };
457
614
 
458
615
  const IndexArrayType = totalVertices > 65535 ? Uint32Array : Uint16Array;
459
616
  const mergedIndices = new IndexArrayType(totalIndices);
@@ -582,7 +739,14 @@ export async function buildTileObjects(
582
739
  }
583
740
  }
584
741
 
585
- // 7. Indices
742
+ // 7. 图集矩形(S3MBLOCK)
743
+ if (mergedRect0) {
744
+ copyAttr(mergedRect0, g.attributes.aAtlasRect, 4, vCount, vertexOffset, (_i, out) => out.push(0, 0, 0, 0));
745
+ copyAttr(mergedRect1!, g.attributes.aAtlasRect1, 4, vCount, vertexOffset, (_i, out) => out.push(0, 0, 0, 0));
746
+ copyAttr(mergedSecUv!, g.attributes.aSecUv, 2, vCount, vertexOffset);
747
+ }
748
+
749
+ // 8. Indices
586
750
  if (g.index) {
587
751
  const idxArr = g.index.array as ArrayLike<number>;
588
752
  const iCount = g.index.count;
@@ -607,6 +771,11 @@ export async function buildTileObjects(
607
771
  if (mergedUv1) mergedGeo.setAttribute('uv1', new THREE.BufferAttribute(mergedUv1, 2));
608
772
  if (mergedColor) mergedGeo.setAttribute('aColor', new THREE.BufferAttribute(mergedColor, 3));
609
773
  if (mergedBatchId) mergedGeo.setAttribute('aBatchId', new THREE.BufferAttribute(mergedBatchId, 1));
774
+ if (mergedRect0) {
775
+ mergedGeo.setAttribute('aAtlasRect', new THREE.BufferAttribute(mergedRect0, 4));
776
+ mergedGeo.setAttribute('aAtlasRect1', new THREE.BufferAttribute(mergedRect1!, 4));
777
+ mergedGeo.setAttribute('aSecUv', new THREE.BufferAttribute(mergedSecUv!, 2));
778
+ }
610
779
  mergedGeo.setIndex(new THREE.BufferAttribute(mergedIndices, 1));
611
780
  mergedGeo.computeBoundingBox();
612
781
  mergedGeo.computeBoundingSphere();
@@ -632,7 +801,7 @@ export async function buildTileObjects(
632
801
  matrix: THREE.Matrix4 | null
633
802
  ): THREE.Object3D => {
634
803
  const { geometry, pack } = cached;
635
- const material = makeMaterial(pack.passNames?.[0]);
804
+ const material = makeMaterial(pack.passNames?.[0], !!geometry.getAttribute('aAtlasRect'));
636
805
  let object: THREE.Mesh | THREE.InstancedMesh;
637
806
 
638
807
  const info = skeletonInfo.get(name);
@@ -656,6 +825,28 @@ export async function buildTileObjects(
656
825
  return lines;
657
826
  }
658
827
 
828
+ // 点云包:顶点即点,按 THREE.Points 渲染。索引只是点序号,直接走 drawRange。
829
+ if (isPointPrimitive(pack.primitiveType)) {
830
+ // PointsMaterial 的顶点色读内置 'color' 槽位,与 aColor 共享同一份 buffer
831
+ if (!geometry.getAttribute('color')) {
832
+ geometry.setAttribute('color', geometry.getAttribute('aColor')!);
833
+ }
834
+ const points = new THREE.Points(geometry, getPointCloudMaterial());
835
+ // 点云不是 Mesh:viewer 的样式 / 拾取 / 卸载链路都靠这个标记放行
836
+ points.userData.isPointCloud = true;
837
+ points.userData.skeletonName = name;
838
+ points.userData.geoRef = geo;
839
+ points.userData.tileRef = tile;
840
+ points.userData.primitiveType = pack.primitiveType;
841
+ if (matrix && !matrix.equals(IDENTITY)) {
842
+ matrix.decompose(points.position, points.quaternion, points.scale);
843
+ }
844
+ group.add(points);
845
+ objects.push(points);
846
+ stats.meshes++;
847
+ return points;
848
+ }
849
+
659
850
  if (info?.instanced && vp.instanceBuffer && vp.instanceCount > 0) {
660
851
  // 逐实例的两条属性:
661
852
  // aInstanceBatchId —— 单体控制位,着色器靠它查状态纹理(改色 / 隐藏 / 抬高)
@@ -822,7 +1013,8 @@ export async function buildTileObjects(
822
1013
  if (list.length === 1) {
823
1014
  placeObject(list[0].cached, list[0].name, list[0].geo, list[0].matrix);
824
1015
  } else {
825
- const material = makeMaterial(matKey === '__default__' ? undefined : matKey);
1016
+ const hasAtlasRect = !!list[0].cached.geometry.getAttribute('aAtlasRect');
1017
+ const material = makeMaterial(matKey === '__default__' ? undefined : matKey, hasAtlasRect);
826
1018
  const batchedMesh = mergeBatchGeometries(list, material, tile);
827
1019
  group.add(batchedMesh);
828
1020
  objects.push(batchedMesh);