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/src/core/S3M1Parser.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 结构(root / L16 / L17 / L18 四块真实瓦片交叉标定,链尾全部精确闭合):
|
|
8
8
|
* ```
|
|
9
|
-
* byte[4] 's3m
|
|
9
|
+
* byte[4] 's3m' + 版本字节(0 = 老导出,1 = iServer 2025 起)+ zlib
|
|
10
10
|
* 解压后:
|
|
11
11
|
* uint32 xmlSize; byte[] XML; <补齐 4 字节对齐>
|
|
12
12
|
* uint32 geoSize; 几何流
|
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
*
|
|
16
16
|
* 几何流: uint32 meshCount,然后每块网格
|
|
17
17
|
* uint32 geoName; uint32 7; uint32 772; uint32 V; uint32 0
|
|
18
|
-
* V × 16B 顶点 (x, y, z, w),实测 w 恒等于 z
|
|
19
|
-
*
|
|
18
|
+
* V × 16B 顶点 (x, y, z, w),实测 w 恒等于 z ← 版本 0
|
|
19
|
+
* V × 12B 顶点 (x, y, z) ← 版本 1(部分数据)
|
|
20
|
+
* uint32[6] 数组头 [0,0,0,kind=1,count=V,flag=2] ← 版本 0
|
|
20
21
|
* V × 8B UV (u, v)
|
|
21
22
|
* uint32[3] 索引头 [1, indexCount, 0x00040100]
|
|
22
23
|
* indexCount × uint16 // indexCount 恒为 3 的倍数,值域最大 V-1
|
|
@@ -28,6 +29,35 @@
|
|
|
28
29
|
* byte[size] // 实测 format=14、0.5B/px,即 DXT1
|
|
29
30
|
* ```
|
|
30
31
|
*
|
|
32
|
+
* **版本 0 与版本 1 的几何流布局不同**(2026-10-28 用 SuperMap Online 的
|
|
33
|
+
* `3D-tishuju`(体数据)与 `3D-Histogram`(柱状图)两组真实瓦片标定):
|
|
34
|
+
* - 版本 0:顶点恒 vec4(16B),顶点块之后一定是上面那套 UV 头。
|
|
35
|
+
* - 版本 1:顶点有 vec3(12B) / vec4(16B) 两种;顶点块之后是若干「属性块」,
|
|
36
|
+
* 块头里带 count=V,但块数(3~4)与行宽逐网格不同,UV / 法线 / 颜色的顺序
|
|
37
|
+
* 没有稳定规律可循(同一文件里 4 个网格的记录长度都不一样)。
|
|
38
|
+
* 所以版本 1 只取顶点与索引,中间那一段属性块整段跳过 —— 实测这两类数据的
|
|
39
|
+
* 材质都没有纹理(texSize 只有 count=0),跳过不影响画面。
|
|
40
|
+
* 索引段带固定标记 `[1, indexCount, 0x00040100]`,跳过就靠它定位。
|
|
41
|
+
*
|
|
42
|
+
* 版本 1 的顶点还可能是**归一化模板**(柱状图:x/z 落在 [-0.5,0.5]、y 落在 [0,1],
|
|
43
|
+
* 即一根菱形截面柱),真实位置与尺寸只写在记录尾的 `[1][8][minx…maxz]` 量化盒里。
|
|
44
|
+
* 不展开的话所有柱子都叠在原点 1 米见方 —— 画面上等于「什么都没加载出来」。
|
|
45
|
+
* 展开后每块网格的包围盒与 XML 的 BoundingSphere 完全重合(4/4,误差 0.00m)。
|
|
46
|
+
*
|
|
47
|
+
* ★ 柱状图真正的几何是**实例化**的(对应规范里 VertexDataPackage 的 InstanceInfo /
|
|
48
|
+
* InstanceObject):瓦片里只有一份「单位盒模板」,每根柱是一条 20 float 记录:
|
|
49
|
+
* [0..11] 3×4 行主序仿射矩阵(缩放≈45 / 平移=柱底中心)
|
|
50
|
+
* [12..15] RGBA —— 官方截图里黄/绿/红的来源
|
|
51
|
+
* [16..19] 预留(值与权重之类,本库暂不用)
|
|
52
|
+
* 实测一块 250×50m 的瓦片里 11 条记录、缩放 45×45×38~44,柱间距 50m,与官方渲染一致。
|
|
53
|
+
* 实例数组紧贴记录尾的量化盒([1][8]+6 float),从盒往前按 80 字节回溯就能整段取到;
|
|
54
|
+
* 块头到实例数组之间还夹着长度不定的零区,所以**不能**正向扫固定偏移。
|
|
55
|
+
*
|
|
56
|
+
* ★ S3M 1.x 容器**没有 pickInfo 段**,而拾取链路(Picker → _instanceFeature)全靠
|
|
57
|
+
* pickInfo.instanceIds 把实例序号翻译成 SMID,缺了它就是「看得见、点不中」。
|
|
58
|
+
* 所以实例化网格会合成一份:控制位 = 实例序号,SMID = geoName × 1e6 + 序号。
|
|
59
|
+
* 非实例化网格不需要(拾取退化成「整块网格」,本来就能选中)。
|
|
60
|
+
*
|
|
31
61
|
* 几何里**没有法线数组**,交给 GeometryBuilder 在绑定索引后 computeVertexNormals()。
|
|
32
62
|
*/
|
|
33
63
|
import { BinaryReader } from './BinaryReader.js';
|
|
@@ -37,22 +67,35 @@ import type {
|
|
|
37
67
|
IndexPackage,
|
|
38
68
|
PageLOD,
|
|
39
69
|
ParsedTile,
|
|
70
|
+
PickEntry,
|
|
40
71
|
S3MTexture,
|
|
41
72
|
VertexAttribute,
|
|
42
73
|
VertexPackage,
|
|
43
74
|
} from './types.js';
|
|
44
75
|
|
|
45
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* S3M 1.x 魔数:ASCII "s3m" + 1 字节版本号。
|
|
78
|
+
*
|
|
79
|
+
* 第 4 字节**不是常量 0**:老导出写 0x00,iServer 2025 写 0x01。原先逐字比对
|
|
80
|
+
* `s3m\0`,版本 1 的瓦片就落到 2.x/3.x 分支,4 字节魔数被当 float32 版本号
|
|
81
|
+
* (≈1.4e-38),紧跟的 zlib 头被当压缩长度,最终抛
|
|
82
|
+
* `RangeError: Invalid typed array length: 1424349816` —— 完全看不出根因。
|
|
83
|
+
* 版本 2 起结构可能再变,宁可让它在 2.x 分支明确报错,也不要猜着解。
|
|
84
|
+
*/
|
|
46
85
|
const S3M_V1_MAGIC = 0x006d3373;
|
|
86
|
+
const S3M_V1_MAGIC_MASK = 0x00ffffff;
|
|
87
|
+
const S3M_V1_MAX_VERSION = 1;
|
|
47
88
|
|
|
48
89
|
export interface ParseS3M1Options {
|
|
49
90
|
inflate?: (bytes: Uint8Array) => Uint8Array | Promise<Uint8Array>;
|
|
50
91
|
}
|
|
51
92
|
|
|
52
|
-
/** 是不是 S3M 1.x
|
|
93
|
+
/** 是不是 S3M 1.x 容器(版本字节 0 或 1) */
|
|
53
94
|
export function isS3M1Buffer(buffer: ArrayBuffer): boolean {
|
|
54
95
|
if (buffer.byteLength < 8) return false;
|
|
55
|
-
|
|
96
|
+
const v = new DataView(buffer, 0, 8).getUint32(0, true);
|
|
97
|
+
if ((v & S3M_V1_MAGIC_MASK) !== S3M_V1_MAGIC) return false;
|
|
98
|
+
return v >>> 24 <= S3M_V1_MAX_VERSION;
|
|
56
99
|
}
|
|
57
100
|
|
|
58
101
|
async function defaultInflate(bytes: Uint8Array): Promise<Uint8Array> {
|
|
@@ -184,12 +227,177 @@ interface MeshRecord {
|
|
|
184
227
|
indexCount: number;
|
|
185
228
|
indices: Uint16Array;
|
|
186
229
|
materialId: string;
|
|
230
|
+
/** 实例矩阵(每实例 16 float:3×4 行主序 + RGBA),没有实例化时为 undefined */
|
|
231
|
+
instances?: Float32Array;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** 索引头的第 3 个 dword,全版本固定 */
|
|
235
|
+
const INDEX_HEADER_MARK = 0x00040100;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* v1 里每个实例记录固定 20 个 float(80 字节):
|
|
239
|
+
* [0..11] 3×4 行主序仿射矩阵(第 4 行隐式 0,0,0,1)
|
|
240
|
+
* [12..15] RGBA —— 实测就是柱子的配色,官方截图里那些黄/绿/红
|
|
241
|
+
* [16..19] 预留(值/权重之类,本库暂时不用)
|
|
242
|
+
* 取前 16 个交给 GeometryBuilder,正好对上它认的 stride=16(矩阵 + 颜色)。
|
|
243
|
+
*/
|
|
244
|
+
const V1_INSTANCE_FLOATS = 20;
|
|
245
|
+
const V1_INSTANCE_BYTES = V1_INSTANCE_FLOATS * 4;
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 合成 SMID 时给每个 geoName 留的实例位宽。
|
|
249
|
+
*
|
|
250
|
+
* geoName 是 uint32 哈希,乘 1e6 后最大约 4.29e15,仍在 Number 安全整数范围内
|
|
251
|
+
* (9.007e15),所以「geoName × 1e6 + 实例序号」可以当作全局唯一键。
|
|
252
|
+
* S3M 1.x 容器不带单体化信息,SMID 只能这样合成——它只用于本库内部的拾取/描边,
|
|
253
|
+
* 和 iServer 侧的 SMID 没有对应关系。
|
|
254
|
+
*/
|
|
255
|
+
const S3M1_SMID_STRIDE = 1_000_000;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 解析实例化数组。
|
|
259
|
+
*
|
|
260
|
+
* 地块级数据(柱状图)不是「一堆柱子的顶点」,而是**一份模板几何 + N 个变换矩阵**:
|
|
261
|
+
* 顶点是单位盒(x/z ∈ [-0.5,0.5]、y ∈ [0,1]),真实位置/尺寸/颜色全在实例矩阵里。
|
|
262
|
+
* 块内布局(实测 5 个网格交叉验证,N 全是整数):
|
|
263
|
+
* [V][2] 块头 | V×8 字节(恒 0) | [1][20] 头 | N×80 实例 | [1][8] + 6 float 量化盒
|
|
264
|
+
*/
|
|
265
|
+
function readInstances(
|
|
266
|
+
view: DataView,
|
|
267
|
+
from: number,
|
|
268
|
+
to: number
|
|
269
|
+
): { count: number; floats: Float32Array } | null {
|
|
270
|
+
// 实例数组紧贴量化盒([1][8] + 6 float)。从它往前按 80 字节回溯,
|
|
271
|
+
// 一旦遇到不像矩阵的记录就停 —— 块头到实例数组之间还夹着长度不定的零区,
|
|
272
|
+
// 正向扫「[1][20] 头」只在单实例的网格上成立(多实例的网格压根没这个头)。
|
|
273
|
+
let aabb = -1;
|
|
274
|
+
for (let p = to - 12; p >= from; p -= 4) {
|
|
275
|
+
if (view.getUint32(p, true) === 1 && view.getUint32(p + 4, true) === 8) {
|
|
276
|
+
aabb = p;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (aabb < 0) return null;
|
|
281
|
+
|
|
282
|
+
const ok = (o: number): boolean => {
|
|
283
|
+
const m00 = view.getFloat32(o, true);
|
|
284
|
+
const m11 = view.getFloat32(o + 20, true);
|
|
285
|
+
const m22 = view.getFloat32(o + 40, true);
|
|
286
|
+
const tx = view.getFloat32(o + 12, true);
|
|
287
|
+
const ty = view.getFloat32(o + 28, true);
|
|
288
|
+
const tz = view.getFloat32(o + 44, true);
|
|
289
|
+
if (![m00, m11, m22, tx, ty, tz].every(Number.isFinite)) return false;
|
|
290
|
+
if (Math.abs(tx) > 1e7 || Math.abs(ty) > 1e7 || Math.abs(tz) > 1e7) return false;
|
|
291
|
+
// 缩放不能全是 0:零填充区被这条挡掉
|
|
292
|
+
if (m00 === 0 && m11 === 0 && m22 === 0) return false;
|
|
293
|
+
for (let k = 12; k < 16; k++) {
|
|
294
|
+
const c = view.getFloat32(o + k * 4, true);
|
|
295
|
+
if (!Number.isFinite(c) || Math.abs(c) > 1e3) return false;
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
let start = aabb;
|
|
301
|
+
while (start - V1_INSTANCE_BYTES >= from && ok(start - V1_INSTANCE_BYTES)) {
|
|
302
|
+
start -= V1_INSTANCE_BYTES;
|
|
303
|
+
}
|
|
304
|
+
const count = (aabb - start) / V1_INSTANCE_BYTES;
|
|
305
|
+
if (count <= 0 || count > 1_000_000) return null;
|
|
306
|
+
|
|
307
|
+
const out = new Float32Array(count * 16);
|
|
308
|
+
for (let i = 0; i < count; i++) {
|
|
309
|
+
const o = start + i * V1_INSTANCE_BYTES;
|
|
310
|
+
for (let k = 0; k < 16; k++) out[i * 16 + k] = view.getFloat32(o + k * 4, true);
|
|
311
|
+
}
|
|
312
|
+
return { count, floats: out };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* 从 `from` 起找索引头 `[1, indexCount, 0x00040100]`,返回相对偏移,找不到返回 -1。
|
|
317
|
+
*
|
|
318
|
+
* 只在「顶点块之后那段不是版本 0 的 UV 头」时用得到:版本 1 的属性块布局没有
|
|
319
|
+
* 稳定规律,与其猜行宽,不如直接跳到结构最稳定的索引段。
|
|
320
|
+
*/
|
|
321
|
+
function findIndexHeader(view: DataView, from: number): number {
|
|
322
|
+
const limit = view.byteLength - 12;
|
|
323
|
+
for (let p = from; p <= limit; p += 4) {
|
|
324
|
+
if (view.getUint32(p, true) !== 1) continue;
|
|
325
|
+
const count = view.getUint32(p + 4, true);
|
|
326
|
+
if (count === 0 || count % 3 !== 0) continue;
|
|
327
|
+
if (view.getUint32(p + 8, true) !== INDEX_HEADER_MARK) continue;
|
|
328
|
+
return p;
|
|
329
|
+
}
|
|
330
|
+
return -1;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* 从 `to - 12` 往前找量化盒 `[1][8][minx,miny,minz,maxx,maxy,maxz]`,返回 6 个 float。
|
|
335
|
+
*
|
|
336
|
+
* 版本 1 的柱状图(`3D-Histogram`)把顶点压进单位盒(x/z 落在 [-0.5,0.5]、y 落在 [0,1],
|
|
337
|
+
* 就是「一根菱形截面的柱」的模板),真实位置与尺寸只存在记录尾部这个盒里。不反量化的
|
|
338
|
+
* 话所有柱子都叠在原点、边长 1 米,看起来就是「什么都加载不出来」。
|
|
339
|
+
*
|
|
340
|
+
* 反过来从索引头往前扫:同一个记录里可能还有别的 `[1][8]`(实测 mesh1 就有一个诱饵),
|
|
341
|
+
* 越靠后越可能是真正的那个。盒本身要求每轴 min < max,这是排除诱饵的硬条件。
|
|
342
|
+
*/
|
|
343
|
+
function findQuantBox(view: DataView, from: number, to: number): Float32Array | null {
|
|
344
|
+
for (let p = to - 12; p >= from; p -= 4) {
|
|
345
|
+
if (view.getUint32(p, true) !== 1 || view.getUint32(p + 4, true) !== 8) continue;
|
|
346
|
+
if (p + 32 > view.byteLength) continue;
|
|
347
|
+
const f = new Float32Array(6);
|
|
348
|
+
let ok = true;
|
|
349
|
+
for (let i = 0; i < 6; i++) {
|
|
350
|
+
const v = view.getFloat32(p + 8 + i * 4, true);
|
|
351
|
+
if (!Number.isFinite(v) || Math.abs(v) > 1e7) {
|
|
352
|
+
ok = false;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
f[i] = v;
|
|
356
|
+
}
|
|
357
|
+
if (!ok) continue;
|
|
358
|
+
if (!(f[0]! < f[3]! && f[1]! < f[4]! && f[2]! < f[5]!)) continue;
|
|
359
|
+
return f;
|
|
360
|
+
}
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* 顶点落在 [-1, 1] 里就认为它是「归一化模板」,按盒逐轴拉伸回去。
|
|
366
|
+
*
|
|
367
|
+
* 逐轴用自己的包围盒做映射(而不是假设 [0,1] 或 [-0.5,0.5]):实测柱状图 x、z 是
|
|
368
|
+
* [-0.5,0.5]、y 是 [0,1],硬编码任一约定都会把柱子摆错。
|
|
369
|
+
*/
|
|
370
|
+
function applyQuantBox(position: Float32Array, box: Float32Array): boolean {
|
|
371
|
+
const min = [Infinity, Infinity, Infinity];
|
|
372
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
373
|
+
for (let i = 0; i < position.length; i += 3) {
|
|
374
|
+
for (let a = 0; a < 3; a++) {
|
|
375
|
+
const v = position[i + a]!;
|
|
376
|
+
if (v < min[a]!) min[a] = v;
|
|
377
|
+
if (v > max[a]!) max[a] = v;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
for (let a = 0; a < 3; a++) {
|
|
381
|
+
if (!(Math.abs(min[a]!) <= 1.001 && Math.abs(max[a]!) <= 1.001)) return false;
|
|
382
|
+
}
|
|
383
|
+
for (let a = 0; a < 3; a++) {
|
|
384
|
+
const span = max[a]! - min[a]!;
|
|
385
|
+
const scale = (box[a + 3]! - box[a]!) / (span > 1e-6 ? span : 1);
|
|
386
|
+
const base = box[a]! - (span > 1e-6 ? min[a]! * scale : (box[a + 3]! - box[a]!) / 2);
|
|
387
|
+
for (let i = a; i < position.length; i += 3) position[i] = base + position[i]! * scale;
|
|
388
|
+
}
|
|
389
|
+
return true;
|
|
187
390
|
}
|
|
188
391
|
|
|
189
|
-
function readGeometry(
|
|
392
|
+
function readGeometry(
|
|
393
|
+
br: BinaryReader,
|
|
394
|
+
geoSize: number
|
|
395
|
+
): { meshes: MeshRecord[]; skippedAttrs: number; quantized: number } {
|
|
190
396
|
const meshCount = br.u32();
|
|
191
397
|
if (meshCount > 4096) throw new Error(`[s3m] S3M 1.x 网格数异常:${meshCount}`);
|
|
192
398
|
const out: MeshRecord[] = [];
|
|
399
|
+
let skippedAttrs = 0;
|
|
400
|
+
let quantized = 0;
|
|
193
401
|
|
|
194
402
|
for (let m = 0; m < meshCount; m++) {
|
|
195
403
|
const geoName = String(br.u32());
|
|
@@ -201,30 +409,66 @@ function readGeometry(br: BinaryReader, geoSize: number): MeshRecord[] {
|
|
|
201
409
|
throw new Error(`[s3m] S3M 1.x 顶点数异常:网格 ${geoName} V=${V}(字段 7=${seven} 772=${seven72})`);
|
|
202
410
|
}
|
|
203
411
|
|
|
204
|
-
//
|
|
412
|
+
// 顶点:版本 0 是 vec4(x,y,z,w),w 实测恒等于 z;版本 1 有一半数据是 vec3。
|
|
413
|
+
// 先认版本 0 的 UV 头(不传参时走的还是这条老路,判据一字未改),认不出才用
|
|
414
|
+
// 「顶点块之后那块的头 dword 等于 V」去定行宽。
|
|
415
|
+
const posStart = br.offset;
|
|
416
|
+
const u32At = (off: number): number =>
|
|
417
|
+
off + 4 <= br.view.byteLength ? br.view.getUint32(off, true) : -1;
|
|
418
|
+
const v0UvAt16 =
|
|
419
|
+
u32At(posStart + V * 16 + 12) === 1 && u32At(posStart + V * 16 + 16) === V;
|
|
420
|
+
const stride: 12 | 16 = v0UvAt16
|
|
421
|
+
? 16
|
|
422
|
+
: u32At(posStart + V * 16) === V
|
|
423
|
+
? 16
|
|
424
|
+
: 12;
|
|
205
425
|
const position = new Float32Array(V * 3);
|
|
206
426
|
for (let i = 0; i < V; i++) {
|
|
207
427
|
const x = br.f32();
|
|
208
428
|
const y = br.f32();
|
|
209
429
|
const z = br.f32();
|
|
210
|
-
br.f32(); // w
|
|
430
|
+
if (stride === 16) br.f32(); // w
|
|
211
431
|
position[i * 3] = x;
|
|
212
432
|
position[i * 3 + 1] = y;
|
|
213
433
|
position[i * 3 + 2] = z;
|
|
214
434
|
}
|
|
215
435
|
|
|
216
|
-
// UV 数组头 [0,0,0,kind=1,count=V,flag=2]
|
|
217
|
-
br.u32(); br.u32(); br.u32();
|
|
218
|
-
const uvKind = br.u32();
|
|
219
|
-
const uvCount = br.u32();
|
|
220
|
-
br.u32(); // flag
|
|
221
|
-
if (uvKind !== 1 || uvCount !== V) {
|
|
222
|
-
throw new Error(`[s3m] S3M 1.x UV 头不符:网格 ${geoName} kind=${uvKind} count=${uvCount} 期望 1/${V}`);
|
|
223
|
-
}
|
|
224
436
|
const uv = new Float32Array(V * 2);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
437
|
+
let instances: Float32Array | undefined;
|
|
438
|
+
if (v0UvAt16) {
|
|
439
|
+
// 版本 0:UV 数组头 [0,0,0,kind=1,count=V,flag=2]
|
|
440
|
+
br.u32(); br.u32(); br.u32();
|
|
441
|
+
const uvKind = br.u32();
|
|
442
|
+
const uvCount = br.u32();
|
|
443
|
+
br.u32(); // flag
|
|
444
|
+
if (uvKind !== 1 || uvCount !== V) {
|
|
445
|
+
throw new Error(`[s3m] S3M 1.x UV 头不符:网格 ${geoName} kind=${uvKind} count=${uvCount} 期望 1/${V}`);
|
|
446
|
+
}
|
|
447
|
+
for (let i = 0; i < V; i++) {
|
|
448
|
+
uv[i * 2] = br.f32();
|
|
449
|
+
uv[i * 2 + 1] = br.f32();
|
|
450
|
+
}
|
|
451
|
+
} else {
|
|
452
|
+
// 版本 1:顶点块之后是若干属性块(UV / 法线 / 颜色混排,行宽逐网格不同),
|
|
453
|
+
// 整段跳过,靠索引头的固定标记落位。这两类数据没有纹理,跳过不影响画面。
|
|
454
|
+
const at = findIndexHeader(br.view, br.offset);
|
|
455
|
+
if (at < 0) {
|
|
456
|
+
throw new Error(`[s3m] S3M 1.x 属性段无法定位索引头:网格 ${geoName}(偏移 ${br.offset}/${geoSize})`);
|
|
457
|
+
}
|
|
458
|
+
if (at !== br.offset) {
|
|
459
|
+
skippedAttrs++;
|
|
460
|
+
// 优先认实例化:柱状图是真·实例化的(一份模板 + N 个变换矩阵),
|
|
461
|
+
// 顶点本来就该保持单位盒,靠矩阵摆位/缩放/上色。
|
|
462
|
+
const inst = readInstances(br.view, br.offset, at);
|
|
463
|
+
if (inst) {
|
|
464
|
+
instances = inst.floats;
|
|
465
|
+
} else {
|
|
466
|
+
// 没有实例矩阵时,顶点是归一化模板,用记录尾的量化盒展开兜底
|
|
467
|
+
const box = findQuantBox(br.view, br.offset, at);
|
|
468
|
+
if (box) quantized += applyQuantBox(position, box) ? 1 : 0;
|
|
469
|
+
}
|
|
470
|
+
br.offset = at;
|
|
471
|
+
}
|
|
228
472
|
}
|
|
229
473
|
|
|
230
474
|
// 索引头 [1, indexCount, 0x00040100]
|
|
@@ -244,9 +488,18 @@ function readGeometry(br: BinaryReader, geoSize: number): MeshRecord[] {
|
|
|
244
488
|
if (one !== 1) {
|
|
245
489
|
throw new Error(`[s3m] S3M 1.x 记录尾标记异常:网格 ${geoName} 期望 1 实得 ${one}`);
|
|
246
490
|
}
|
|
247
|
-
out.push({
|
|
491
|
+
out.push({
|
|
492
|
+
geoName,
|
|
493
|
+
vertexCount: V,
|
|
494
|
+
position,
|
|
495
|
+
uv,
|
|
496
|
+
indexCount,
|
|
497
|
+
indices,
|
|
498
|
+
materialId,
|
|
499
|
+
...(instances ? { instances } : {}),
|
|
500
|
+
});
|
|
248
501
|
}
|
|
249
|
-
return out;
|
|
502
|
+
return { meshes: out, skippedAttrs, quantized };
|
|
250
503
|
}
|
|
251
504
|
|
|
252
505
|
/**
|
|
@@ -308,6 +561,10 @@ function readTextures(br: BinaryReader, texSize: number): Record<string, S3MText
|
|
|
308
561
|
|
|
309
562
|
/* ------------------------------------------------------------------ 入口 */
|
|
310
563
|
|
|
564
|
+
/** 版本 1 布局的提示只打一次:几百块瓦片各喊一句会把控制台刷爆 */
|
|
565
|
+
let warnedV1Attrs = false;
|
|
566
|
+
let warnedQuantBox = false;
|
|
567
|
+
|
|
311
568
|
export async function parseS3M1(arrayBuffer: ArrayBuffer, options: ParseS3M1Options = {}): Promise<ParsedTile> {
|
|
312
569
|
const inflate = options.inflate ?? defaultInflate;
|
|
313
570
|
const r = new BinaryReader(arrayBuffer);
|
|
@@ -326,7 +583,23 @@ export async function parseS3M1(arrayBuffer: ArrayBuffer, options: ParseS3M1Opti
|
|
|
326
583
|
|
|
327
584
|
const geoSize = br.u32();
|
|
328
585
|
const geoStart = br.offset;
|
|
329
|
-
const meshes
|
|
586
|
+
const { meshes, skippedAttrs, quantized } = readGeometry(
|
|
587
|
+
new BinaryReader(br.buffer, br.base + geoStart, geoSize),
|
|
588
|
+
geoSize
|
|
589
|
+
);
|
|
590
|
+
if (quantized && !warnedQuantBox) {
|
|
591
|
+
warnedQuantBox = true;
|
|
592
|
+
console.warn(
|
|
593
|
+
`[s3m] S3M 1.x(版本 1 布局):顶点是归一化模板,已按记录尾的量化盒展开到真实尺寸/位置(本进程只提示一次)。`
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
if (skippedAttrs && !warnedV1Attrs) {
|
|
597
|
+
warnedV1Attrs = true;
|
|
598
|
+
console.warn(
|
|
599
|
+
`[s3m] S3M 1.x(版本 1 布局):UV/法线/颜色属性块已跳过,几何与索引正常。` +
|
|
600
|
+
`若该数据集带纹理,贴图会取到同一像素(本进程只提示一次)。`
|
|
601
|
+
);
|
|
602
|
+
}
|
|
330
603
|
br.offset = geoStart + geoSize;
|
|
331
604
|
|
|
332
605
|
const smallSize = br.u32();
|
|
@@ -365,6 +638,34 @@ export async function parseS3M1(arrayBuffer: ArrayBuffer, options: ParseS3M1Opti
|
|
|
365
638
|
instanceMode: 0,
|
|
366
639
|
instanceIndex: -1,
|
|
367
640
|
};
|
|
641
|
+
let pickInfo: Record<number, PickEntry> | undefined;
|
|
642
|
+
if (mesh.instances && mesh.instances.length >= 16) {
|
|
643
|
+
// stride 16 = 3×4 行主序矩阵 + RGBA,正是 GeometryBuilder 认的布局:
|
|
644
|
+
// 它会 setMatrixAt(i) 并拿 [12..14] 当实例颜色(柱状图的黄/绿/红就来自这里)
|
|
645
|
+
const count = mesh.instances.length / 16;
|
|
646
|
+
vp.instanceCount = count;
|
|
647
|
+
vp.instanceMode = 16;
|
|
648
|
+
vp.instanceIndex = 0;
|
|
649
|
+
vp.instanceBuffer = new Uint8Array(
|
|
650
|
+
mesh.instances.buffer,
|
|
651
|
+
mesh.instances.byteOffset,
|
|
652
|
+
mesh.instances.byteLength
|
|
653
|
+
);
|
|
654
|
+
// S3M 1.x 容器里**没有 pickInfo 段**,而拾取链路(Picker → _instanceFeature)
|
|
655
|
+
// 完全靠 pickInfo.instanceIds 把「实例序号」翻译成 SMID;缺了它就一路返回 null,
|
|
656
|
+
// 表现就是「模型能看见、但怎么点都选不中」。这里按实例合成一份:
|
|
657
|
+
// 控制位 = 实例序号(写进 aInstanceBatchId,状态纹理按它索引)
|
|
658
|
+
// SMID = geoName × 1e6 + 实例序号(哈希值本身 32 位,跨瓦片不重复)
|
|
659
|
+
const ids = new Float32Array(count);
|
|
660
|
+
const instPick: Record<number, PickEntry> = {};
|
|
661
|
+
const smidBase = Number(mesh.geoName) * S3M1_SMID_STRIDE;
|
|
662
|
+
for (let i = 0; i < count; i++) {
|
|
663
|
+
ids[i] = i;
|
|
664
|
+
instPick[smidBase + i] = { batchId: i, instanceIds: [i] };
|
|
665
|
+
}
|
|
666
|
+
vp.instanceBatchIds = ids;
|
|
667
|
+
pickInfo = instPick;
|
|
668
|
+
}
|
|
368
669
|
const pack: IndexPackage = {
|
|
369
670
|
indexCount: mesh.indexCount,
|
|
370
671
|
indexType: 0, // 0 = UNSIGNED_SHORT
|
|
@@ -373,7 +674,12 @@ export async function parseS3M1(arrayBuffer: ArrayBuffer, options: ParseS3M1Opti
|
|
|
373
674
|
indices: new Uint8Array(mesh.indices.buffer, mesh.indices.byteOffset, mesh.indices.byteLength),
|
|
374
675
|
passNames: [mesh.materialId],
|
|
375
676
|
};
|
|
376
|
-
geoPackage[name] = {
|
|
677
|
+
geoPackage[name] = {
|
|
678
|
+
name,
|
|
679
|
+
vertexPackage: vp,
|
|
680
|
+
indexPackages: [pack],
|
|
681
|
+
...(pickInfo ? { pickInfo } : {}),
|
|
682
|
+
};
|
|
377
683
|
}
|
|
378
684
|
|
|
379
685
|
// pageLods 与网格必须按 GeoName 绑定,不能按出现顺序:
|
package/src/core/S3MBParser.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { BinaryReader } from './BinaryReader.js';
|
|
|
13
13
|
import { parseDracoGeometry } from './Draco.js';
|
|
14
14
|
import { parseMeshOptGeometry } from './MeshOpt.js';
|
|
15
15
|
import { isS3M1Buffer, parseS3M1 } from './S3M1Parser.js';
|
|
16
|
+
import type { DegradationKind } from './degradations.js';
|
|
16
17
|
import type {
|
|
17
18
|
AttrDType,
|
|
18
19
|
BoundingBox,
|
|
@@ -663,6 +664,22 @@ export async function parseS3MB(arrayBuffer: ArrayBuffer, options: ParseOptions
|
|
|
663
664
|
extensions = s ? JSON.parse(s) : undefined;
|
|
664
665
|
}
|
|
665
666
|
|
|
667
|
+
// 3DGS:泼溅参数(gsScale / gsRotation)的量化格式已经标定(见 core/GaussianSplat.ts),
|
|
668
|
+
// 正常数据会被渲染成各向异性的高斯片,**这不再是降级**。
|
|
669
|
+
// 只有「数据声称自己是 3DGS、但骨架里根本没有这两条流」时才退回点渲染 ——
|
|
670
|
+
// 那种情况没有任何东西可以补,只能如实上报,而不是画一堆点让人猜。
|
|
671
|
+
const degradations: DegradationKind[] = [];
|
|
672
|
+
const gs = (extensions as { GaussianSplatting?: { isGaussianSplatting?: boolean } } | undefined)
|
|
673
|
+
?.GaussianSplatting;
|
|
674
|
+
if (gs?.isGaussianSplatting) {
|
|
675
|
+
const hasSplatStreams = Object.values(geoPackage).some((e) =>
|
|
676
|
+
e.vertexPackage.vertexAttributes.some(
|
|
677
|
+
(a) => a.semantic === 'gsScale' || a.semantic === 'gsRotation'
|
|
678
|
+
)
|
|
679
|
+
);
|
|
680
|
+
if (!hasSplatStreams) degradations.push('gaussian-splatting-points');
|
|
681
|
+
}
|
|
682
|
+
|
|
666
683
|
return {
|
|
667
684
|
version,
|
|
668
685
|
compressedType,
|
|
@@ -673,6 +690,7 @@ export async function parseS3MB(arrayBuffer: ArrayBuffer, options: ParseOptions
|
|
|
673
690
|
materials,
|
|
674
691
|
pickInfo,
|
|
675
692
|
extensions,
|
|
693
|
+
...(degradations.length > 0 ? { degradations } : {}),
|
|
676
694
|
bytesRead: t.offset,
|
|
677
695
|
bytesTotal: t.view.byteLength,
|
|
678
696
|
};
|
|
@@ -73,6 +73,8 @@ import { parseGroupNodeEntries, parseSkeletonEntries, parsePickInfoEntries } fro
|
|
|
73
73
|
import { atlasAlphaMask, shrinkRectToOpaque } from './AtlasAlpha.js';
|
|
74
74
|
import { packAtlases, decodeAtlasImage } from './AtlasPack.js';
|
|
75
75
|
import type { PackedAtlases } from './AtlasPack.js';
|
|
76
|
+
import { transcodeCRN } from './CRNDecode.js';
|
|
77
|
+
import { resolveTextureKind, PixelFormat, TextureCompress } from './TextureFormat.js';
|
|
76
78
|
|
|
77
79
|
/** 图集子区域,以及它绑定的几何顶点区间 */
|
|
78
80
|
export interface S3MSubTextureBinding {
|
|
@@ -487,13 +489,35 @@ export async function parseTexblockSegments(
|
|
|
487
489
|
): Promise<S3MBlockAtlasImage[][]> {
|
|
488
490
|
const inflate = options.inflate ?? defaultInflate;
|
|
489
491
|
const bytes = new Uint8Array(arrayBuffer);
|
|
490
|
-
|
|
491
|
-
if (
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
492
|
+
let segments: S3MBlockAtlasImage[][] | null = await tryDynPack(bytes, inflate);
|
|
493
|
+
if (!segments) segments = await tryStaticPack(bytes, inflate);
|
|
494
|
+
if (!segments) {
|
|
495
|
+
const container = await readContainerEntries(bytes, inflate);
|
|
496
|
+
if (container) segments = [container];
|
|
497
|
+
}
|
|
498
|
+
if (!segments) {
|
|
499
|
+
throw new Error('[s3m] .texblock 无法识别的外层格式(动态 pack / 静态 pack / 容器均不成立)');
|
|
500
|
+
}
|
|
501
|
+
// 3D-CBD-2 的 texblock 图像是 CRN 压缩(ct=26/27,'Hx' 魔数)。leaf 组合管线
|
|
502
|
+
// (composeLeafAtlas → decodeAtlasImage)只认 DXT / 已解 RGBA,这里统一在解析层
|
|
503
|
+
// 转码成 DXT:与主纹理解码的 crn->dxt 路径同一份 crunch wasm。
|
|
504
|
+
// 转码失败的记录原样保留(下游按缺失精灵处理并计数,不静默吞)。
|
|
505
|
+
for (const seg of segments) {
|
|
506
|
+
for (const rec of seg) {
|
|
507
|
+
if (resolveTextureKind(rec.compressType, rec.pixelFormat) !== 'crn') continue;
|
|
508
|
+
try {
|
|
509
|
+
const crn = await transcodeCRN(rec.data);
|
|
510
|
+
rec.data = crn.data;
|
|
511
|
+
rec.width = crn.width;
|
|
512
|
+
rec.height = crn.height;
|
|
513
|
+
rec.compressType = crn.dxtFormat === 1 ? TextureCompress.DXT1_RGBA : 14; // 14 = DXT5(见 TextureFormat 实测注释)
|
|
514
|
+
rec.pixelFormat = crn.dxtFormat === 1 ? PixelFormat.DXT1 : PixelFormat.DXT5;
|
|
515
|
+
} catch {
|
|
516
|
+
// 保留原记录,交给下游缺失统计
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return segments;
|
|
497
521
|
}
|
|
498
522
|
|
|
499
523
|
/** 解压后的裸图像记录区:`ct|w|h|pf|size|data` 可多条连排,尾部允许有无关字节 */
|
|
@@ -512,6 +536,27 @@ function readImageRecords(raw: Uint8Array, id: string): S3MBlockAtlasImage[] {
|
|
|
512
536
|
return out;
|
|
513
537
|
}
|
|
514
538
|
|
|
539
|
+
/**
|
|
540
|
+
* 条目负载解压。`zip == unzip` 是**存储态**(未压缩,内容直接就是图像记录区):
|
|
541
|
+
* 3D-CBD-2 的 Tex_0_000.texblock 实测存在这种条目,无条件 inflate 会把整包
|
|
542
|
+
* 判成「无法识别的外层格式」。zlib 流必有 ~11 字节头尾开销,zip == unzip
|
|
543
|
+
* 不可能是有效 zlib,此判定安全。
|
|
544
|
+
*/
|
|
545
|
+
async function inflateEntryPayload(
|
|
546
|
+
payload: Uint8Array,
|
|
547
|
+
unzipSize: number,
|
|
548
|
+
zipSize: number,
|
|
549
|
+
inflate: InflateFn
|
|
550
|
+
): Promise<Uint8Array | null> {
|
|
551
|
+
if (zipSize === unzipSize) return payload;
|
|
552
|
+
try {
|
|
553
|
+
const raw = await inflate(payload);
|
|
554
|
+
return raw.length === unzipSize ? raw : null;
|
|
555
|
+
} catch {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
515
560
|
/** 读 `count | 条目 × count` 容器(尾部允许有无关字节)。结构不成立返回 null。 */
|
|
516
561
|
async function readContainerEntries(bytes: Uint8Array, inflate: InflateFn): Promise<S3MBlockAtlasImage[] | null> {
|
|
517
562
|
if (bytes.byteLength < 8) return null;
|
|
@@ -529,8 +574,8 @@ async function readContainerEntries(bytes: Uint8Array, inflate: InflateFn): Prom
|
|
|
529
574
|
const unzipSize = r.u32();
|
|
530
575
|
const zipSize = r.u32();
|
|
531
576
|
if (zipSize > r.remaining) return null;
|
|
532
|
-
const raw = await
|
|
533
|
-
if (raw.length !== unzipSize) return null;
|
|
577
|
+
const raw = await inflateEntryPayload(r.bytesCopy(zipSize), unzipSize, zipSize, inflate);
|
|
578
|
+
if (!raw || raw.length !== unzipSize) return null;
|
|
534
579
|
out.push(...readImageRecords(raw, name));
|
|
535
580
|
}
|
|
536
581
|
return out.length ? out : null;
|
|
@@ -552,8 +597,8 @@ async function readSingleEntry(bytes: Uint8Array, inflate: InflateFn): Promise<S
|
|
|
552
597
|
const unzipSize = r.u32();
|
|
553
598
|
const zipSize = r.u32();
|
|
554
599
|
if (zipSize !== r.remaining) return null;
|
|
555
|
-
const raw = await
|
|
556
|
-
if (raw.length !== unzipSize) return null;
|
|
600
|
+
const raw = await inflateEntryPayload(r.bytesCopy(zipSize), unzipSize, zipSize, inflate);
|
|
601
|
+
if (!raw || raw.length !== unzipSize) return null;
|
|
557
602
|
const recs = readImageRecords(raw, name);
|
|
558
603
|
return recs.length ? recs : null;
|
|
559
604
|
} catch {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 泼溅 / 点云瓦片的细化判据
|
|
3
|
+
*
|
|
4
|
+
* 判据只有一条:**单个高斯在屏幕上有多大**。3σ 半轴超过 `SPLAT_REFINE_PX` 像素
|
|
5
|
+
* 就再细化一层 —— 一个高斯铺出十几二十个像素时,它已经把整片屋顶/柱子糊成一坨,
|
|
6
|
+
* 细节全在「这一片高斯自己」里,再远的层级也换不回来。
|
|
7
|
+
*
|
|
8
|
+
* 走过两轮弯路,都不要再回去:
|
|
9
|
+
*
|
|
10
|
+
* 1. **包围球投影像素 vs rangeValue**(网格 LOD 的判据)。泼溅每层的 σ 是同一个
|
|
11
|
+
* 物理量,包围球半径和 rangeValue 跟「画面够不够密」没有因果关系。实测寿宁阁
|
|
12
|
+
* 的 rangeMode=2、rangeValue≈0.066·半径,按它算在任何距离都远大于阈值,等于
|
|
13
|
+
* 「永远细化到叶子」。
|
|
14
|
+
*
|
|
15
|
+
* 2. **屏幕覆盖率**(`高斯数/包围球投影面积 × 单个高斯面积`)。看起来很有道理,
|
|
16
|
+
* 但实测下来是个陷阱:根瓦片 σ=0.21 m、1888 个点,覆盖率就已经算到 1.7;
|
|
17
|
+
* 子瓦片直接跳到 8 —— 于是**在第二层就停了**,最终只画了 35.6 万个高斯,
|
|
18
|
+
* 占总量 2.3%。而官方是画到叶子层。21 cm 的高斯画不出 30 cm 宽的柱子,
|
|
19
|
+
* 画面上就是「柱子没了、到处是一坨坨横着的糊斑」。
|
|
20
|
+
* 覆盖率回答的是「屏幕盖满了没有」,盖满了也完全可能全是粗高斯。
|
|
21
|
+
*
|
|
22
|
+
* 所以现在改成屏幕尺寸判据。它天然自带下限:`splatPx` 已经小到着色器会剔除的量级
|
|
23
|
+
* (见 SplatProjection 的 `SPLAT_CULL_PIXELS`)时不再细化,再下一层只是白下载。
|
|
24
|
+
*/
|
|
25
|
+
import { SPLAT_SIGMA } from './SplatProjection.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 3σ 半轴超过这个像素数就继续细化。
|
|
29
|
+
*
|
|
30
|
+
* 4 px 意味着单个高斯在屏幕上约 8 px 宽 —— 再细下去肉眼分辨不出,却要多占几倍显存
|
|
31
|
+
* (每深一层点数约 ×5)。着色器的亚像素剔除线是 2 px 半径,这里留了一倍余量。
|
|
32
|
+
*/
|
|
33
|
+
export const SPLAT_REFINE_PX = 4.0;
|
|
34
|
+
|
|
35
|
+
/** 垂直视场角写死 45°,与 LOD 其它路径、官方 PageLod 口径一致 */
|
|
36
|
+
const HALF_FOV_TAN = Math.tan(Math.PI / 8);
|
|
37
|
+
|
|
38
|
+
/** 高斯实际可见面积相对 π(3σ)² 的比例(exp(-4r²) 衰减下的等效半径) */
|
|
39
|
+
const EFFECTIVE_AREA_RATIO = 0.25;
|
|
40
|
+
|
|
41
|
+
export interface SplatLodInput {
|
|
42
|
+
/** 该瓦片里的高斯数 */
|
|
43
|
+
splats: number;
|
|
44
|
+
/** 包围球半径(米) */
|
|
45
|
+
radius: number;
|
|
46
|
+
/** 相机到球心距离(米) */
|
|
47
|
+
distance: number;
|
|
48
|
+
/** 该瓦片高斯的代表性半轴(米);0/缺省 = 未知,退回调用方的通用判据 */
|
|
49
|
+
sigma: number;
|
|
50
|
+
/** 绘图缓冲高度(像素) */
|
|
51
|
+
screenHeight: number;
|
|
52
|
+
/** 是否还有子瓦片 */
|
|
53
|
+
hasChild: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface SplatLodResult {
|
|
57
|
+
/** 3σ 半轴在屏幕上的长度(像素)—— 判据本体 */
|
|
58
|
+
splatPx: number;
|
|
59
|
+
/** 每像素高斯数(诊断用) */
|
|
60
|
+
density: number;
|
|
61
|
+
/** 屏幕覆盖率(诊断用;**不是**判据,原因见文件头) */
|
|
62
|
+
coverage: number;
|
|
63
|
+
needRefine: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 决定泼溅瓦片要不要再细化一层。
|
|
68
|
+
* `sigma <= 0` 时返回 `needRefine: false`,调用方应退回原判据。
|
|
69
|
+
*/
|
|
70
|
+
export function splatLodDecision(o: SplatLodInput): SplatLodResult {
|
|
71
|
+
const dist = Math.max(o.distance, 1e-6);
|
|
72
|
+
const ppm = o.screenHeight / 2 / HALF_FOV_TAN / dist;
|
|
73
|
+
const splatPx = SPLAT_SIGMA * Math.max(o.sigma, 0) * ppm;
|
|
74
|
+
if (!(o.sigma > 0) || !(o.radius > 0)) {
|
|
75
|
+
return { splatPx, density: 0, coverage: 0, needRefine: false };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const areaPx = Math.PI * (ppm * o.radius) ** 2;
|
|
79
|
+
const density = areaPx > 1e-6 ? o.splats / areaPx : 0;
|
|
80
|
+
const coverage = density * Math.PI * splatPx * splatPx * EFFECTIVE_AREA_RATIO;
|
|
81
|
+
const needRefine = o.hasChild && splatPx > SPLAT_REFINE_PX;
|
|
82
|
+
return { splatPx, density, coverage, needRefine };
|
|
83
|
+
}
|