s3m-three 0.4.19 → 0.4.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/browser/assets/{s3mWorker-4iTAmWIu.js → s3mWorker-DvAUA8NJ.js} +1 -1
  2. package/dist/browser/s3m-three.iife.ext-three.js +243 -243
  3. package/dist/browser/s3m-three.iife.js +227 -227
  4. package/dist/core/Draco.d.ts.map +1 -1
  5. package/dist/core/Draco.js +0 -1
  6. package/dist/core/Draco.js.map +1 -1
  7. package/dist/core/S3MBlockParser.d.ts.map +1 -1
  8. package/dist/core/S3MBlockParser.js +27 -14
  9. package/dist/core/S3MBlockParser.js.map +1 -1
  10. package/dist/core/SplatSort.d.ts +14 -1
  11. package/dist/core/SplatSort.d.ts.map +1 -1
  12. package/dist/core/SplatSort.js +51 -4
  13. package/dist/core/SplatSort.js.map +1 -1
  14. package/dist/core/TileGeometry.d.ts +10 -0
  15. package/dist/core/TileGeometry.d.ts.map +1 -1
  16. package/dist/core/TileGeometry.js +17 -14
  17. package/dist/core/TileGeometry.js.map +1 -1
  18. package/dist/render/FeatureStyleManager.d.ts.map +1 -1
  19. package/dist/render/GeometryBuilder.d.ts +7 -0
  20. package/dist/render/GeometryBuilder.d.ts.map +1 -1
  21. package/dist/render/GeometryBuilder.js +39 -7
  22. package/dist/render/GeometryBuilder.js.map +1 -1
  23. package/dist/render/S3MViewer.d.ts +98 -440
  24. package/dist/render/S3MViewer.d.ts.map +1 -1
  25. package/dist/render/S3MViewer.js +272 -1652
  26. package/dist/render/S3MViewer.js.map +1 -1
  27. package/dist/render/SplatSorter.d.ts.map +1 -1
  28. package/dist/render/SplatSorter.js +8 -1
  29. package/dist/render/SplatSorter.js.map +1 -1
  30. package/dist/render/viewer/EdgeDecorator.d.ts +206 -0
  31. package/dist/render/viewer/EdgeDecorator.d.ts.map +1 -0
  32. package/dist/render/viewer/EdgeDecorator.js +628 -0
  33. package/dist/render/viewer/EdgeDecorator.js.map +1 -0
  34. package/dist/render/viewer/HeightIndex.d.ts +36 -0
  35. package/dist/render/viewer/HeightIndex.d.ts.map +1 -0
  36. package/dist/render/viewer/HeightIndex.js +98 -0
  37. package/dist/render/viewer/HeightIndex.js.map +1 -0
  38. package/dist/render/viewer/LightRig.d.ts +88 -0
  39. package/dist/render/viewer/LightRig.d.ts.map +1 -0
  40. package/dist/render/viewer/LightRig.js +229 -0
  41. package/dist/render/viewer/LightRig.js.map +1 -0
  42. package/dist/render/viewer/PickService.d.ts +109 -0
  43. package/dist/render/viewer/PickService.d.ts.map +1 -0
  44. package/dist/render/viewer/PickService.js +591 -0
  45. package/dist/render/viewer/PickService.js.map +1 -0
  46. package/dist/render/viewer/SelectionDecorator.d.ts +84 -0
  47. package/dist/render/viewer/SelectionDecorator.d.ts.map +1 -0
  48. package/dist/render/viewer/SelectionDecorator.js +339 -0
  49. package/dist/render/viewer/SelectionDecorator.js.map +1 -0
  50. package/dist/render/viewer/SplatLayer.d.ts +26 -0
  51. package/dist/render/viewer/SplatLayer.d.ts.map +1 -0
  52. package/dist/render/viewer/SplatLayer.js +44 -0
  53. package/dist/render/viewer/SplatLayer.js.map +1 -0
  54. package/dist/render/viewer/types.d.ts +250 -0
  55. package/dist/render/viewer/types.d.ts.map +1 -0
  56. package/dist/render/viewer/types.js +3 -0
  57. package/dist/render/viewer/types.js.map +1 -0
  58. package/dist/tileset/S3MTileset.d.ts +0 -1
  59. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  60. package/dist/tileset/S3MTileset.js +0 -1
  61. package/dist/tileset/S3MTileset.js.map +1 -1
  62. package/dist/worker/s3mWorker.js +1 -1
  63. package/dist/worker/s3mWorker.js.map +1 -1
  64. package/docs/FORMAT-NOTES.md +38 -14
  65. package/package.json +1 -1
  66. package/src/core/Draco.ts +1 -2
  67. package/src/core/S3MBlockParser.ts +27 -16
  68. package/src/core/SplatSort.ts +67 -5
  69. package/src/core/TileGeometry.ts +24 -11
  70. package/src/render/FeatureStyleManager.ts +1 -1
  71. package/src/render/GeometryBuilder.ts +38 -7
  72. package/src/render/S3MViewer.ts +302 -1915
  73. package/src/render/SplatSorter.ts +8 -1
  74. package/src/render/viewer/EdgeDecorator.ts +722 -0
  75. package/src/render/viewer/HeightIndex.ts +94 -0
  76. package/src/render/viewer/LightRig.ts +267 -0
  77. package/src/render/viewer/PickService.ts +651 -0
  78. package/src/render/viewer/SelectionDecorator.ts +377 -0
  79. package/src/render/viewer/SplatLayer.ts +44 -0
  80. package/src/render/viewer/types.ts +258 -0
  81. package/src/tileset/S3MTileset.ts +0 -1
