s3m-three 0.4.18 → 0.4.19
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 +1 -0
- package/dist/browser/s3m-three.iife.ext-three.js +406 -91
- package/dist/browser/s3m-three.iife.js +434 -119
- package/dist/core/GaussianSplat.d.ts +64 -0
- package/dist/core/GaussianSplat.d.ts.map +1 -0
- package/dist/core/GaussianSplat.js +139 -0
- package/dist/core/GaussianSplat.js.map +1 -0
- package/dist/core/S3M1Parser.d.ts +1 -1
- package/dist/core/S3M1Parser.d.ts.map +1 -1
- package/dist/core/S3M1Parser.js +320 -25
- package/dist/core/S3M1Parser.js.map +1 -1
- package/dist/core/S3MBParser.d.ts.map +1 -1
- package/dist/core/S3MBParser.js +13 -0
- package/dist/core/S3MBParser.js.map +1 -1
- package/dist/core/S3MBlockParser.d.ts.map +1 -1
- package/dist/core/S3MBlockParser.js +56 -14
- package/dist/core/S3MBlockParser.js.map +1 -1
- package/dist/core/SplatLod.d.ts +36 -0
- package/dist/core/SplatLod.d.ts.map +1 -0
- package/dist/core/SplatLod.js +54 -0
- package/dist/core/SplatLod.js.map +1 -0
- package/dist/core/SplatProjection.d.ts +75 -0
- package/dist/core/SplatProjection.d.ts.map +1 -0
- package/dist/core/SplatProjection.js +162 -0
- package/dist/core/SplatProjection.js.map +1 -0
- package/dist/core/SplatSort.d.ts +13 -0
- package/dist/core/SplatSort.d.ts.map +1 -0
- package/dist/core/SplatSort.js +14 -0
- package/dist/core/SplatSort.js.map +1 -0
- package/dist/core/TexCoordZ.d.ts +94 -0
- package/dist/core/TexCoordZ.d.ts.map +1 -0
- package/dist/core/TexCoordZ.js +125 -0
- package/dist/core/TexCoordZ.js.map +1 -0
- package/dist/core/TextureDecode.d.ts +19 -0
- package/dist/core/TextureDecode.d.ts.map +1 -1
- package/dist/core/TextureDecode.js +53 -11
- package/dist/core/TextureDecode.js.map +1 -1
- package/dist/core/TileGeometry.d.ts +29 -0
- package/dist/core/TileGeometry.d.ts.map +1 -1
- package/dist/core/TileGeometry.js +130 -14
- package/dist/core/TileGeometry.js.map +1 -1
- package/dist/core/degradations.d.ts +37 -0
- package/dist/core/degradations.d.ts.map +1 -0
- package/dist/core/degradations.js +32 -0
- package/dist/core/degradations.js.map +1 -0
- package/dist/core/types.d.ts +36 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/render/DatasetManager.d.ts +11 -0
- package/dist/render/DatasetManager.d.ts.map +1 -1
- package/dist/render/DatasetManager.js +2 -0
- package/dist/render/DatasetManager.js.map +1 -1
- package/dist/render/FeatureStyleManager.d.ts.map +1 -1
- package/dist/render/FeatureStyleManager.js +7 -5
- package/dist/render/FeatureStyleManager.js.map +1 -1
- package/dist/render/GeometryBuilder.d.ts +24 -0
- package/dist/render/GeometryBuilder.d.ts.map +1 -1
- package/dist/render/GeometryBuilder.js +196 -5
- package/dist/render/GeometryBuilder.js.map +1 -1
- package/dist/render/Picker.d.ts.map +1 -1
- package/dist/render/Picker.js +19 -5
- package/dist/render/Picker.js.map +1 -1
- package/dist/render/S3MViewer.d.ts +69 -0
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +222 -12
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/render/SplatMaterial.d.ts +103 -0
- package/dist/render/SplatMaterial.d.ts.map +1 -0
- package/dist/render/SplatMaterial.js +388 -0
- package/dist/render/SplatMaterial.js.map +1 -0
- package/dist/render/SplatSorter.d.ts +14 -0
- package/dist/render/SplatSorter.d.ts.map +1 -0
- package/dist/render/SplatSorter.js +59 -0
- package/dist/render/SplatSorter.js.map +1 -0
- package/dist/render/StyleMaterial.d.ts +48 -0
- package/dist/render/StyleMaterial.d.ts.map +1 -1
- package/dist/render/StyleMaterial.js +292 -69
- package/dist/render/StyleMaterial.js.map +1 -1
- package/dist/render/TextureFactory.d.ts +10 -0
- package/dist/render/TextureFactory.d.ts.map +1 -1
- package/dist/render/TextureFactory.js +35 -5
- package/dist/render/TextureFactory.js.map +1 -1
- package/dist/render/lightingPreset.d.ts +53 -0
- package/dist/render/lightingPreset.d.ts.map +1 -0
- package/dist/render/lightingPreset.js +36 -0
- package/dist/render/lightingPreset.js.map +1 -0
- package/dist/render/materialAlpha.d.ts +51 -0
- package/dist/render/materialAlpha.d.ts.map +1 -0
- package/dist/render/materialAlpha.js +68 -0
- package/dist/render/materialAlpha.js.map +1 -0
- package/dist/tileset/S3MTileset.d.ts +42 -0
- package/dist/tileset/S3MTileset.d.ts.map +1 -1
- package/dist/tileset/S3MTileset.js +52 -1
- package/dist/tileset/S3MTileset.js.map +1 -1
- package/dist/vendor/browser-node-stub.js +10 -0
- package/dist/worker/s3mWorker.js +1166 -979
- package/dist/worker/s3mWorker.js.map +1 -1
- package/docs/API.md +55 -1
- package/docs/FORMAT-NOTES.md +338 -0
- package/package.json +1 -1
- package/src/core/GaussianSplat.ts +175 -0
- package/src/core/S3M1Parser.ts +330 -24
- package/src/core/S3MBParser.ts +18 -0
- package/src/core/S3MBlockParser.ts +56 -11
- package/src/core/SplatLod.ts +83 -0
- package/src/core/SplatProjection.ts +231 -0
- package/src/core/SplatSort.ts +30 -0
- package/src/core/TexCoordZ.ts +160 -0
- package/src/core/TextureDecode.ts +63 -11
- package/src/core/TileGeometry.ts +138 -14
- package/src/core/degradations.ts +74 -0
- package/src/core/types.ts +32 -0
- package/src/index.ts +8 -1
- package/src/render/DatasetManager.ts +10 -0
- package/src/render/FeatureStyleManager.ts +7 -5
- package/src/render/GeometryBuilder.ts +239 -6
- package/src/render/Picker.ts +21 -5
- package/src/render/S3MViewer.ts +255 -11
- package/src/render/SplatMaterial.ts +433 -0
- package/src/render/SplatSorter.ts +59 -0
- package/src/render/StyleMaterial.ts +339 -69
- package/src/render/TextureFactory.ts +39 -5
- package/src/render/lightingPreset.ts +76 -0
- package/src/render/materialAlpha.ts +75 -0
- package/src/tileset/S3MTileset.ts +78 -1
- package/src/vendor/browser-node-stub.js +10 -0
- package/dist/browser/assets/s3mWorker-BjPOUgjg.js +0 -1
package/docs/API.md
CHANGED
|
@@ -297,6 +297,23 @@ interface LightOptions {
|
|
|
297
297
|
|
|
298
298
|
调用 `setLight` 会即时更新场景光源并自动触发重新绘制(`triggerRepaint()`)。
|
|
299
299
|
|
|
300
|
+
### 金属度 / 粗糙度与环境贴图
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
viewer.setStyle({ metalness: 0.85, roughness: 0.25 });
|
|
304
|
+
await viewer.setEnvironment('/env/city_2k.hdr', 1.0); // 等距柱状图(2:1),null = 关闭
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
- `metalness` / `roughness` 走共享 uniform(`uMetalness` / `uRoughness`),注入在
|
|
308
|
+
`metalnessmap_fragment` 之后改写 `metalnessFactor` / `roughnessFactor`:不重建材质、
|
|
309
|
+
不重编译 program,可以进 `setStyle` 的过渡通道逐帧插值。
|
|
310
|
+
- **不传 = 不覆盖**(内部用 `-1` 当哨兵),保持数据自身的口径:`metalness` 恒 0,
|
|
311
|
+
`roughness` 由 S3M 的 `shininess` 映射(常见 0.95)。
|
|
312
|
+
- 只有 `materialQuality: 'pbr'`(默认)支持;`'lambert'` 模板里没有这两个 chunk,注入是 no-op。
|
|
313
|
+
- ⚠️ 金属几乎没有漫反射,**没有环境贴图时 `metalness` 一高就整片发黑**,只剩平行光的
|
|
314
|
+
高光点。先 `setEnvironment()` 挂一张等距柱状图(内部会转成 PMREM 预滤波),
|
|
315
|
+
强度由 `scene.environmentIntensity` 控制。
|
|
316
|
+
|
|
300
317
|
### 单体样式与状态
|
|
301
318
|
|
|
302
319
|
```ts
|
|
@@ -396,9 +413,44 @@ getBounds(): [[number, number], [number, number]] | null // 合并经纬度范
|
|
|
396
413
|
interface Stats {
|
|
397
414
|
tiles: number; loading: number; failed: number;
|
|
398
415
|
meshes: number; vertices: number; triangles: number; datasets: number;
|
|
416
|
+
/** GPU 侧观测,没有渲染器(未 mount / 纯解析)时恒为 0 */
|
|
417
|
+
drawCalls: number; programs: number; textures: number; geometries: number;
|
|
418
|
+
/** 场景里出现过的降级项(跨数据集去重)。空数组 = 所有数据的能力都完整实现了 */
|
|
419
|
+
degradations: DegradationKind[];
|
|
399
420
|
}
|
|
400
421
|
```
|
|
401
422
|
|
|
423
|
+
### 降级上报(degradations)
|
|
424
|
+
|
|
425
|
+
**「解析不了」应该抛错,「解析得了但画不出该有的样子」必须显式说出来。** 后者最容易被写成静默降级 —— 用户看到一个能跑但不对的画面,会去怀疑数据、怀疑网络、怀疑显卡,很难怀疑到「这个分支还没做完」。
|
|
426
|
+
|
|
427
|
+
目前只有一类:
|
|
428
|
+
|
|
429
|
+
| kind | 数据 | 现状 |
|
|
430
|
+
|:---|:---|:---|
|
|
431
|
+
| `gaussian-splatting-points` | 3DGS(`dataType = "3DGaussianSplatting"`) | **按 Points 渲染**。`gsScale` / `gsRotation` 的量化格式未标定,画不出 EWA splatting,详见 [FORMAT-NOTES 第九节](./FORMAT-NOTES.md#九未标定字段d-级) |
|
|
432
|
+
|
|
433
|
+
三层可见,任选自己需要的:
|
|
434
|
+
|
|
435
|
+
1. **`ParsedTile.degradations`** —— 单块瓦片,解析期打标。只有真的命中才写字段,非 3DGS 数据的 `ParsedTile` 形状与旧版逐字一致。
|
|
436
|
+
2. **`stats.degradations`** —— 整个场景,跨数据集去重。状态栏 / 诊断面板直接读它,不必翻 console。
|
|
437
|
+
3. **`console.warn`** —— 首次遇到告警一次。同一类只报一次:一个 3DGS 服务一上来几十块瓦片都是同一个原因,不去重会把真正有用的日志冲掉。
|
|
438
|
+
|
|
439
|
+
处置策略 `unsupported`(默认 `'warn'`),图层级和数据集级都能配,数据集优先:
|
|
440
|
+
|
|
441
|
+
```ts
|
|
442
|
+
new S3MLayer({ /* ... */, unsupported: 'warn' }) // 图层级默认
|
|
443
|
+
viewer.addDataset({ /* ... */, unsupported: 'throw' }) // 数据集级覆盖
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
| 值 | 行为 | 适用 |
|
|
447
|
+
|:---|:---|:---|
|
|
448
|
+
| `'warn'`(默认) | 首次告警一次,照常出图 | 大多数情况。**不传参时的渲染路径与旧版逐字未变**,只是多一条日志 |
|
|
449
|
+
| `'throw'` | 拒绝这类瓦片,标 `permanent` 不重试(不会在重试里空转) | 宁可不出图,也不要出一张看不出是错的图 |
|
|
450
|
+
| `'silent'` | 只进 `stats.degradations`,不告警 | 已经自己接管提示的 UI |
|
|
451
|
+
|
|
452
|
+
纯函数 `planDegradations(incoming, known, policy)` 也导出了,需要自定义聚合(比如按数据集分行显示)时可以直接用。
|
|
453
|
+
|
|
402
454
|
### 投影
|
|
403
455
|
|
|
404
456
|
```ts
|
|
@@ -444,6 +496,8 @@ interface S3MStyle {
|
|
|
444
496
|
edgeHullWidth?: number; // 背面外扩轮廓厚度(米,默认 0.9),仅 edges 含 'hull'
|
|
445
497
|
forceCpuEdges?: boolean; // 忽略数据自带的棱线包,一律 CPU 现算(默认 false)
|
|
446
498
|
roofThreshold?: number; // 顶面法线判定阈值,默认 0.65
|
|
499
|
+
metalness?: number; // 金属度 0~1,不传 = 保持数据自身(0)。pbr 才有意义,见「金属度 / 粗糙度与环境贴图」
|
|
500
|
+
roughness?: number; // 粗糙度 0~1,不传 = 保持数据自身(shininess 映射,常见 0.95)
|
|
447
501
|
fadeIn?: number; // 新瓦片淡入时长(毫秒,默认 0 = 关闭)
|
|
448
502
|
}
|
|
449
503
|
```
|
|
@@ -844,7 +898,7 @@ setMode(mode: StyleMode, opts?: { duration?: number; dim?: number }): Promise<vo
|
|
|
844
898
|
|---|---|---|
|
|
845
899
|
| `style.opacity` | 1 | 共享 `uOpacity`;混合开关只在跨 `0.999` 时切一次 |
|
|
846
900
|
| `style.colorA` / `colorB` / `colorC` / `selectColor` / `edgeColor` | 3 | 线性空间 RGB |
|
|
847
|
-
| `style.texMix` / `floorHeight` / `selectOpacity` / `selectEmissive` / `selectElevation` / `dimFactor` / `roofThreshold` / `ambientGain` / `edgeWidth` / `edgeOpacity` / `edgeOffset` / `edgeHullWidth` / `outlineWidth` | 1 | 直接写 uniform / 字段 |
|
|
901
|
+
| `style.texMix` / `floorHeight` / `selectOpacity` / `selectEmissive` / `selectElevation` / `dimFactor` / `roofThreshold` / `ambientGain` / `metalness` / `roughness` / `edgeWidth` / `edgeOpacity` / `edgeOffset` / `edgeHullWidth` / `outlineWidth` | 1 | 直接写 uniform / 字段 |
|
|
848
902
|
| `style.edgeFade` | 1 | 棱线淡入乘子,`setEdges` 内部使用 |
|
|
849
903
|
| `style.fade` | 1 | 整体压暗乘子,`setMode` 内部使用 |
|
|
850
904
|
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# S3M 格式逆向档案
|
|
2
|
+
|
|
3
|
+
这份文档记录的是:**哪些字段是确定知道的、凭什么确定、上游一变怎么第一时间发现。**
|
|
4
|
+
|
|
5
|
+
S3M 没有一份能直接照着实现的完整公开规范。`Specification/S3MB/S3MBBinary.md` 覆盖了 v1/v2 的骨架,但 v3 起(MeshOpt、Draco、S3MBLOCK、CRN、高斯泼溅)大量布局只能从真实瓦片逐字节标定,再拿官方 SDK 的混淆产物交叉对账。这类知识原本散落在七个解析器的头部注释里,改一次代码就得翻七个文件——集中在这里,是为了让下一个人(或者半年后的自己)能在半小时内接手。
|
|
6
|
+
|
|
7
|
+
**最后核对**:0.4.18 / 2026-10-28。读到与代码不一致时,以代码为准并回来改这份文档。
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 一、置信度分级
|
|
12
|
+
|
|
13
|
+
每个结论都标了级别。**没有标记的一律视为 A 级**。
|
|
14
|
+
|
|
15
|
+
| 级别 | 含义 | 变了会怎样 |
|
|
16
|
+
|:---|:---|:---|
|
|
17
|
+
| **A** | 官方源码 + 规范文档 + 实测三方一致 | 基本不会变;变了说明大版本换代 |
|
|
18
|
+
| **B** | 实测标定 + 内部不变量闭合(长度链严丝合缝、OBB 与包围球同心、`bytesRead === bytesTotal`) | 有测试兜着,改坏必红 |
|
|
19
|
+
| **C** | 实测标定,但样本少(1~2 块),且没有独立的内部闭合可验 | **最危险**,换个数据集就可能不成立 |
|
|
20
|
+
| **D** | 字段位置已知、语义未知(能跳过,不能用) | 不影响加载,但对应能力做不出来 |
|
|
21
|
+
|
|
22
|
+
C 级的处置原则:**只在新样本进来时提高级别,不靠推理提高**。D 级的处置原则:**保留原始字节,不猜语义**。
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 二、先判断是哪一种数据
|
|
27
|
+
|
|
28
|
+
同一个 URL 样式底下可能是三种完全不同的容器。**看两处就够**:`…/rest/realspace/datas.json` 列出的数据源名,和 `…/datas/<数据源>/config` 返回的是 JSON 还是 XML。
|
|
29
|
+
|
|
30
|
+
| 形态 | config | 瓦片 | 解析器 |
|
|
31
|
+
|:---|:---|:---|:---|
|
|
32
|
+
| S3M 瓦片缓存(主流) | JSON(`rootTiles` / `position.point3D` / `geoBounds`) | `.s3mb` v2 / v3 / v3.01,或 `.s3mblock` | `S3MBParser` / `S3MBlockParser` |
|
|
33
|
+
| SuperMapCache / S3M 1.x | **XML**(`<SuperMapCache>`,`sml:FileType = OSGBFile`) | `.s3m`(整棵 LOD 树同一种容器) | `S3M1Parser` |
|
|
34
|
+
| 高斯泼溅 | JSON,`dataType = "3DGaussianSplatting"` | `.s3mb` v3.01 + MeshOpt,**无索引包** | `S3MBParser` + `MeshOpt` |
|
|
35
|
+
|
|
36
|
+
**S3M 1.x 必须靠魔数分流,不能靠配置。** 头 4 字节 `73 33 6D 00`(`"s3m\0"`)紧接 `78 da` 的 zlib。不分流的话,头 4 字节会被 float32 读成 1e-38 的「版本号」,再把 zlib 头当长度,报出来的是 `RangeError: Invalid typed array length` —— 一个彻底误导人的错。
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 三、S3MB v2 / v3 / v3.01
|
|
41
|
+
|
|
42
|
+
实现依据:`SuperMap/s3m-spec` 的 `S3M_SDK/S3M_JS/S3M_module/S3MParser/S3ModelParser.js`(Apache-2.0)+ `Specification/S3MB/S3MBBinary.md`。 —— **A 级**
|
|
43
|
+
|
|
44
|
+
与官方实现的三处已知分歧:
|
|
45
|
+
|
|
46
|
+
1. **官方 v3 非压缩分支把整个 ArrayBuffer(含文件头)当数据流** —— 官方 bug,这里按偏移读。
|
|
47
|
+
2. **`position` 的键名在 v3.01 换过**:老数据是 `position.x/y/z` + `units`,v3.01 是 `position.point3D` + `unit`。两个键名都得认,否则**投影单位是米的数据会被当成度**,模型甩到几千公里外。(B 级,实测)
|
|
48
|
+
3. **`geoBounds` 与 `position` 偶尔不一致**(生产时的占位值)。`_resolveAnchor` 的处置:偏移超过 0.5° 时改用 `geoBounds` 中心并告警。但反向不成立 —— 全 0 的退化 `geoBounds` 不能反过来否定一个正常的 `position`,那等于把模型甩到 (0,0)。(C 级)
|
|
49
|
+
|
|
50
|
+
### 3.1 骨架条目边界(B 级,重要)
|
|
51
|
+
|
|
52
|
+
v3.01 的多骨架瓦片里,**每块骨架自带的 `skeletonStreamSize` 是权威边界**;OBB 一律按「条目末尾 96 字节」定位,读完对齐到条目末尾。
|
|
53
|
+
|
|
54
|
+
**不要用「读出来像不像包围盒」做启发式。** 实测踩过的坑:子瓦第 3 块骨架在几何段与末尾 OBB 之间比别块多 4 字节填充,按启发式读出来的框是 `center(0,0,0) + extent 2.0/4.0` —— 有限、量级正常,于是放行,整条流从此偏 4 字节,下一块的 `skeletonStreamSize` 读成 1072419456、名字长度读成 13360,最后抛「未知骨架标记 0」。整套高斯数据加载失败,而报错位置离真正的错位点差了几万字节。
|
|
55
|
+
|
|
56
|
+
`readV3BoundingBox` 的启发式现在只挡「非有限 / 超地球量级」的垃圾值,边界判定交给长度链。测试用「OBB 中心 == pageLOD 里 geode 的包围球中心」来证明是**读对位置**而不是蒙对了一个有限值。
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 四、S3M 1.x(SuperMapCache,`.s3m`)
|
|
61
|
+
|
|
62
|
+
四块真实瓦片(root / L16 / L17 / L18)交叉标定,长度链在链尾精确闭合。 —— **B 级**
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
byte[4] 's3m\0' + zlib
|
|
66
|
+
解压后:
|
|
67
|
+
uint32 xmlSize; byte[] XML; <补齐 4 字节>
|
|
68
|
+
uint32 geoSize; 几何流
|
|
69
|
+
uint32 4; uint32 0 // 固定小段,实测恒 0
|
|
70
|
+
uint32 texSize; 纹理流
|
|
71
|
+
|
|
72
|
+
几何流:uint32 meshCount,然后每块网格
|
|
73
|
+
uint32 geoName; uint32 7; uint32 772; uint32 V; uint32 0
|
|
74
|
+
V × 16B 顶点 (x, y, z, w),实测 w ≡ z
|
|
75
|
+
uint32[6] 数组头 [0, 0, 0, kind=1, count=V, flag=2]
|
|
76
|
+
V × 8B UV (u, v)
|
|
77
|
+
uint32[3] 索引头 [1, indexCount, 0x00040100]
|
|
78
|
+
indexCount × uint16 // 恒为 3 的倍数,最大 V-1
|
|
79
|
+
u32 1, u32 materialId // 与 XML 材质号一致
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
三条互相印证的不变量:`indexCount` 恒被 3 整除、最大索引恰为 `V-1`、逐 LOD 几何都落在 XML 的包围球内。
|
|
83
|
+
|
|
84
|
+
三个必须记住的坑:
|
|
85
|
+
|
|
86
|
+
1. **没有法线数组**(全段扫 `|v|≈1` 最长游程只有 4~10 个)→ 必须 `computeVertexNormals()`。
|
|
87
|
+
2. **头里的 `format=14` 不是 `S3MPixelFormat`**(那个枚举里 14 = BGRA = 4 B/px)。照搬就把 0.5 B/px 的 DXT1 当 4 字节读 → 满屏彩噪。按 `size / (w·h)` 反推。
|
|
88
|
+
3. **UV 的 `v=0` 对应文件首行**,与 DXT 解码内部那次 GL 方向的 Y 翻转方向相反 → 不抵消就正好采到图集空的那半张(区域均值亮度 5.6 vs 76.1),模型一片黑块。所以置 `flipY`。
|
|
89
|
+
|
|
90
|
+
另外两件事:
|
|
91
|
+
|
|
92
|
+
- **XML 里 `RangeDataList` 写的 `.osgb` 不参与寻址**。那只是切片时的原始文件名,服务实际发布的是**同名换 `.s3m` 后缀**,路径 `datas/<数据源>/data/path/<瓦片目录>/<名>.s3m`(L16/L17/L18 实测全部 200,`?sceneName=` 不参与)。所以整棵树是同一种容器,不需要 OSG Binary 解码器。
|
|
93
|
+
- **XML 后面那几个 `0x30` 是对齐填充**(2~3 个,取决于 XML 长度),不是固定标记。早期文档把它当成了 `byte[3] "000"`。
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 五、S3MBLOCK / texblock(纹理分包)
|
|
98
|
+
|
|
99
|
+
从 `3D-CBD/Xiaopin@CBD` 逐字节标定,并与官方 `@supermapgis/s3m-content-loader` 的 `S3MBlockBufferLoader` 交叉验证。 —— **B 级**
|
|
100
|
+
|
|
101
|
+
容器:
|
|
102
|
+
|
|
103
|
+
```
|
|
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 瓦片体 + 内嵌纹理图集)
|
|
110
|
+
```
|
|
111
|
+
|
|
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 一个像素都不用改。
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 六、MeshOpt 分支(v3.01 几何压缩)
|
|
122
|
+
|
|
123
|
+
规范里的 `CompressedDataPackage` 描述**已经落后于 v3.01 的实际数据**,布局从真实瓦片逐字节标定。 —— **B 级**
|
|
124
|
+
|
|
125
|
+
与官方 `S3M_JS` SDK 的两处分歧:
|
|
126
|
+
|
|
127
|
+
1. **官方 `parseMeshOptSkeleton` 在描述字符串后多推进 4 字节**,导致后续索引包全部错位(实测 v3.01 数据必然崩在 `parseMeshOpIndexPackage`)。按数据实际布局读。
|
|
128
|
+
2. **官方把「源字节长度」当 meshopt 的 filter 参数传进去**,等于不滤波。这里 normals 走八面体解码,UV / 位置按各自量化常量还原。
|
|
129
|
+
|
|
130
|
+
量化公式(用 OBB 反推验证过,误差 0):
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
position = minVerticesValue + uint16 * vertCompressConstant
|
|
134
|
+
texcoord = minTexCoordValue[i] + uint16 * texCoordCompressConstant[i]
|
|
135
|
+
normal = octDecode(int16 / 32767)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**顶点语义是位标记,不是枚举序号。** 自定义流(`CUSTOM0` / `CUSTOM1` / `GS_SCALE` / `GS_ROTATION`)原样保留解码结果,语义由数据自带的 `vertexAttributeDescript`(名字 → 槽位)给出。
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 七、Draco 分支(v3.01 点云压缩)
|
|
143
|
+
|
|
144
|
+
**规范里的 Draco 分支描述和实际数据对不上**,字段顺序是从真实瓦片逐字节标定(两个瓦片、5 个骨架完全一致,并用 `payloadSize` / `dracoSize` / `"DRACO"` magic / `skeletonStreamSize` 四重自校验)。 —— **C 级:样本只有两块瓦片。**
|
|
145
|
+
|
|
146
|
+
官方 `parseDracoSkeleton` 解析不了同一个服务:多了 `materialCount`,且 `version === 3` 的判定对 v3.01 不成立。
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
u32 payloadSize // 覆盖 [本字段之后 .. 条目末尾 OBB 之前]
|
|
150
|
+
u32 unknown = 32 // 实测恒 32,语义未知(D 级)
|
|
151
|
+
i32 posUniqueId // POSITION;-1 = 无
|
|
152
|
+
i32 normalUniqueId
|
|
153
|
+
i32 colorUniqueId
|
|
154
|
+
i32 secondColorUniqueId
|
|
155
|
+
u32 texCoordCount // 自定义属性个数,实测 3
|
|
156
|
+
i32 texCoordUniqueIds[texCoordCount]
|
|
157
|
+
u32 vertexAttrCount // 实测恒 0
|
|
158
|
+
u32 dracoSize
|
|
159
|
+
byte draco[dracoSize] // 标准 draco 比特流,前 5 字节 "DRACO"
|
|
160
|
+
pad4
|
|
161
|
+
String descript // {"PointCloudClassCode":2,"PointCloudIntensity":1,"VertexWeight":0}
|
|
162
|
+
pad4
|
|
163
|
+
f64 obb[12] // 由 parseSkeletonEntries 按条目末尾统一读
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`descript` 的 value 是**自定义属性在 `texCoordUniqueIds` 里的下标**,不是分量数:`VertexWeight → uid2`(float 高度)、`PointCloudIntensity → uid3`、`PointCloudClassCode → uid4`。**uid1 没被 descript 引用**(实测是 0~29 的 uint32,用途不明,D 级)。
|
|
167
|
+
|
|
168
|
+
回归测试:`tests/unit/draco.test.ts`,fixture 是 `pointcloud.s3mb`(单骨架 6149 点)和 `pointcloud-multi.s3mb`(4 骨架 —— 多骨架才是藏 bug 的地方)。断言 `bytesRead === bytesTotal`、属性清单、自定义映射。**改字段顺序这几条必红。**
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## 八、纹理
|
|
173
|
+
|
|
174
|
+
### 8.1 「是不是 CRN」只能看 `compressType`
|
|
175
|
+
|
|
176
|
+
纹理头里两个字段特别容易搞混 —— **A 级,实测三组组合**:
|
|
177
|
+
|
|
178
|
+
| 版本 | `compressType` | `pixelFormat` | 实际 |
|
|
179
|
+
|:---|:---|:---|:---|
|
|
180
|
+
| v3.00 | 26 | 13(ABGR,无意义) | CRN,数据以 `Hx` 开头 |
|
|
181
|
+
| v3.00 | 14 | 21 | DXT5 裸块 |
|
|
182
|
+
| v3.01 | 50000 | 32856(GL_RGBA8,也不是 DXT) | CRN,数据以 `Hx` 开头 |
|
|
183
|
+
|
|
184
|
+
- v3.00 的 `compressType` **直接借用了 PixelFormat 的数值**(26/27 = CRN)。
|
|
185
|
+
- v3.01 改成了 GL 压缩枚举:33776 DXT1、33779 DXT5、38000 WEBP、38001 CRN、38002 KTX2,外加自定义的 50000(CRN_DXT5)。
|
|
186
|
+
|
|
187
|
+
**按 `pixelFormat` 26/27 判断 CRN 的话,真实服务里的 CRN 纹理会被当成未知格式整包丢掉,模型变成无贴图灰模。** 这个坑踩过一次。
|
|
188
|
+
|
|
189
|
+
### 8.2 CRN
|
|
190
|
+
|
|
191
|
+
`pixelFormat` 26/27 是 crunch 的熵编码容器,不是裸 DXT 块。必须先转码回 DXT 才能上传 —— 用内联的 `crunch_decoder` wasm,只取第 0 层(其余交给 three 生成),然后交回现有 DXT 链路。
|
|
192
|
+
|
|
193
|
+
iServer 在「纹理压缩 = CRN_DXT5」时会把整包纹理都压成这个格式,**不是边角情况**。实测跑过:`3D-BianDianZhan3`(512², v3.00)、`3D-BIMTreeModel` / `3D-local3DCache-SubwayStationCombined`(32²/64², v3.01)、`3D-scyx`(1024²)。
|
|
194
|
+
|
|
195
|
+
### 8.3 DXT 与 flipY
|
|
196
|
+
|
|
197
|
+
DXT 块走 `CompressedTexture` 直传,GPU 硬件解压(需要 `WEBGL_compressed_texture_s3tc` 与 `s3tc_srgb` **两个**扩展;缺 srgb 变体会让 SRGBColorSpace 的贴图发白)。移动端大多没有 s3tc,回退 Worker 解成 RGBA。
|
|
198
|
+
|
|
199
|
+
压缩纹理不能 `generateMipmaps`,得用数据自带的 mip 或用 2/3 采样公式。S3M 1.x 的 UV 需要注意 8.3 之外的那次 Y 翻转(见第四节)。
|
|
200
|
+
|
|
201
|
+
### 8.4 KTX2 / Basis —— 不是「不支持」,是基本不会出现
|
|
202
|
+
|
|
203
|
+
官方 `S3MPixelFormat` 枚举里没有 KTX2 / Basis 条目,iServer 也不会发。真要支持得引入 `basis_transcoder.wasm`,但没有数据样本可测。
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## 九、3DGS 泼溅参数(已标定,A 级)
|
|
208
|
+
|
|
209
|
+
**曾经是本文档最大的未解项,现在解了。** 结论不是靠统计猜的,是从官方 worker
|
|
210
|
+
`SuperMap3D/Workers/GaussianSplating.js`(3.9 KB)里逐字读出来的:
|
|
211
|
+
|
|
212
|
+
```js
|
|
213
|
+
function j(t){ // scale
|
|
214
|
+
a[n*3+0] = Math.exp(t[n*4+0]/16 - 10)
|
|
215
|
+
a[n*3+1] = Math.exp(t[n*4+1]/16 - 10)
|
|
216
|
+
a[n*3+2] = Math.exp(t[n*4+2]/16 - 10)
|
|
217
|
+
}
|
|
218
|
+
function D(t){ // rotation
|
|
219
|
+
c = t[n]/127.5 - 1; f = t[n+1]/127.5 - 1; x = t[n+2]/127.5 - 1
|
|
220
|
+
s = Math.sqrt(Math.max(0, 1 - (c*c + f*f + x*x)))
|
|
221
|
+
a[n] = s; a[n+1] = c; a[n+2] = f; a[n+3] = x // (w, x, y, z)
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
| 流 | 编码 | 语义 |
|
|
226
|
+
|:---|:---|:---|
|
|
227
|
+
| `gsScale` (4 B) | 3 个 u8 | `exp(byte/16 - 10)` 米。值域 [4.5e-5, 380] 米 |
|
|
228
|
+
| `gsRotation` (4 B) | 3 个 u8 | `(b/127.5 - 1)` 得 x/y/z;w 由单位化补出 |
|
|
229
|
+
| 两条流的第 4 字节 | —— | **填充,官方完全不用** |
|
|
230
|
+
|
|
231
|
+
**为什么当初看它像乱码**:第 4 字节确实不参与解码,于是同一瓦片里它一会儿是 0、
|
|
232
|
+
一会儿是 207(填充残留)。把它当 32 位整数或第 4 个分量去套,怎么都对不上。
|
|
233
|
+
只取前 3 字节、w 由单位化补出,立刻就是干净的四元数。
|
|
234
|
+
|
|
235
|
+
两条判定分支,靠数据自己区分:
|
|
236
|
+
|
|
237
|
+
| 条件 | 分支 | 公式 |
|
|
238
|
+
|:---|:---|:---|
|
|
239
|
+
| `isSPZCompress && version === 1.1` | smallest-three | 10-10-10-2 打包,最大维由单位化补 |
|
|
240
|
+
| 其余(**当前线上数据都是这条**) | `D()` | 3 个 u8 直接归一化 |
|
|
241
|
+
|
|
242
|
+
**判据是四元数模长残差**:`D()` 分支下 `|q|²` 等于 1 到 float32 精度(实测 2.2e-16,
|
|
243
|
+
个别 8e-3 是 u8 量化本身的误差);把同一份数据按 smallest-three 解,残差会到 1e-1 量级。
|
|
244
|
+
实现里两条都写了(`decodeRotation` / `decodeRotationSmallestThree`),测试用残差钉住选择。
|
|
245
|
+
|
|
246
|
+
落点:`core/GaussianSplat.ts`(解码 + 协方差)、`render/SplatMaterial.ts`(实例化四边形)、
|
|
247
|
+
`GeometryBuilder` 里「有 gsScale/gsRotation 就画泼溅,否则退回 Points」的分流。
|
|
248
|
+
|
|
249
|
+
> **降级只在数据不自洽时才报**:声明是 3DGS、骨架里却没有这两条流。参数完整的数据
|
|
250
|
+
> 走正常泼溅路径,不再产生 `gaussian-splatting-points`。机制本身(`stats.degradations`
|
|
251
|
+
> / `unsupported`)保留,见 `API.md#降级上报degradations`。
|
|
252
|
+
|
|
253
|
+
## 九之二、仍未标定的字段(D 级)
|
|
254
|
+
|
|
255
|
+
**保留原始字节,不猜语义。**
|
|
256
|
+
|
|
257
|
+
| 字段 | 现状 | 影响 |
|
|
258
|
+
|:---|:---|:---|
|
|
259
|
+
| Draco `uid1` | 实测 0~29 的 uint32,未被 `descript` 引用 | 未知,跳过不影响解码 |
|
|
260
|
+
| Draco `unknown = 32` | 实测恒 32 | 未知,跳过不影响解码 |
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## 十、复验手册
|
|
265
|
+
|
|
266
|
+
### 10.1 上游一变,按这个顺序查
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
# 1. 单元测试:真实瓦片 fixture,不需要网络,最快
|
|
270
|
+
NODE_OPTIONS= bun test ./tests/unit/parser.test.ts ./tests/unit/draco.test.ts \
|
|
271
|
+
./tests/unit/gaussian-splatting.test.ts ./tests/unit/s3mblock.test.ts
|
|
272
|
+
|
|
273
|
+
# 2. 60 个真实瓦片的 MeshOpt 结构校验(需要网络,走 SuperMap Online 公有服务)
|
|
274
|
+
node tools/meshopt-check.mjs --preset shenyin
|
|
275
|
+
|
|
276
|
+
# 3. 官方解析器当 oracle 对账(同一份样例,逐段比较)
|
|
277
|
+
node tools/official-oracle.mjs
|
|
278
|
+
|
|
279
|
+
# 4. 逐字段追踪单个瓦片,定位第一个对不上的偏移
|
|
280
|
+
node tools/trace-one.mjs <tile.s3mb>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### 10.2 七条关键不变量
|
|
284
|
+
|
|
285
|
+
解析坏了,先看这几条哪条不成立——它们把「读错位」和「数据本身有问题」区分开:
|
|
286
|
+
|
|
287
|
+
1. **`bytesRead === bytesTotal`** —— 整块流不多不少吃完。字段顺序读错,这里第一个炸。
|
|
288
|
+
2. **索引数量恒被 3 整除,最大索引 = `V-1`** —— 几何段边界读对了吗。
|
|
289
|
+
3. **每块骨架的 OBB 中心 == `pageLOD` 里 geode 的包围球中心** —— 证明读对了位置,而不是蒙对了一个有限值。
|
|
290
|
+
4. **分块边界链**:`skeletonStreamSize` 逐块累加严丝合缝落到条目末尾(实测 `1660+4+25396=27060`、`27060+4+32204=59268`、`59268+4+76252=135524`)。
|
|
291
|
+
5. **量化还原后的顶点包围盒 == OBB extent**(`meshopt-check.mjs` 逐项校验)。
|
|
292
|
+
6. **S3M 1.x 的三段长度链落到解压末尾**,`0x30` 填充数 = `(4 - xmlSize % 4) % 4`。
|
|
293
|
+
7. **纹理尺寸 == 头里的 `w × h`**,且 `size / (w·h)` 推出的字节数 = 格式的 B/px。
|
|
294
|
+
|
|
295
|
+
### 10.3 加新格式的规矩
|
|
296
|
+
|
|
297
|
+
1. **先拿真实瓦片**,至少要两个(一个根、一个细化子瓦)。只有一个样本的结论是 C 级,不要往文档里写成 B。
|
|
298
|
+
2. **找内部不变量**。长度链闭合、几何与包围球一致、属性个数与 descript 对得上 —— 有闭合才算标定完成,否则只是「能跑」。
|
|
299
|
+
3. **和官方混淆产物对账**(`official-oracle.mjs`)。官方错的地方也记下来,那是下次快速识别的指纹。
|
|
300
|
+
4. **先写测试再写实现**(见 `AGENTS.md`)。测试要断言不变量,不要复述实现。
|
|
301
|
+
5. **不确定的字段进第九节**,标注 D 级,保留原始字节。
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## 十一、样本清单
|
|
306
|
+
|
|
307
|
+
仓库内的 fixture(离线,单测用):
|
|
308
|
+
|
|
309
|
+
| 路径 | 大小 | 覆盖 | 对应的坑 |
|
|
310
|
+
|:---|:---|:---|:---|
|
|
311
|
+
| `tests/fixtures/draco/pointcloud.s3mb` | — | 单骨架 6149 点 | Draco 容器字段顺序 |
|
|
312
|
+
| `tests/fixtures/draco/pointcloud-multi.s3mb` | 96K(两块合计) | 4 骨架 | 多骨架边界、自定义映射 |
|
|
313
|
+
| `tests/fixtures/gaussian-splatting/Tile_0019…` | 184K(两块合计) | 3DGS 根瓦片(单骨架 1888 点) | `rangeMode=2`、`childTile` 路径 |
|
|
314
|
+
| `tests/fixtures/gaussian-splatting/Tile_0020…` | ↑ | 3DGS 子瓦(4 骨架 9448 点) | **+4 字节填充错位**、无索引包按 Points 出图 |
|
|
315
|
+
| `tests/fixtures/s3mblock/` | 936K | S3MBLOCK + 内嵌图集 | 图集 UV 重映射 |
|
|
316
|
+
| `tests/fixtures/osgb/` | 68K | S3M 1.x (`.s3m`) | 无后法线、`format=14`、UV flipY |
|
|
317
|
+
|
|
318
|
+
外部样例(需要网络,CI 里 `S3M_REQUIRE_SAMPLES=1` 强制执行,不设就 skip —— 静默 skip 比失败更危险):
|
|
319
|
+
|
|
320
|
+
- `../s3m-spec/S3M_SDK/S3M_JS/data/` —— 官方 9 个样例(v1 / v2 / v3)
|
|
321
|
+
- SuperMap Online 公有服务 `3D-ShouNingGe`(3DGS)、`3D-CBD/Xiaopin@CBD`(S3MBLOCK)、`3D-scyx` / `3D-BianDianZhan3`(CRN)
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## 十二、和官方 SDK 的分歧清单
|
|
326
|
+
|
|
327
|
+
按「官方错、我们对」记录。下次遇到新数据先查这张表,很可能又是同一类问题。
|
|
328
|
+
|
|
329
|
+
| 位置 | 官方实现 | 实际数据 | 后果 |
|
|
330
|
+
|:---|:---|:---|:---|
|
|
331
|
+
| `parseMeshOptSkeleton` | 描述字符串后多推进 4 字节 | 不推进 | 后续索引包全错位 |
|
|
332
|
+
| MeshOpt filter 参数 | 传源字节长度 | 应按各自量化常量 | 等于不滤波,精度丢失 |
|
|
333
|
+
| `parseDracoSkeleton` | 多读 `materialCount`;`version === 3` 判定 | 无此字段;v3.01 不满足 | 官方解析不了自己的 v3.01 点云服务 |
|
|
334
|
+
| v3 非压缩分支 | 整个 ArrayBuffer 当数据流 | 要从文件头之后开始 | 官方 bug |
|
|
335
|
+
| S3M 1.x 几何头 `format` | 当 `S3MPixelFormat` 用 | 是另外一套编码 | 把 DXT1 当 BGRA 读,满屏彩噪 |
|
|
336
|
+
| `PageLod.calcPixFromCam` | —— | **这条是官方对的,我们照抄** | 见下 |
|
|
337
|
+
|
|
338
|
+
> `calcPixFromCam` 的公式里有两个容易漏的细节:视场角是**写死的 45°**(`tan(22.5°)`),不取渲染器实际 fov;末尾那个 `*2`(用直径量屏幕尺寸)不能少。少这个 `*2` 会让 metric 只有官方的一半,`needRefine` 门槛永远差一级——表现是「贴着看也一直在渲染最粗的根图集,一张整楼雪碧被 uv 平铺几十遍」。实测瓦片 `6_01`:radius 293.4、rangeValue 586.8、distance ≈ 824 m、H = 854 → 官方算 734 > 586.8 该细化,旧公式只有 326。
|
package/package.json
CHANGED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 3DGS(高斯泼溅)逐点属性的解码
|
|
3
|
+
*
|
|
4
|
+
* 输入是 MeshOpt 骨架里的两条自定义流,各 4 字节/顶点:
|
|
5
|
+
* `gsScale` —— 3 个 u8,log 量化;第 4 字节是填充
|
|
6
|
+
* `gsRotation` —— 3 个 u8 存四元数的 x/y/z;w 由公式补;第 4 字节是填充
|
|
7
|
+
*
|
|
8
|
+
* 这两条流以前是「保留原始字节但语义未知」(FORMAT-NOTES 第九节的 D 级),
|
|
9
|
+
* 所以只能按点渲染。量化格式最终不是靠统计猜出来的,是从官方 worker
|
|
10
|
+
* `SuperMap3D/Workers/GaussianSplating.js`(3.9 KB)里读出来的,公式逐字如下:
|
|
11
|
+
*
|
|
12
|
+
* ```js
|
|
13
|
+
* function j(t){ // scale
|
|
14
|
+
* a[n*3+0] = Math.exp(t[n*4+0]/16 - 10)
|
|
15
|
+
* a[n*3+1] = Math.exp(t[n*4+1]/16 - 10)
|
|
16
|
+
* a[n*3+2] = Math.exp(t[n*4+2]/16 - 10)
|
|
17
|
+
* }
|
|
18
|
+
* function D(t){ // rotation
|
|
19
|
+
* c = t[n]/127.5 - 1; f = t[n+1]/127.5 - 1; x = t[n+2]/127.5 - 1
|
|
20
|
+
* s = Math.sqrt(Math.max(0, 1 - (c*c + f*f + x*x)))
|
|
21
|
+
* a[n] = s; a[n+1] = c; a[n+2] = f; a[n+3] = x // (w, x, y, z)
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* 官方还有第二条分支(`isSPZCompress && version === 1.1`)走 10-10-10-2 的
|
|
26
|
+
* 「smallest three」打包,本模块也实现了 —— 见 `decodeRotation` 的 mode 判定。
|
|
27
|
+
*
|
|
28
|
+
* **为什么以前看这 4 字节像乱码**:第 4 字节确实不参与解码,于是同一瓦片里它
|
|
29
|
+
* 一会儿是 0 一会儿是 207(填充的残留),拿 32 位整数字段或 4 分量向量去套,
|
|
30
|
+
* 自然怎么都对不上。只取前 3 字节 + w 由单位化补出,立刻就是干净的四元数。
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** SPZ 的 log 量化:scale 实际是 exp(v/16 - 10) 米 */
|
|
34
|
+
const SCALE_LOG_DIV = 16;
|
|
35
|
+
const SCALE_LOG_BIAS = 10;
|
|
36
|
+
|
|
37
|
+
/** rotation 的 u8 归一化:0 → -1,255 → 1 */
|
|
38
|
+
const ROT_U8_SCALE = 127.5;
|
|
39
|
+
|
|
40
|
+
export interface SplatData {
|
|
41
|
+
/** 3N(米,场景 ENU 坐标) */
|
|
42
|
+
positions: Float32Array;
|
|
43
|
+
/** 3N(米,三个主轴的高斯半径) */
|
|
44
|
+
scales: Float32Array;
|
|
45
|
+
/** 4N,单位四元数 (x, y, z, w) —— three 的 Quaternion 顺序 */
|
|
46
|
+
rotations: Float32Array;
|
|
47
|
+
/** 4N,RGBA8 */
|
|
48
|
+
colors: Uint8Array;
|
|
49
|
+
count: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* scale:`exp(byte/16 - 10)` 米。
|
|
54
|
+
* 值域 byte ∈ [0,255] → [4.54e-5, 380] 米,建筑泼溅的典型值(0.01~1 米)落在中段。
|
|
55
|
+
*/
|
|
56
|
+
export function decodeScale(bytes: Uint8Array, stride: number, offset: number, count: number): Float32Array {
|
|
57
|
+
const out = new Float32Array(count * 3);
|
|
58
|
+
for (let i = 0; i < count; i++) {
|
|
59
|
+
const o = i * stride + offset;
|
|
60
|
+
out[i * 3] = Math.exp(bytes[o]! / SCALE_LOG_DIV - SCALE_LOG_BIAS);
|
|
61
|
+
out[i * 3 + 1] = Math.exp(bytes[o + 1]! / SCALE_LOG_DIV - SCALE_LOG_BIAS);
|
|
62
|
+
out[i * 3 + 2] = Math.exp(bytes[o + 2]! / SCALE_LOG_DIV - SCALE_LOG_BIAS);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* rotation:3 个 u8 直接归一化到 [-1,1],第四个分量由单位化补出。
|
|
69
|
+
*
|
|
70
|
+
* 输出顺序是 three 的 `(x, y, z, w)`,而官方内部是 `(w, x, y, z)` —— 这里已经换过。
|
|
71
|
+
*/
|
|
72
|
+
export function decodeRotation(bytes: Uint8Array, stride: number, offset: number, count: number): Float32Array {
|
|
73
|
+
const out = new Float32Array(count * 4);
|
|
74
|
+
let maxErr = 0;
|
|
75
|
+
for (let i = 0; i < count; i++) {
|
|
76
|
+
const o = i * stride + offset;
|
|
77
|
+
const x = bytes[o]! / ROT_U8_SCALE - 1;
|
|
78
|
+
const y = bytes[o + 1]! / ROT_U8_SCALE - 1;
|
|
79
|
+
const z = bytes[o + 2]! / ROT_U8_SCALE - 1;
|
|
80
|
+
const w = Math.sqrt(Math.max(0, 1 - (x * x + y * y + z * z)));
|
|
81
|
+
out[i * 4] = x;
|
|
82
|
+
out[i * 4 + 1] = y;
|
|
83
|
+
out[i * 4 + 2] = z;
|
|
84
|
+
out[i * 4 + 3] = w;
|
|
85
|
+
maxErr = Math.max(maxErr, Math.abs(x * x + y * y + z * z + w * w - 1));
|
|
86
|
+
}
|
|
87
|
+
// 模长偏差只可能在「四元数确实不是这套编码」时出现,留给调用方判断
|
|
88
|
+
rotationResidual = maxErr;
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 上一次 `decodeRotation` 的单位化残差(|q|²-1 的最大绝对值)。
|
|
94
|
+
*
|
|
95
|
+
* 这是判定「编码分支猜对没有」的判据:`D()` 分支下 w 是补出来的,残差恒 ≈ 0;
|
|
96
|
+
* 如果数据其实是 smallest-three 打包,把前 3 字节当分量解出来的 |q|² 会明显偏离 1。
|
|
97
|
+
*/
|
|
98
|
+
let rotationResidual = 0;
|
|
99
|
+
|
|
100
|
+
export function lastRotationResidual(): number {
|
|
101
|
+
return rotationResidual;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 10-10-10-2 的 smallest-three(官方 `U()`,`isSPZCompress && version === 1.1` 时走这条)。
|
|
106
|
+
*
|
|
107
|
+
* 3 个分量各 10 bit(第 9 bit 是符号),剩下 2 bit 记录「哪一维是最大的」,
|
|
108
|
+
* 最大维由单位化补出 —— 这样只需要 32 bit 就能无损表达一个单位四元数。
|
|
109
|
+
*/
|
|
110
|
+
export function decodeRotationSmallestThree(
|
|
111
|
+
bytes: Uint8Array,
|
|
112
|
+
stride: number,
|
|
113
|
+
offset: number,
|
|
114
|
+
count: number
|
|
115
|
+
): Float32Array {
|
|
116
|
+
const out = new Float32Array(count * 4);
|
|
117
|
+
const MASK = (1 << 9) - 1;
|
|
118
|
+
const SQRT_HALF = Math.SQRT1_2;
|
|
119
|
+
for (let i = 0; i < count; i++) {
|
|
120
|
+
const o = i * stride + offset;
|
|
121
|
+
const packed =
|
|
122
|
+
(bytes[o]! | (bytes[o + 1]! << 8) | (bytes[o + 2]! << 16) | (bytes[o + 3]! << 24)) >>> 0;
|
|
123
|
+
const largest = (packed >>> 30) & 3;
|
|
124
|
+
let rest = packed;
|
|
125
|
+
let sumSq = 0;
|
|
126
|
+
for (let axis = 3; axis >= 0; axis--) {
|
|
127
|
+
if (axis === largest) continue;
|
|
128
|
+
const mag = rest & MASK;
|
|
129
|
+
const negative = (rest >>> 9) & 1;
|
|
130
|
+
rest >>>= 10;
|
|
131
|
+
// 值域 [-√½, √½]:单位四元数里任一分量的上界
|
|
132
|
+
let v = (SQRT_HALF * mag) / MASK;
|
|
133
|
+
if (negative) v = -v;
|
|
134
|
+
out[i * 4 + axis] = v;
|
|
135
|
+
sumSq += v * v;
|
|
136
|
+
}
|
|
137
|
+
out[i * 4 + largest] = Math.sqrt(Math.max(1 - sumSq, 0));
|
|
138
|
+
}
|
|
139
|
+
rotationResidual = 0;
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 逐点协方差矩阵的上三角(6 个 float):`Σ = R S Sᵀ Rᵀ`,供 EWA 投影用 */
|
|
144
|
+
export function covariance(
|
|
145
|
+
sx: number,
|
|
146
|
+
sy: number,
|
|
147
|
+
sz: number,
|
|
148
|
+
qx: number,
|
|
149
|
+
qy: number,
|
|
150
|
+
qz: number,
|
|
151
|
+
qw: number,
|
|
152
|
+
out: Float32Array,
|
|
153
|
+
at: number
|
|
154
|
+
): void {
|
|
155
|
+
// 旋转矩阵(列主序 3×3)
|
|
156
|
+
const xx = qx * qx, yy = qy * qy, zz = qz * qz;
|
|
157
|
+
const xy = qx * qy, xz = qx * qz, yz = qy * qz;
|
|
158
|
+
const wx = qw * qx, wy = qw * qy, wz = qw * qz;
|
|
159
|
+
|
|
160
|
+
const r00 = 1 - 2 * (yy + zz), r01 = 2 * (xy - wz), r02 = 2 * (xz + wy);
|
|
161
|
+
const r10 = 2 * (xy + wz), r11 = 1 - 2 * (xx + zz), r12 = 2 * (yz - wx);
|
|
162
|
+
const r20 = 2 * (xz - wy), r21 = 2 * (yz + wx), r22 = 1 - 2 * (xx + yy);
|
|
163
|
+
|
|
164
|
+
// M = R · diag(s),则 Σ = M Mᵀ
|
|
165
|
+
const m00 = r00 * sx, m01 = r01 * sy, m02 = r02 * sz;
|
|
166
|
+
const m10 = r10 * sx, m11 = r11 * sy, m12 = r12 * sz;
|
|
167
|
+
const m20 = r20 * sx, m21 = r21 * sy, m22 = r22 * sz;
|
|
168
|
+
|
|
169
|
+
out[at] = m00 * m00 + m01 * m01 + m02 * m02;
|
|
170
|
+
out[at + 1] = m00 * m10 + m01 * m11 + m02 * m12;
|
|
171
|
+
out[at + 2] = m00 * m20 + m01 * m21 + m02 * m22;
|
|
172
|
+
out[at + 3] = m10 * m10 + m11 * m11 + m12 * m12;
|
|
173
|
+
out[at + 4] = m10 * m20 + m11 * m21 + m12 * m22;
|
|
174
|
+
out[at + 5] = m20 * m20 + m21 * m21 + m22 * m22;
|
|
175
|
+
}
|