s3m-three 0.4.19 → 0.4.21
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.
- package/dist/browser/assets/{s3mWorker-4iTAmWIu.js → s3mWorker-DvAUA8NJ.js} +1 -1
- package/dist/browser/s3m-three.iife.ext-three.js +243 -243
- package/dist/browser/s3m-three.iife.js +227 -227
- package/dist/core/Draco.d.ts.map +1 -1
- package/dist/core/Draco.js +0 -1
- package/dist/core/Draco.js.map +1 -1
- package/dist/core/S3MBlockParser.d.ts.map +1 -1
- package/dist/core/S3MBlockParser.js +27 -14
- package/dist/core/S3MBlockParser.js.map +1 -1
- package/dist/core/SplatSort.d.ts +14 -1
- package/dist/core/SplatSort.d.ts.map +1 -1
- package/dist/core/SplatSort.js +51 -4
- package/dist/core/SplatSort.js.map +1 -1
- package/dist/core/TileGeometry.d.ts +10 -0
- package/dist/core/TileGeometry.d.ts.map +1 -1
- package/dist/core/TileGeometry.js +17 -14
- package/dist/core/TileGeometry.js.map +1 -1
- package/dist/render/DatasetManager.d.ts +3 -1
- package/dist/render/DatasetManager.d.ts.map +1 -1
- package/dist/render/DatasetManager.js +2 -1
- package/dist/render/DatasetManager.js.map +1 -1
- package/dist/render/FeatureStyleManager.d.ts.map +1 -1
- package/dist/render/GeometryBuilder.d.ts +8 -0
- package/dist/render/GeometryBuilder.d.ts.map +1 -1
- package/dist/render/GeometryBuilder.js +62 -7
- package/dist/render/GeometryBuilder.js.map +1 -1
- package/dist/render/S3MViewer.d.ts +106 -440
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +297 -1653
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/render/SplatSorter.d.ts.map +1 -1
- package/dist/render/SplatSorter.js +8 -1
- package/dist/render/SplatSorter.js.map +1 -1
- package/dist/render/viewer/EdgeDecorator.d.ts +206 -0
- package/dist/render/viewer/EdgeDecorator.d.ts.map +1 -0
- package/dist/render/viewer/EdgeDecorator.js +628 -0
- package/dist/render/viewer/EdgeDecorator.js.map +1 -0
- package/dist/render/viewer/HeightIndex.d.ts +36 -0
- package/dist/render/viewer/HeightIndex.d.ts.map +1 -0
- package/dist/render/viewer/HeightIndex.js +98 -0
- package/dist/render/viewer/HeightIndex.js.map +1 -0
- package/dist/render/viewer/LightRig.d.ts +88 -0
- package/dist/render/viewer/LightRig.d.ts.map +1 -0
- package/dist/render/viewer/LightRig.js +229 -0
- package/dist/render/viewer/LightRig.js.map +1 -0
- package/dist/render/viewer/PickService.d.ts +109 -0
- package/dist/render/viewer/PickService.d.ts.map +1 -0
- package/dist/render/viewer/PickService.js +591 -0
- package/dist/render/viewer/PickService.js.map +1 -0
- package/dist/render/viewer/SelectionDecorator.d.ts +84 -0
- package/dist/render/viewer/SelectionDecorator.d.ts.map +1 -0
- package/dist/render/viewer/SelectionDecorator.js +339 -0
- package/dist/render/viewer/SelectionDecorator.js.map +1 -0
- package/dist/render/viewer/SplatLayer.d.ts +26 -0
- package/dist/render/viewer/SplatLayer.d.ts.map +1 -0
- package/dist/render/viewer/SplatLayer.js +44 -0
- package/dist/render/viewer/SplatLayer.js.map +1 -0
- package/dist/render/viewer/types.d.ts +256 -0
- package/dist/render/viewer/types.d.ts.map +1 -0
- package/dist/render/viewer/types.js +3 -0
- package/dist/render/viewer/types.js.map +1 -0
- package/dist/tileset/S3MTileset.d.ts +30 -3
- package/dist/tileset/S3MTileset.d.ts.map +1 -1
- package/dist/tileset/S3MTileset.js +99 -5
- package/dist/tileset/S3MTileset.js.map +1 -1
- package/dist/worker/s3mWorker.js +1 -1
- package/dist/worker/s3mWorker.js.map +1 -1
- package/docs/API.md +9 -28
- package/docs/FORMAT-NOTES.md +38 -14
- package/package.json +1 -1
- package/src/core/Draco.ts +1 -2
- package/src/core/S3MBlockParser.ts +27 -16
- package/src/core/SplatSort.ts +67 -5
- package/src/core/TileGeometry.ts +24 -11
- package/src/render/DatasetManager.ts +5 -1
- package/src/render/FeatureStyleManager.ts +1 -1
- package/src/render/GeometryBuilder.ts +59 -7
- package/src/render/S3MViewer.ts +327 -1916
- package/src/render/SplatSorter.ts +8 -1
- package/src/render/viewer/EdgeDecorator.ts +722 -0
- package/src/render/viewer/HeightIndex.ts +94 -0
- package/src/render/viewer/LightRig.ts +267 -0
- package/src/render/viewer/PickService.ts +651 -0
- package/src/render/viewer/SelectionDecorator.ts +377 -0
- package/src/render/viewer/SplatLayer.ts +44 -0
- package/src/render/viewer/types.ts +264 -0
- package/src/tileset/S3MTileset.ts +106 -6
package/docs/API.md
CHANGED
|
@@ -162,6 +162,7 @@ new S3MViewer(options?: S3MViewerOptions)
|
|
|
162
162
|
| `pickable` | `boolean` | `true` | 是否响应拾取 |
|
|
163
163
|
| `maxTiles` | `number` | `64` | 每个数据集驻留瓦片上限,超了淘汰最久没用的(不是停止加载) |
|
|
164
164
|
| `maxTriangles` | `number` | `Infinity` | 每个数据集**驻留三角形总数**的软上限(不限制)。`maxTiles` 数不了「一块精模几十万面」,这条按实际面数做刹车,走同一套 make-before-break 淘汰,不开洞 |
|
|
165
|
+
| `maxMemory` | `number` | `Infinity` | 每个数据集**驻留显存/内存字节数**的软上限(不限制)。包含几何缓冲精确字节与纹理显存估计,超限走同一套 make-before-break 淘汰,不开洞 |
|
|
165
166
|
| `totalMaxTiles` | `number` | — | 多数据集全局驻留瓦片总预算;开启后在可见数据集间动态均衡分配,防止叠加爆显存 |
|
|
166
167
|
| `light` | `LightOptions` | — | 场景光照配置,可配环境光与主平行光的颜色、强度、模式与方位角/高度角 |
|
|
167
168
|
| `maxConcurrent` | `number` | `4` | 每个数据集并发请求数 |
|
|
@@ -397,6 +398,8 @@ setLodPixelRatioCap(cap: number): void // Infinity = 不截断(默认
|
|
|
397
398
|
readonly lodPixelRatioCap: number
|
|
398
399
|
setMaxTriangles(n: number, datasetId?: string): void // Infinity = 不限制(默认)
|
|
399
400
|
readonly maxTriangles: number
|
|
401
|
+
setMaxMemory(bytes: number, datasetId?: string): void // 显存字节上限,Infinity = 不限制(默认)
|
|
402
|
+
readonly maxMemory: number
|
|
400
403
|
setAnisotropyCap(cap: number): void // Infinity = 取 GPU 最大值(默认)
|
|
401
404
|
readonly anisotropyCap: number
|
|
402
405
|
setMaxTextureSize(n: number): void // Infinity = 不限制(默认)
|
|
@@ -1311,6 +1314,7 @@ buildAttributes({ smid, box, triangles, floorHeight }) // → S3MFeatureAttribu
|
|
|
1311
1314
|
| `totalMaxTiles` | 多数据集全局瓦片总预算(如 `96` 或 `128`),自动在可见数据集间均衡分配配额,隐藏数据集时自动回收预算,防止多图层叠加爆显存 |
|
|
1312
1315
|
| `maxTiles` | 单数据集瓦片驻留上限,默认 64。叠加多个数据集时建议配合 `totalMaxTiles` 或下调至 32~48 |
|
|
1313
1316
|
| `maxTriangles` | 单数据集驻留面数软上限,默认不限制。`maxTiles` 只数瓦片个数,精模场景(一块几十万面)建议配 60 万~120 万 |
|
|
1317
|
+
| `maxMemory` | 单数据集驻留显存/内存软上限(字节),默认不限制。精确统计几何缓冲与纹理显存,超限按 make-before-break 自动主动回收 |
|
|
1314
1318
|
| `lodPixelRatioCap` | LOD 判定用屏幕高度的 DPR 上限,默认不截断。见下方「移动端 / 差设备」 |
|
|
1315
1319
|
| `anisotropyCap` | 各向异性过滤上限,默认取 GPU 最大值。移动端 4 / 8 通常肉眼无差 |
|
|
1316
1320
|
| `maxTextureSize` | 纹理长边像素上限,默认不限制。手机上 1024 即可,显存降到 1/4 |
|
|
@@ -1395,34 +1399,11 @@ viewer.flushOverrides();
|
|
|
1395
1399
|
|
|
1396
1400
|
## 已知限制
|
|
1397
1401
|
|
|
1398
|
-
-
|
|
1399
|
-
-
|
|
1400
|
-
-
|
|
1401
|
-
-
|
|
1402
|
-
|
|
1403
|
-
- **解析与 UV 重映射**:解析端把顶点 UV 用 `fract` 平铺语义重映射进图集空间,多图集自动走货架式装箱合并(`AtlasPack`),下游材质直接绑定整张图集;
|
|
1404
|
-
- **细级 LOD(leaf)外部图集加载**:细级瓦片的图集未内嵌原始像素(`data = null`),由 `_fillExternalAtlases` 解析子请求名(`_subRequestNames`):
|
|
1405
|
-
- 支持展开 `Tex_x.texblock#<三位数字串>` 多精灵容器索引;
|
|
1406
|
-
- 对齐官方 `Uci` 规范的 pack 请求:URL 拼装 `extratiles=<names>` 批量合并拉取,避免高并发零碎请求;
|
|
1407
|
-
- 响应体通过 `parseTexblockSegments` 解码 `kMi` 分段记录,并通过 `leafRequestCache` 与 `_leafPending` 实现跨瓦片并发请求自动去重缓存;
|
|
1408
|
-
- **组合画布与装配**:对 leaf 模式,解出子精灵 RGBA 缓冲,按 `subTexInfos[i]` 的偏移量(`offsetX`、`offsetY`)以标准自上而下行序拷贝进高分辨率组合画布;
|
|
1409
|
-
- **安全判定与重拼**:外部图集接入前通过 `evaluateAtlasUpgrade` 进行长宽比与下采样色差校验,判定通过后由 `rebuildBlockTextureState` 重新生成拼图与重算归一化 `texRect`;
|
|
1410
|
-
- 实现位于 `core/S3MBlockParser.ts`、`core/AtlasPack.ts` 与 `tileset/S3MTileset.ts`,并通过 `tests/unit/s3mblock.test.ts`、`tests/unit/leaf-atlas.test.ts` 和 `tests/unit/leaf-uv-render.test.ts` 交叉验证钉住。
|
|
1411
|
-
- **多块 MeshOpt 骨架**(`blockCount > 1`)已支持。这类骨架里**每块后面紧跟自己的索引包**(不是所有块之后再统一放一份),索引是块内局部编号;解析时按块顺序合并顶点流,再把每块索引加上顶点基数偏移,对外仍是一份顶点缓冲 + 多组索引包。实测 `3D-WorkSpace_pbr1/pbrmodel` 的 `blockCount=2` / `3` 瓦片可正常加载。
|
|
1412
|
-
- **切线顶点流**只跳过不解码(渲染没有法线贴图通道),带 TANGENT 的数据不会被判失败。
|
|
1413
|
-
- **S3M 1.x(`.s3m`,SuperMapCache)已支持**。这类服务的 `config` 返回 `SuperMapCache` XML(`sml:FileType = OSGBFile`),位置与范围按米制盒反算(见「锚点」);瓦片是 `73 33 6D 00` 魔数 + zlib 的老容器,`parseS3MB` 认出魔数后交给 `core/S3M1Parser.ts`,输出同形状的 `ParsedTile`,下游渲染 / LOD / 拾取零改动。
|
|
1414
|
-
**注意:它的下一级 LOD 并不是 `.osgb`** —— XML 里 `RangeDataList` 写的 `..._L16_0.osgb` 只是切片时的原始文件名,服务实际发布的是同名换 `.s3m` 后缀,走 `datas/<数据源>/data/path/<瓦片目录>/<名>.s3m`(实测 L16/L17/L18 全部 200,`?sceneName=` 不参与寻址)。所以整棵 LOD 树都是同一种容器,**不需要 OSG Binary 解码器**;早先「因为子瓦是 .osgb 所以做不了」的结论是没验证就下的,已作废。
|
|
1415
|
-
已标定:`73 33 6D 00` 魔数 + zlib;解压后 `uint32 xmlSize + XML + 补齐 4 字节对齐(实测 2~3 个 0x30,不是固定标记)+ (uint32 size + data) × 3`,三段长度链在四块样本上全部精确闭合;几何流 = `uint32 meshCount` + 每块网格 `{ geoName, 7, 772, V, 0 }` + `V × 16B` 顶点 `(x, y, z, z)` + 数组区 + `uint32 1` + `uint32 materialId`(与 XML 材质号一致);纹理流 = `uint32 count` + 每条 `{ id, width, height, format, size, flag }` + `size` 字节(实测 `format=14`、0.5 B/px = DXT1,`28 + size` 精确等于段长);索引确认为 u16(值域最大恰为 `V-1`);几何里**没有法线数组**,接解码时要 `computeVertexNormals()`。
|
|
1416
|
-
接这种格式时有三个坑,都是实测出来的,别凭直觉:
|
|
1417
|
-
1. **几何里没有法线数组**(全段扫 `|v|≈1` 的最长游程只有 4~10 个),建完要 `computeVertexNormals()`;
|
|
1418
|
-
2. **头里的 `format` 不是 `S3MPixelFormat` 枚举** —— 实测四块瓦片都是 `14`,而这枚举里 14 = BGRA(4 B/px),照搬就会把 0.5 B/px 的 DXT1 当 4 字节读,满屏彩噪。必须按 `size / (width*height)` 反推(`pixelFormatOfBpp`);
|
|
1419
|
-
3. **UV 是「v = 0 对应文件首行」**,与 DXT 解码器内部那次 GL 方向的 Y 翻转相反。不抵消的话几何的 v(实测 0.483~0.983)会正好采到图集空的那半张 —— 区域均值亮度 5.6 对比 76.1,模型上就是一片黑块。所以 v1 纹理要置 `S3MTexture.flipY = true`。
|
|
1420
|
-
|
|
1421
|
-
子瓦地址规则见 `s3m1ChildUrl`:XML 的 `RangeDataList` 给的是切片时残留的 `.osgb` 名,实际发布是同名换 `.s3m` 并插在 `datas/<数据源>/data/path/<瓦片目录>/` 下;父瓦自己已经在 `data/path` 下时只能换文件名,再插一层就成了 `data/path/data/path/...` 全部 404(有回归测试钉住)。
|
|
1422
|
-
- **高斯泼溅(`3DGaussianSplatting`)只按点渲染**。加载、细化、出图整条链路是通的(这类瓦片没有索引包,顶点即点,`GeometryBuilder` 会补一个「顶点即点」的伪包走 `THREE.Points`;不补的话整块瓦片落到「无可用几何」被丢掉,这是之前"加载不出来"的第二个原因)。但画出来的是带顶色的点集,不是真正的泼溅:`gsScale` / `gsRotation` 两条自定义流的量化格式还没标定,而且颜色是 RGBA(A 通道是泼溅不透明度),现在的顶点色路径会丢掉 alpha。要上 EWA splatting 得先按深度层级取样把这两条流对准,再补协方差 + 深度排序的 alpha 混合。
|
|
1423
|
-
- **KTX2** 不在 S3M 的 `pixelFormat` 枚举里,iServer 也不会发;要支持需自行引入 `basis_transcoder.wasm` 并解析容器,属于「换数据源才需要考虑」的项。
|
|
1424
|
-
- **骨骼动画**解析了但未驱动。
|
|
1425
|
-
- 跨城市量级的数据集叠加会有 float32 精度问题。
|
|
1402
|
+
- **实例化单体(`instanceMode` 16 / 17)**:白模中较常见(`pickInfo` 里给的是 `instanceIds` 而不是 `batchId`)。目前支持正常网格渲染,单体级操作(`setFeatureStyle` / `filterByField` / `elevateByField`)与 GPU 单体拾取走的都是 `aBatchId`,对实例化几何暂不生效。
|
|
1403
|
+
- **切线(TANGENT)顶点流不参与法线贴图**:着色器目前未接入切线空间法线贴图通道,TANGENT 顶点流直接跳过不解码;带 TANGENT 的模型可正常着色渲染,不受影响。
|
|
1404
|
+
- **骨骼动画未挂接驱动**:S3M 数据中的骨骼动画元信息与 JSON 已解析,但目前未接入逐帧骨骼驱动。
|
|
1405
|
+
- **KTX2 纹理未内置**:S3M 官方 `pixelFormat` 枚举与主流 iServer 服务不包含 KTX2 / Basis 压缩纹理,当前未内置 `basis_transcoder.wasm` 转码。
|
|
1406
|
+
- **多数据集跨城市叠加精度**:跨城市广域(数十公里以上)多个不同中心的数据集叠加受 WebGL 单精度 float32 深度和位置精度限制,建议同场景数据集位于同一城市量级内(几公里内)。
|
|
1426
1407
|
|
|
1427
1408
|
---
|
|
1428
1409
|
|
package/docs/FORMAT-NOTES.md
CHANGED
|
@@ -96,25 +96,49 @@ byte[4] 's3m\0' + zlib
|
|
|
96
96
|
|
|
97
97
|
## 五、S3MBLOCK / texblock(纹理分包)
|
|
98
98
|
|
|
99
|
-
从 `3D-CBD/Xiaopin@CBD`
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
-
|
|
115
|
-
-
|
|
116
|
-
-
|
|
117
|
-
-
|
|
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
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,
|
|
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
|
|
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
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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,
|
package/src/core/SplatSort.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
}
|
package/src/core/TileGeometry.ts
CHANGED
|
@@ -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
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
p.index
|
|
863
|
-
|
|
864
|
-
|
|
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
|
|
|
@@ -24,6 +24,8 @@ export interface S3MDatasetOptions {
|
|
|
24
24
|
maxTiles?: number;
|
|
25
25
|
/** 该数据集驻留三角形总数的软上限(默认跟随图层级设置;都不传则不限制) */
|
|
26
26
|
maxTriangles?: number;
|
|
27
|
+
/** 该数据集驻留显存/内存字节数的软上限(默认跟随图层级设置;都不传则不限制) */
|
|
28
|
+
maxMemory?: number;
|
|
27
29
|
/** 该数据集的纹理缓存条数上限(默认跟随图层级设置,图层不传则 128) */
|
|
28
30
|
maxImageCache?: number;
|
|
29
31
|
lodRangeScale?: number;
|
|
@@ -185,7 +187,8 @@ export class DatasetManager {
|
|
|
185
187
|
callbacks: DatasetManagerCallbacks,
|
|
186
188
|
maxTriangles: number = Infinity,
|
|
187
189
|
maxImageCache: number = 128,
|
|
188
|
-
authenticate?: AuthenticateFn
|
|
190
|
+
authenticate?: AuthenticateFn,
|
|
191
|
+
maxMemory: number = Infinity
|
|
189
192
|
): Promise<S3MDatasetEntry> {
|
|
190
193
|
const existing = this.datasets.get(ds.id);
|
|
191
194
|
if (existing) return existing;
|
|
@@ -216,6 +219,7 @@ export class DatasetManager {
|
|
|
216
219
|
maxTiles: ds.maxTiles ?? maxTiles,
|
|
217
220
|
maxConcurrent: maxConcurrent,
|
|
218
221
|
maxTriangles: ds.maxTriangles ?? maxTriangles,
|
|
222
|
+
maxMemory: ds.maxMemory ?? maxMemory,
|
|
219
223
|
maxImageCache: ds.maxImageCache ?? maxImageCache,
|
|
220
224
|
lodRangeScale: ds.lodRangeScale ?? defaultLodRangeScale,
|
|
221
225
|
buildTile: buildTile ?? null,
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
buildAttributes,
|
|
19
19
|
pickEntryId,
|
|
20
20
|
} from '../core/FeatureInfo.js';
|
|
21
|
-
import type { AttributeQuery,
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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);
|
|
@@ -304,6 +323,7 @@ export interface TileBuildStats {
|
|
|
304
323
|
/** 该瓦片的高斯实例数。泼溅瓦片才有值,LOD 判据用它算屏幕覆盖率 */
|
|
305
324
|
splatCount?: number;
|
|
306
325
|
/** 被跳过的东西,加载完可以直接在控制台看 */
|
|
326
|
+
memory?: number;
|
|
307
327
|
skipped: string[];
|
|
308
328
|
}
|
|
309
329
|
|
|
@@ -319,6 +339,13 @@ export interface BuildTileOptions {
|
|
|
319
339
|
* 对倾斜摄影、大网格切片瓦片可减少 90%+ DrawCall,显著提高渲染帧率。
|
|
320
340
|
*/
|
|
321
341
|
batching?: boolean;
|
|
342
|
+
/**
|
|
343
|
+
* 是否剔除同单体共面重复三角形(默认 true)。
|
|
344
|
+
*
|
|
345
|
+
* 见 `AssembleOptions.dedupeCoincident`。只在主线程现场组装时生效——
|
|
346
|
+
* 走 Worker 的路径在 Worker 里已经按默认值组装完了。
|
|
347
|
+
*/
|
|
348
|
+
dedupeCoincident?: boolean;
|
|
322
349
|
/**
|
|
323
350
|
* Worker 侧已完成的纯数据几何组装(经典 .s3mb 路径由解析 Worker 顺带产出)。
|
|
324
351
|
* 缺省时在主线程现场组装(core/TileGeometry 的同一份实现),两条路径产物一致。
|
|
@@ -526,7 +553,12 @@ export async function buildTileObjects(
|
|
|
526
553
|
|
|
527
554
|
// 3) 纯数据几何组装:Worker 解析时已顺带产出就直接用,否则主线程现场组装
|
|
528
555
|
// (core/TileGeometry 的同一份实现,两条路径产物一致)
|
|
529
|
-
const assembled =
|
|
556
|
+
const assembled =
|
|
557
|
+
options.assembled ??
|
|
558
|
+
assembleTileGeometry(tile, {
|
|
559
|
+
batching: options.batching !== false,
|
|
560
|
+
dedupeCoincident: options.dedupeCoincident,
|
|
561
|
+
});
|
|
530
562
|
stats.vertices = assembled.stats.vertices;
|
|
531
563
|
stats.edgeLines = assembled.stats.edgeLines;
|
|
532
564
|
stats.instanced = assembled.stats.instanced;
|
|
@@ -738,6 +770,26 @@ export async function buildTileObjects(
|
|
|
738
770
|
|
|
739
771
|
if (!group.children.length) stats.skipped.push('tile 无可用几何');
|
|
740
772
|
|
|
773
|
+
let totalGeoBytes = 0;
|
|
774
|
+
for (const obj of objects) {
|
|
775
|
+
const mesh = obj as THREE.Mesh;
|
|
776
|
+
const geo = mesh.geometry;
|
|
777
|
+
if (geo) {
|
|
778
|
+
if (geo.attributes) {
|
|
779
|
+
for (const attr of Object.values(geo.attributes)) {
|
|
780
|
+
if (attr && attr.array) totalGeoBytes += attr.array.byteLength;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
if (geo.index && geo.index.array) totalGeoBytes += geo.index.array.byteLength;
|
|
784
|
+
}
|
|
785
|
+
if ((mesh as THREE.InstancedMesh).isInstancedMesh) {
|
|
786
|
+
const inst = mesh as THREE.InstancedMesh;
|
|
787
|
+
if (inst.instanceMatrix && inst.instanceMatrix.array) totalGeoBytes += inst.instanceMatrix.array.byteLength;
|
|
788
|
+
if (inst.instanceColor && inst.instanceColor.array) totalGeoBytes += inst.instanceColor.array.byteLength;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
stats.memory = totalGeoBytes;
|
|
792
|
+
|
|
741
793
|
group.userData.textureKeys = usedTextureKeys;
|
|
742
794
|
return { group, stats, objects, textureKeys: usedTextureKeys };
|
|
743
795
|
}
|