@@ -96,25 +96,49 @@ byte[4] 's3m\0' + zlib
96
96
 
97
97
  ## 五、S3MBLOCK / texblock(纹理分包)
98
98
 
99
- 从 `3D-CBD/Xiaopin@CBD` 逐字节标定,并与官方 `@supermapgis/s3m-content-loader` 的 `S3MBlockBufferLoader` 交叉验证。 —— **B 级**
99
+ 从 `3D-CBD/Xiaopin@CBD` 与 `3D-OlympicGreen-2`(Road / Building)逐字节标定,并与官方 `@supermapgis/s3m-content-loader` 的 `S3MBlockBufferLoader` 交叉验证。 —— **B 级**
100
100
 
101
- 容器:
101
+ ### 5.1 容器结构
102
+
103
+ 一个 `.s3mblock` 文件不是“单块瓦片”,而是**一整棵 LOD 子树的打包**(例如 `1_00.s3mblock` 依次包含 `1_00`、`1_0000`、`1_000000` 三层 LOD 组)。
102
104
 
103
105
  ```
104
- float32 blockVersion // 2.1
105
- uint32 groupCount // RO 组数,实测 1
106
- 每组重复:
107
- String name // "0.s3mblock"
108
- if blockVersion <= 3.0: uint32 pageLodCount,每组 {float32 rangeValue; uint16 rangeMode; f64 sphere}
109
- ...(组内是 zlib 压缩的 v2 瓦片体 + 内嵌纹理图集)
106
+ float32 blockVersion // 2.1 或 3.0
107
+ uint32 groupCount // RO 组数(LOD 档位数,实测 1 ~ 5 组)
108
+ 每组重复(按 LOD 从粗到细排列):
109
+ String name // 组名,如 "1_00"、"1_0000"、"1_000000"
110
+ if blockVersion <= 3.0:
111
+ uint32 pageLodCount // 外层 pageLOD 条数(注意:叶子瓦片恒为 0,见下文)
112
+ for 0..pageLodCount:
113
+ float32 rangeValue // 切换阈值(RangeMode=1 时为屏幕像素直径)
114
+ uint16 rangeMode // 1 = Pixel_Size_OnScreen, 0 = Distance_From_EyePoint
115
+ float64 sphere.center.xyz // 包围球中心(ENU 米)
116
+ float64 sphere.radius // 包围球半径(米)
117
+ String childTile // 下一级瓦片名,如 "1_0000.s3mblock"
118
+ float32 version // 内层瓦片数据版本(如 2.1 或 3.0)
119
+ if version >= 3:
120
+ uint32 compressedType // 压缩方式(0 = 默认 zlib)
121
+ uint32 unzipSize // 解压后字节数
122
+ uint32 zipSize // 压缩块字节数
123
+ byte zipData[zipSize] // zlib 压缩的瓦片体(含段目录:GroupNode / 骨架 / 图集 / 材质 / 单体化)
110
124
  ```
111
125
 
112
- 关键语义:
113
-
114
- - 纹理**不跟瓦片走**,作为「图集」内嵌在块里,材质通过图集 id(如 `-667946976.texblock`)引用,几何顶点的 UV 绑定到图集的子区域 → 解析时要把 UV 重映射进图集空间,多图集走货架式装箱(`AtlasPack`)。
115
- - **根块(`6.s3mblock`)的图集是内嵌的**(`isSaveData=1`);但**子瓦(`6_00` / `6_01`)的图集段只有元信息**:`data = null` + `requestNames`,像素在同目录的 `.texblock` 里。不补这一步的后果不是「糊一点」,而是**子瓦整块无贴图**——`tile.textures` 里根本没有这条纹理,建筑变成纯白几何。
116
- - **只认 `requestNames`。`childTexNames` 是下一级 LOD 的图集指针**,它有自己的 `subTexInfos`:同一个精灵在父级叫 `1460_3`(96×64)、子级叫 `1460_2`(192×128),排布也不一样。拿父级的 rect 去套子级像素必然错位 —— 上一版就是这么把画面搞坏的。官方从不拿它换像素,那个 LOD 的瓦片自己会加载。
117
- - leaf 多精灵的图集没有整图像素,只有 `subTexInfos` 里每块一个 rect,按 `subName` 把精灵贴回各 rect 组合成整张自上而下 RGBA 画布(`composeLeafAtlas`)。精灵自带「左 2/3 base + 右 1/3 内联 mip 金字塔」,与 `StyleMaterial` 的 `s3mAtlasLod` 2/3 采样公式配套,rect 一个像素都不用改。
126
+ ### 5.2 关键语义与常见坑
127
+
128
+ - **叶子瓦片(Leaf)外层 `pageLodCount` 恒为 0,绝对不能冲掉内层 `groupNode`**:
129
+ - S3MBLOCK 的外层 pageLOD 仅记录具有下级子代的瓦片调度门限与下级文件名。到了子树末端的叶子瓦片(没有后续子代),外层记录的 `pageLodCount` 恒为 `0`;
130
+ - 真正包含几何摆放的权威数据在**解压后的内层 GroupNode 段**里:内层始终带有一组完整的 `PageLOD`,其中挂着该瓦片的核心 `geodes`(每个 geode 携带 4×4 的 `matrix` 空间局部变换矩阵,以及包围球与 OBB);
131
+ - **历史血泪 Bug**:若用外层 `pageLods.map(...)` 覆盖内层,当叶子瓦片外层为 0 条时,`mergedLods` 会直接变成空数组 `[]`,彻底丢弃内层的 `geodes`。渲染器取不到 geode 矩阵回退为单位矩阵,在存在场景大偏移的数据集(如 `Road@OlympicGreen` 矩阵平移为 `[-130, -1354, -0.15]` 米)中,表现为**瓦片细化瞬间闪烁并瞬移数公里**;且由于网格坐标偏离几公里,被 Three.js 视锥裁剪直接剔除,表现为**道路加载着加载着整片消失**;
132
+ - **正确处理方式**:当外层 `pageLods.length === 0` 时,直接回退并完整保留内层 `groupNode.pageLods`;若内层层数多于外层,亦将超出的内层条目追加保留;
133
+ - **官方 loader 对账**:官方 `@supermapgis/s3m-content-loader` 在解压完直接使用内层的 `groupNode = ShellEntitiesLoader.load(...)`,外层 pageLOD 仅用来跳过字节流,官方从不把内层清空。
134
+ - **纹理不跟瓦片走(图集机制)**:
135
+ - 材质通过图集 id(如 `-667946976.texblock`)引用,几何顶点的 UV 绑定到图集的子区域 → 解析时要把 UV 重映射进图集空间,多图集走货架式装箱(`AtlasPack`)。
136
+ - **根块与子瓦的图集分离**:
137
+ - 根块(`6.s3mblock`)的图集是内嵌的(`isSaveData=1`);但子瓦(`6_00` / `6_01`)的图集段只有元信息:`data = null` + `requestNames`,像素在同目录的 `.texblock` 里。必须补齐外部图集,否则子瓦整块无贴图变成纯白。
138
+ - **只认 `requestNames`,禁止用 `childTexNames` 替换像素**:
139
+ - `childTexNames` 是下一级 LOD 的图集指针,它有自己的 `subTexInfos`。同一个精灵在父级与子级的命名(如 `1460_3` vs `1460_2`)和排布不同,拿父级的 rect 去套子级像素必然错位。官方从不拿它换像素。
140
+ - **Leaf 多精灵组合画布**:
141
+ - leaf 多精灵的图集没有整图像素,只有 `subTexInfos` 里每块一个 rect,按 `subName` 把精灵贴回各 rect 组合成整张自上而下 RGBA 画布(`composeLeafAtlas`)。精灵自带「左 2/3 base + 右 1/3 内联 mip 金字塔」,与 `StyleMaterial` 的 `s3mAtlasLod` 2/3 采样公式配套,rect 一个像素都不用改。
118
142
 
119
143
  ---
120
144
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "s3m-three",
3
- "version": "0.4.19",
3
+ "version": "0.4.20",
4
4
  "description": "SuperMap S3M 三维模型格式的 three.js 渲染内核:浏览器内直接解析 .s3mb 瓦片,无需转 3D Tiles,支持 LOD 调度、白膜/按高度/按单体/动态扫描着色、单体级 GPU 拾取。宿主无关,可单独用于任意 three 场景。",
5
5
  "type": "module",
6
6
  "repository": {
package/src/core/Draco.ts CHANGED
@@ -32,7 +32,7 @@
32
32
  * `version === 3` 的判定对 3.01 瓦片不成立),实际解析不了这个服务,所以以实测为准。
33
33
  */
34
34
  import type { BinaryReader } from './BinaryReader.js';
35
- import type { AttrDType, IndexPackage, VertexAttribute, VertexPackage } from './types.js';
35
+ import type { AttrDType, IndexPackage, VertexPackage } from './types.js';
36
36
  import type { DracoModule, DracoPointAttribute, DracoPointCloud } from '../vendor/draco_decoder.module.js';
37
37
 
38
38
  /* ------------------------------------------------------------ draco 枚举(本地副本) */
@@ -41,7 +41,6 @@ import type { DracoModule, DracoPointAttribute, DracoPointCloud } from '../vendo
41
41
  const ATTR_POSITION = 0;
42
42
  const ATTR_NORMAL = 1;
43
43
  const ATTR_COLOR = 2;
44
- const ATTR_GENERIC = 4;
45
44
 
46
45
  /** draco::DataType(DT_INVALID = 0 占位) */
47
46
  const DT_UINT8 = 2;
@@ -67,13 +67,12 @@
67
67
  * 等价,但不需要官方的采样数组。顶点上取 fract 会让跨平铺边界的三角形反向扫纹理,别那样做。
68
68
  */
69
69
  import { BinaryReader } from './BinaryReader.js';
70
- import type { InflateFn, ParseOptions } from './S3MBParser.js';
70
+ import type { InflateFn } from './S3MBParser.js';
71
71
  import type { GeoPackageEntry, GroupNode, PageLOD, ParsedTile, S3MAtlasRects, S3MTexture } from './types.js';
72
- import { parseGroupNodeEntries, parseSkeletonEntries, parsePickInfoEntries } from './S3MBParser.js';
73
- import { atlasAlphaMask, shrinkRectToOpaque } from './AtlasAlpha.js';
74
72
  import { packAtlases, decodeAtlasImage } from './AtlasPack.js';
75
73
  import type { PackedAtlases } from './AtlasPack.js';
76
74
  import { transcodeCRN } from './CRNDecode.js';
75
+ import { parseGroupNodeEntries, parseSkeletonEntries, parsePickInfoEntries } from './S3MBParser.js';
77
76
  import { resolveTextureKind, PixelFormat, TextureCompress } from './TextureFormat.js';
78
77
 
79
78
  /** 图集子区域,以及它绑定的几何顶点区间 */
@@ -961,19 +960,31 @@ export async function parseS3MBlockGroups(
961
960
  const atlasRects = buildAtlasRects(geoPackage, texturePackage, packed);
962
961
  const textures = buildTextureTable(texturePackage);
963
962
 
964
- // pageLOD:容器提供的 LOD/子瓦片信息是权威;geodes 在内层 groupNode 里
965
- const innerLods = groupNode.pageLods;
966
- const mergedLods: PageLOD[] = pageLods.map((lod, i) => {
967
- const inner = innerLods[i];
968
- return {
969
- rangeValue: lod.rangeValue,
970
- rangeMode: lod.rangeMode,
971
- boundingSphere: lod.boundingSphere,
972
- childTile: lod.childTile,
973
- geodes: inner?.geodes ?? [],
974
- ...(inner?.obb ? { obb: inner.obb } : {}),
975
- };
976
- });
963
+ // pageLOD:容器提供的 LOD/子瓦片信息是权威;geodes 在内层 groupNode 里。
964
+ // 注意:叶子瓦片(没有子代)在容器外层通常 pageLodCount 为 0,
965
+ // 此时绝对不能清空 pageLods,必须回退使用内层 groupNode 携带的真实 pageLOD
966
+ // (含关键的 geode 空间变换矩阵与包围球)。否则叶子瓦片会丢失 geode 矩阵,
967
+ // 退化为单位矩阵渲染,表现为「闪烁后瞬移/偏移数公里」以及因视锥剔除「渲染着消失」。
968
+ const innerLods = groupNode.pageLods ?? [];
969
+ let mergedLods: PageLOD[];
970
+ if (pageLods.length === 0) {
971
+ mergedLods = innerLods;
972
+ } else {
973
+ mergedLods = pageLods.map((lod, i) => {
974
+ const inner = innerLods[i];
975
+ return {
976
+ rangeValue: lod.rangeValue,
977
+ rangeMode: lod.rangeMode,
978
+ boundingSphere: lod.boundingSphere,
979
+ childTile: lod.childTile,
980
+ geodes: inner?.geodes ?? [],
981
+ ...(inner?.obb ? { obb: inner.obb } : {}),
982
+ };
983
+ });
984
+ if (innerLods.length > pageLods.length) {
985
+ mergedLods.push(...innerLods.slice(pageLods.length));
986
+ }
987
+ }
977
988
 
978
989
  tiles.push({
979
990
  version,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 高斯泼溅的 back-to-front 排序键(纯数学,可单测)
2
+ * 高斯泼溅的 back-to-front 排序(纯数学,可单测)
3
3
  *
4
4
  * alpha 混合是顺序相关的:先画近的再画远的,远的颜色会盖到近的上头,
5
5
  * 画面就是一片随视角乱闪的彩色噪点。3DGS 必须按视深从远到近画。
@@ -7,9 +7,38 @@
7
7
  * 排序键是**视图空间 z**(相机前方为负),升序 = 远的先画。
8
8
  * view 矩阵由调用方给(通常是 camera.matrixWorldInverse × mesh.matrixWorld),
9
9
  * 本模块不关心相机朝向约定,只取矩阵第三行做点积。
10
+ *
11
+ * ## 为什么是基数排序
12
+ *
13
+ * 这里是**每帧**都要跑的路径(相机一动就重排),原来的
14
+ * `TypedArray.sort((a, b) => keys[a] - keys[b])` 有两个代价:
15
+ * - 比较函数是 JS 调用,10 万点实测 38 ms/帧;
16
+ * - 每次调用都 `new Float32Array(count)` + `new Uint32Array(count)`,每帧分配 800 KB。
17
+ *
18
+ * 换成 4 遍 8 位 LSD 基数排序后实测 3 ms,且缓冲全部复用。没选桶排序是因为
19
+ * 深度分布会很不均匀(正对相机的墙面,几万个点的视深几乎相同),桶内再排会退化。
20
+ *
21
+ * 排序键先转成「无符号可比」的 32 位整数(IEEE754 的标准技巧),
22
+ * 这样无符号整数比较的大小顺序与 float 完全一致,基数排序才成立。
10
23
  */
11
24
  import type * as THREE from 'three';
12
25
 
26
+ /** 模块级 scratch:只增不减。排序是同步的,同一时刻只有一个在跑 */
27
+ let _keys = new Uint32Array(0);
28
+ let _scratch = new Uint32Array(0);
29
+ const _counts = new Uint32Array(256);
30
+
31
+ /** float → 无符号可比的 32 位键(正数翻符号位,负数按位取反) */
32
+ const _f32 = new Float32Array(1);
33
+ const _u32 = new Uint32Array(_f32.buffer);
34
+
35
+ function sortableBits(k: number): number {
36
+ _f32[0] = k;
37
+ const u = _u32[0]!;
38
+ // 负数的 IEEE754 表示是「符号位 1 + 绝对值」,直接按无符号比会把 -1 排到 0 后面
39
+ return u & 0x80000000 ? ~u >>> 0 : (u | 0x80000000) >>> 0;
40
+ }
41
+
13
42
  export function splatDepthOrder(
14
43
  positions: Float32Array,
15
44
  count: number,
@@ -17,14 +46,47 @@ export function splatDepthOrder(
17
46
  out?: Uint32Array
18
47
  ): Uint32Array {
19
48
  const idx = out && out.length >= count ? out : new Uint32Array(count);
20
- const keys = new Float32Array(count);
49
+ if (count <= 1) {
50
+ if (count === 1) idx[0] = 0;
51
+ return idx;
52
+ }
53
+
54
+ if (_keys.length < count) _keys = new Uint32Array(count);
55
+ if (_scratch.length < count) _scratch = new Uint32Array(count);
56
+ const keys = _keys;
57
+ const scratch = _scratch;
58
+
21
59
  const e = view.elements;
22
60
  const r0 = e[2]!, r1 = e[6]!, r2 = e[10]!, r3 = e[14]!;
23
61
  for (let i = 0; i < count; i++) {
62
+ keys[i] = sortableBits(r0 * positions[i * 3]! + r1 * positions[i * 3 + 1]! + r2 * positions[i * 3 + 2]! + r3);
24
63
  idx[i] = i;
25
- keys[i] = r0 * positions[i * 3]! + r1 * positions[i * 3 + 1]! + r2 * positions[i * 3 + 2]! + r3;
26
64
  }
27
- // TypedArray.sort 带比较函数时是数值排序,且 V8/JSC 对相同键稳定
28
- idx.sort((a, b) => keys[a]! - keys[b]!);
65
+
66
+ // 4 遍 × 8 位。偶数遍 → 结果一定回到 idx,不用再拷一次
67
+ let src = idx;
68
+ let dst = scratch;
69
+ for (let pass = 0; pass < 4; pass++) {
70
+ const shift = pass * 8;
71
+ _counts.fill(0);
72
+ for (let i = 0; i < count; i++) _counts[(keys[src[i]!]! >>> shift) & 0xff]!++;
73
+ // 前缀和 → 每个桶的起始写位置
74
+ let sum = 0;
75
+ for (let b = 0; b < 256; b++) {
76
+ const c = _counts[b]!;
77
+ _counts[b] = sum;
78
+ sum += c;
79
+ }
80
+ for (let i = 0; i < count; i++) {
81
+ const v = src[i]!;
82
+ dst[_counts[(keys[v]! >>> shift) & 0xff]!++] = v;
83
+ }
84
+ const t = src;
85
+ src = dst;
86
+ dst = t;
87
+ }
88
+
89
+ // 4 遍之后 src 就是 idx(起始 src=idx,偶数次交换后回到 idx);防御性兜一下
90
+ if (src !== idx) idx.set(src);
29
91
  return idx;
30
92
  }
@@ -287,6 +287,16 @@ export interface TileGeometryData {
287
287
  export interface AssembleOptions {
288
288
  /** 是否对 primitiveType=4 的三角面做瓦片内同材质合批(默认 true) */
289
289
  batching?: boolean;
290
+ /**
291
+ * 是否剔除「同单体、同位置、同绕向」的共面重复三角形(默认 true)。
292
+ *
293
+ * 开:修掉数据自重叠造成的斜向摩尔纹(`3D-BIMbuilding-2` 实测单 geo 有 1/3 重复)。
294
+ * 关:完全不碰索引,一个循环都不跑 —— 数据本来没问题、又不想付这份 CPU 时用。
295
+ *
296
+ * 注意这份开销按三角形数线性增长(10 万三角形约 90 ms),在 Worker 里跑不占主线程;
297
+ * `useWorker: false` 或 Worker 崩溃降级时它落在主线程上。
298
+ */
299
+ dedupeCoincident?: boolean;
290
300
  }
291
301
 
292
302
  /* ------------------------------------------------------------------ 合批合并 */
@@ -851,17 +861,20 @@ export function assembleTileGeometry(tile: ParsedTile, options: AssembleOptions
851
861
  // 数据自重叠:同单体、同位置、同绕向的重复三角形会互相压深度,出斜向摩尔纹。
852
862
  // 只在**单 geo** 的 primitive 上做:合批产物里多个 geo 的 batchId 各自从 0 起,
853
863
  // 混在一起会把「不同单体恰好共面」误判成同一条。
854
- for (const p of primitives) {
855
- if (p.primitiveType !== 4 || !p.index) continue;
856
- if (p.geoNames.length > 1) continue;
857
- const positions = p.attributes.position?.array;
858
- if (!positions) continue;
859
- const batchIds = p.attributes.aBatchId?.array ?? null;
860
- const { index, dropped } = dropCoincidentTriangles(p.index, positions, batchIds);
861
- if (dropped > 0) {
862
- p.index = index;
863
- p.indexCount = index.length;
864
- stats.deduped = (stats.deduped ?? 0) + dropped;
864
+ // 默认开;显式关掉时连循环都不进(stats 里也不会出现 deduped)。
865
+ if (options.dedupeCoincident !== false) {
866
+ for (const p of primitives) {
867
+ if (p.primitiveType !== 4 || !p.index) continue;
868
+ if (p.geoNames.length > 1) continue;
869
+ const positions = p.attributes.position?.array;
870
+ if (!positions) continue;
871
+ const batchIds = p.attributes.aBatchId?.array ?? null;
872
+ const { index, dropped } = dropCoincidentTriangles(p.index, positions, batchIds);
873
+ if (dropped > 0) {
874
+ p.index = index;
875
+ p.indexCount = index.length;
876
+ stats.deduped = (stats.deduped ?? 0) + dropped;
877
+ }
865
878
  }
866
879
  }
867
880
 
@@ -18,7 +18,7 @@ import {
18
18
  buildAttributes,
19
19
  pickEntryId,
20
20
  } from '../core/FeatureInfo.js';
21
- import type { AttributeQuery, AttributeQueryOptions, AttributeRow } from '../core/AttributeQuery.js';
21
+ import type { AttributeQuery, AttributeRow } from '../core/AttributeQuery.js';
22
22
  import type { StyledMaterial } from './StyleMaterial.js';
23
23
  import { restoreBaseBlendState } from './StyleMaterial.js';
24
24
 
@@ -127,14 +127,33 @@ function buildSplatGeometry(
127
127
  // 真实包围球:拾取要先用它做「射线有没有碰到这一块」的快速排除。
128
128
  // createQuadGeometry 里给的是空球(渲染靠 frustumCulled = false,不看它),
129
129
  // 留成 Infinity 的话每点一次要把所有瓦片的每个高斯都算一遍。
130
- const box = new THREE.Box3();
131
- const p = new THREE.Vector3();
132
- for (let i = 0; i < count; i++) {
133
- box.expandByPoint(p.set(posAttr.array[i * 3]!, posAttr.array[i * 3 + 1]!, posAttr.array[i * 3 + 2]!));
134
- }
130
+ //
131
+ // 包围盒和最长轴在同一遍里扫完:10 万点的瓦片,多一遍就是几毫秒。
132
+ const pos = posAttr.array;
133
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
134
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
135
135
  let maxAxis = 0;
136
136
  for (let i = 0; i < count; i++) {
137
- maxAxis = Math.max(maxAxis, scales[i * 3]!, scales[i * 3 + 1]!, scales[i * 3 + 2]!);
137
+ const x = pos[i * 3]!;
138
+ const y = pos[i * 3 + 1]!;
139
+ const z = pos[i * 3 + 2]!;
140
+ if (x < minX) minX = x;
141
+ if (x > maxX) maxX = x;
142
+ if (y < minY) minY = y;
143
+ if (y > maxY) maxY = y;
144
+ if (z < minZ) minZ = z;
145
+ if (z > maxZ) maxZ = z;
146
+ const sx = scales[i * 3]!;
147
+ const sy = scales[i * 3 + 1]!;
148
+ const sz = scales[i * 3 + 2]!;
149
+ if (sx > maxAxis) maxAxis = sx;
150
+ if (sy > maxAxis) maxAxis = sy;
151
+ if (sz > maxAxis) maxAxis = sz;
152
+ }
153
+ const box = new THREE.Box3();
154
+ if (count > 0) {
155
+ box.min.set(minX, minY, minZ);
156
+ box.max.set(maxX, maxY, maxZ);
138
157
  }
139
158
  const sphere = new THREE.Sphere(new THREE.Vector3(), 0);
140
159
  if (!box.isEmpty()) box.getBoundingSphere(sphere);
@@ -319,6 +338,13 @@ export interface BuildTileOptions {
319
338
  * 对倾斜摄影、大网格切片瓦片可减少 90%+ DrawCall,显著提高渲染帧率。
320
339
  */
321
340
  batching?: boolean;
341
+ /**
342
+ * 是否剔除同单体共面重复三角形(默认 true)。
343
+ *
344
+ * 见 `AssembleOptions.dedupeCoincident`。只在主线程现场组装时生效——
345
+ * 走 Worker 的路径在 Worker 里已经按默认值组装完了。
346
+ */
347
+ dedupeCoincident?: boolean;
322
348
  /**
323
349
  * Worker 侧已完成的纯数据几何组装(经典 .s3mb 路径由解析 Worker 顺带产出)。
324
350
  * 缺省时在主线程现场组装(core/TileGeometry 的同一份实现),两条路径产物一致。
@@ -526,7 +552,12 @@ export async function buildTileObjects(
526
552
 
527
553
  // 3) 纯数据几何组装:Worker 解析时已顺带产出就直接用,否则主线程现场组装
528
554
  // (core/TileGeometry 的同一份实现,两条路径产物一致)
529
- const assembled = options.assembled ?? assembleTileGeometry(tile, { batching: options.batching !== false });
555
+ const assembled =
556
+ options.assembled ??
557
+ assembleTileGeometry(tile, {
558
+ batching: options.batching !== false,
559
+ dedupeCoincident: options.dedupeCoincident,
560
+ });
530
561
  stats.vertices = assembled.stats.vertices;
531
562
  stats.edgeLines = assembled.stats.edgeLines;
532
563
  stats.instanced = assembled.stats.instanced;