s3m-three 0.1.0 → 0.2.0
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/LICENSE +202 -202
- package/dist/render/DatasetManager.d.ts +10 -3
- package/dist/render/DatasetManager.d.ts.map +1 -1
- package/dist/render/DatasetManager.js +14 -5
- package/dist/render/DatasetManager.js.map +1 -1
- package/dist/render/EdgeLines.js +46 -46
- package/dist/render/GroundManager.d.ts +9 -0
- package/dist/render/GroundManager.d.ts.map +1 -1
- package/dist/render/GroundManager.js +13 -1
- package/dist/render/GroundManager.js.map +1 -1
- package/dist/render/Picker.js +66 -66
- package/dist/render/S3MViewer.d.ts +11 -3
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +18 -3
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/render/StyleMaterial.js +265 -265
- package/dist/vendor/meshopt_decoder.module.js +118 -118
- package/docs/API.md +20 -0
- package/package.json +1 -1
- package/src/core/BinaryReader.ts +116 -116
- package/src/core/DXTTextureDecode.ts +260 -260
- package/src/core/FeatureInfo.ts +150 -150
- package/src/core/MeshOpt.ts +401 -401
- package/src/core/S3MBParser.ts +590 -590
- package/src/core/types.ts +222 -222
- package/src/render/DatasetManager.ts +17 -6
- package/src/render/EdgeLines.ts +343 -343
- package/src/render/GeometryBuilder.ts +840 -840
- package/src/render/GroundManager.ts +14 -1
- package/src/render/Picker.ts +293 -293
- package/src/render/S3MViewer.ts +18 -3
- package/src/render/StyleMaterial.ts +602 -602
- package/src/render/TextureFactory.ts +134 -134
- package/src/tileset/S3MTileset.ts +552 -552
- package/src/vendor/meshopt_decoder.module.d.ts +31 -31
- package/src/vendor/meshopt_decoder.module.js +118 -118
- package/dist/render/CoordinateHelper.d.ts +0 -90
- package/dist/render/CoordinateHelper.d.ts.map +0 -1
- package/dist/render/CoordinateHelper.js +0 -85
- package/dist/render/CoordinateHelper.js.map +0 -1
- package/dist/render/S3MLayer.d.ts +0 -457
- package/dist/render/S3MLayer.d.ts.map +0 -1
- package/dist/render/S3MLayer.js +0 -1860
- package/dist/render/S3MLayer.js.map +0 -1
package/src/core/S3MBParser.ts
CHANGED
|
@@ -1,590 +1,590 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* S3MB 瓦片解析器(S3M 1.0 / 2.0 / 3.0)
|
|
3
|
-
*
|
|
4
|
-
* 实现依据:SuperMap/s3m-spec 官方 S3M_SDK/S3M_JS/S3M_module/S3MParser/S3ModelParser.js
|
|
5
|
-
* (Apache-2.0)+ Specification/S3MB/S3MBBinary.md。
|
|
6
|
-
*
|
|
7
|
-
* 与官方实现的差异:
|
|
8
|
-
* 1. 不依赖 Cesium —— 输出纯数据对象,几何上传交给渲染层。
|
|
9
|
-
* 2. 修复了官方 v3 非压缩分支把整个 ArrayBuffer(含文件头)当数据流的缺陷。
|
|
10
|
-
* 3. Draco / MeshOpt 几何压缩只做识别,不做解码(需要额外的 wasm 解码器)。
|
|
11
|
-
*/
|
|
12
|
-
import { BinaryReader } from './BinaryReader.js';
|
|
13
|
-
import { parseMeshOptSkeleton, parseMeshOptIndexPackages } from './MeshOpt.js';
|
|
14
|
-
import type {
|
|
15
|
-
AttrDType,
|
|
16
|
-
GeoPackageEntry,
|
|
17
|
-
Geode,
|
|
18
|
-
GroupNode,
|
|
19
|
-
IndexPackage,
|
|
20
|
-
PageLOD,
|
|
21
|
-
ParsedTile,
|
|
22
|
-
PickEntry,
|
|
23
|
-
S3MTexture,
|
|
24
|
-
Vec3,
|
|
25
|
-
VertexAttribute,
|
|
26
|
-
VertexPackage,
|
|
27
|
-
} from './types.js';
|
|
28
|
-
|
|
29
|
-
export const S3M_VERSION = { V1: 1.0, V2: 2.0, V3: 3.0 } as const;
|
|
30
|
-
|
|
31
|
-
export const VertexTag = { UNKNOWN: 0, STANDARD: 1, COMPRESSED: 2, DRACO: 3 } as const;
|
|
32
|
-
export const VertexTagV3 = { STANDARD: 0, DRACO: 1, MESHOPT: 2 } as const;
|
|
33
|
-
|
|
34
|
-
const CompressOption = {
|
|
35
|
-
VERTEX: 1,
|
|
36
|
-
NORMAL: 2,
|
|
37
|
-
VERTEX_COLOR: 4,
|
|
38
|
-
SECOND_COLOR: 8,
|
|
39
|
-
TEXCOORD: 16,
|
|
40
|
-
TEXCOORD_IS_W: 32,
|
|
41
|
-
} as const;
|
|
42
|
-
|
|
43
|
-
const AttrTypeSize: Record<number, number> = { 0: 4, 1: 4, 2: 8, 3: 2 };
|
|
44
|
-
|
|
45
|
-
export type InflateFn = (data: Uint8Array) => Promise<Uint8Array> | Uint8Array;
|
|
46
|
-
|
|
47
|
-
export interface ParseOptions {
|
|
48
|
-
/** 自定义解压实现,默认用 DecompressionStream('deflate');Node 下传 zlib.inflateSync */
|
|
49
|
-
inflate?: InflateFn;
|
|
50
|
-
/** 逐段追踪偏移,调试解析用 */
|
|
51
|
-
trace?: (section: string, detail?: unknown, offset?: number) => void;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** 默认解压:zlib(文件头 78 9C / 78 DA)。浏览器与 Node 18+ 均可用。 */
|
|
55
|
-
async function defaultInflate(bytes: Uint8Array): Promise<Uint8Array> {
|
|
56
|
-
if (typeof DecompressionStream === 'undefined') {
|
|
57
|
-
throw new Error('[s3m] 当前环境无 DecompressionStream,请通过 options.inflate 注入解压实现');
|
|
58
|
-
}
|
|
59
|
-
const stream = new Blob([bytes as BlobPart]).stream().pipeThrough(new DecompressionStream('deflate'));
|
|
60
|
-
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function pushAttr(
|
|
64
|
-
vp: VertexPackage,
|
|
65
|
-
semantic: string,
|
|
66
|
-
data: Uint8Array,
|
|
67
|
-
components: number,
|
|
68
|
-
stride: number,
|
|
69
|
-
offset = 0,
|
|
70
|
-
divisor?: number,
|
|
71
|
-
dtype: AttrDType = 'f32'
|
|
72
|
-
): void {
|
|
73
|
-
vp.attrLocation[semantic] = vp.vertexAttributes.length;
|
|
74
|
-
vp.vertexAttributes.push({
|
|
75
|
-
semantic,
|
|
76
|
-
data,
|
|
77
|
-
components,
|
|
78
|
-
stride: stride || components * 4,
|
|
79
|
-
offsetInBytes: offset,
|
|
80
|
-
divisor,
|
|
81
|
-
dtype,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function newVertexPackage(): VertexPackage {
|
|
86
|
-
return { vertexAttributes: [], attrLocation: {}, instanceCount: 0, instanceMode: 0, instanceIndex: -1, verticesCount: 0 };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/* ---------------------------------------------------------------- LOD 树 */
|
|
90
|
-
|
|
91
|
-
function parseGeode(r: BinaryReader): Geode {
|
|
92
|
-
const matrix: number[] = new Array(16);
|
|
93
|
-
for (let i = 0; i < 16; i++) matrix[i] = r.f64();
|
|
94
|
-
const n = r.u32();
|
|
95
|
-
const skeletonNames: string[] = [];
|
|
96
|
-
for (let i = 0; i < n; i++) skeletonNames.push(r.string());
|
|
97
|
-
return { matrix, skeletonNames };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function parsePageLOD(r: BinaryReader, version: number): PageLOD {
|
|
101
|
-
const rangeValue = r.f32();
|
|
102
|
-
const rangeMode = r.u16();
|
|
103
|
-
const boundingSphere = { center: r.vec3f64(), radius: r.f64() };
|
|
104
|
-
|
|
105
|
-
const pageLod: PageLOD = { rangeValue, rangeMode, boundingSphere, childTile: '', geodes: [] };
|
|
106
|
-
|
|
107
|
-
if (version >= 3) {
|
|
108
|
-
pageLod.obb = {
|
|
109
|
-
center: r.vec3f64(),
|
|
110
|
-
xExtent: r.vec3f64(),
|
|
111
|
-
yExtent: r.vec3f64(),
|
|
112
|
-
zExtent: r.vec3f64(),
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// childTile 里可能带 "Geometry" 后缀,官方在切分时去掉
|
|
117
|
-
let childTile = r.string();
|
|
118
|
-
const gi = childTile.indexOf('Geometry');
|
|
119
|
-
if (gi !== -1) childTile = childTile.substring(0, gi);
|
|
120
|
-
pageLod.childTile = childTile;
|
|
121
|
-
|
|
122
|
-
const geodeCount = r.u32();
|
|
123
|
-
for (let i = 0; i < geodeCount; i++) pageLod.geodes.push(parseGeode(r));
|
|
124
|
-
|
|
125
|
-
if (version >= 3) r.string(); // 动画 JSON,暂不解析
|
|
126
|
-
|
|
127
|
-
return pageLod;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function parseGroupNode(r: BinaryReader, version: number): GroupNode {
|
|
131
|
-
r.u32(); // streamSize
|
|
132
|
-
const count = r.u32();
|
|
133
|
-
const pageLods: PageLOD[] = [];
|
|
134
|
-
for (let i = 0; i < count; i++) pageLods.push(parsePageLOD(r, version));
|
|
135
|
-
r.align4();
|
|
136
|
-
return { pageLods };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/* ------------------------------------------------------------ 顶点数据包 */
|
|
140
|
-
|
|
141
|
-
function parseVertex(r: BinaryReader, vp: VertexPackage): void {
|
|
142
|
-
const count = r.u32();
|
|
143
|
-
vp.verticesCount = count;
|
|
144
|
-
if (count === 0) return;
|
|
145
|
-
const dim = r.u16();
|
|
146
|
-
r.u16(); // stride 字段不可信,官方也忽略它
|
|
147
|
-
pushAttr(vp, 'position', r.bytes(count * dim * 4), dim, dim * 4);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function parseNormal(r: BinaryReader, vp: VertexPackage): void {
|
|
151
|
-
const count = r.u32();
|
|
152
|
-
if (count <= 0) return;
|
|
153
|
-
const dim = r.u16();
|
|
154
|
-
const stride = r.u16();
|
|
155
|
-
pushAttr(vp, 'normal', r.bytes(count * dim * 4), dim, stride || dim * 4);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function parseVertexColor(r: BinaryReader, vp: VertexPackage): void {
|
|
159
|
-
const count = r.u32();
|
|
160
|
-
let data: Uint8Array;
|
|
161
|
-
if (count > 0) {
|
|
162
|
-
r.u16(); // stride
|
|
163
|
-
r.skip(2);
|
|
164
|
-
data = r.bytes(count * 4);
|
|
165
|
-
} else {
|
|
166
|
-
// 缺省白色,保证 shader 拿得到 aColor
|
|
167
|
-
data = new Uint8Array((vp.verticesCount || 0) * 4).fill(255);
|
|
168
|
-
}
|
|
169
|
-
pushAttr(vp, 'color', data, 4, 4, 0, undefined, 'u8');
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function parseSecondColor(r: BinaryReader, vp: VertexPackage): void {
|
|
173
|
-
const count = r.u32();
|
|
174
|
-
if (count <= 0) return;
|
|
175
|
-
r.u16();
|
|
176
|
-
r.skip(2);
|
|
177
|
-
pushAttr(vp, 'secondColor', r.bytes(count * 4), 4, 4, 0, undefined, 'u8');
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function parseTexCoord(r: BinaryReader, vp: VertexPackage): void {
|
|
181
|
-
const count = r.u16();
|
|
182
|
-
r.skip(2);
|
|
183
|
-
for (let i = 0; i < count; i++) {
|
|
184
|
-
const n = r.u32();
|
|
185
|
-
const dim = r.u16();
|
|
186
|
-
const stride = r.u16();
|
|
187
|
-
pushAttr(vp, i === 0 ? 'uv' : `uv${i + 1}`, r.bytes(n * dim * 4), dim, stride || dim * 4);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* 实例化信息。官方按 dimension 分派:
|
|
193
|
-
* 16 → 每实例一个 4x4 矩阵(另有 2/4 字节 per-instance 前缀,v3 与 v1/v2 不同)
|
|
194
|
-
* 17/29 → 每实例一个矩阵 + 颜色
|
|
195
|
-
* 其余情况按 dimension * count * 4 字节整体跳过。
|
|
196
|
-
*/
|
|
197
|
-
function parseInstanceInfo(r: BinaryReader, vp: VertexPackage, version: number): void {
|
|
198
|
-
const count = r.u16();
|
|
199
|
-
r.skip(2);
|
|
200
|
-
|
|
201
|
-
for (let i = 0; i < count; i++) {
|
|
202
|
-
const instanceCount = r.u32();
|
|
203
|
-
const dim = r.u16();
|
|
204
|
-
|
|
205
|
-
if (dim === 16) {
|
|
206
|
-
r.offset -= 2; // 官方此处回退 2 字节,perInstance 前缀直接计入行长
|
|
207
|
-
const prefix = version === 3 ? 2 : 4;
|
|
208
|
-
const rowLen = dim * 4 + prefix;
|
|
209
|
-
const total = instanceCount * rowLen;
|
|
210
|
-
const raw = r.bytes(total);
|
|
211
|
-
const clean = new Uint8Array(instanceCount * dim * 4);
|
|
212
|
-
for (let k = 0; k < instanceCount; k++) {
|
|
213
|
-
clean.set(raw.subarray(k * rowLen + prefix, k * rowLen + rowLen), k * dim * 4);
|
|
214
|
-
}
|
|
215
|
-
vp.instanceCount = instanceCount;
|
|
216
|
-
vp.instanceMode = dim;
|
|
217
|
-
vp.instanceBuffer = clean;
|
|
218
|
-
vp.instanceIndex = 1;
|
|
219
|
-
const byteStride = 64;
|
|
220
|
-
pushAttr(vp, 'instanceMatrix0', clean, 4, byteStride, 0, 1);
|
|
221
|
-
pushAttr(vp, 'instanceMatrix1', clean, 4, byteStride, 16, 1);
|
|
222
|
-
pushAttr(vp, 'instanceMatrix2', clean, 4, byteStride, 32, 1);
|
|
223
|
-
pushAttr(vp, 'instanceColor', clean, 4, byteStride, 48, 1);
|
|
224
|
-
} else {
|
|
225
|
-
r.u16(); // stride
|
|
226
|
-
const total = instanceCount * dim * 4;
|
|
227
|
-
if (dim === 17 || dim === 29) {
|
|
228
|
-
vp.instanceCount = instanceCount;
|
|
229
|
-
vp.instanceMode = dim;
|
|
230
|
-
vp.instanceBuffer = r.bytesCopy(total);
|
|
231
|
-
vp.instanceIndex = 1;
|
|
232
|
-
} else {
|
|
233
|
-
r.skip(total);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function parseVertexAttrExtension(r: BinaryReader, vp: VertexPackage): void {
|
|
240
|
-
const n = r.u32();
|
|
241
|
-
for (let i = 0; i < n; i++) {
|
|
242
|
-
const count = r.u32();
|
|
243
|
-
const dim = r.u16();
|
|
244
|
-
const type = r.u16();
|
|
245
|
-
const size = AttrTypeSize[type] ?? 4;
|
|
246
|
-
pushAttr(vp, `custom${i}`, r.bytes(count * dim * size), dim, dim * size);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function parseTangent(r: BinaryReader): void {
|
|
251
|
-
const n = r.u32();
|
|
252
|
-
if (n <= 0) return;
|
|
253
|
-
const dim = r.u16();
|
|
254
|
-
r.u16();
|
|
255
|
-
r.skip(n * dim * 4);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function parseStandardSkeleton(r: BinaryReader, vp: VertexPackage, version: number): void {
|
|
259
|
-
if (version >= 3) r.u32(); // streamSize
|
|
260
|
-
|
|
261
|
-
parseVertex(r, vp);
|
|
262
|
-
parseNormal(r, vp);
|
|
263
|
-
parseVertexColor(r, vp);
|
|
264
|
-
if (version < 3) parseSecondColor(r, vp);
|
|
265
|
-
parseTexCoord(r, vp);
|
|
266
|
-
parseInstanceInfo(r, vp, version);
|
|
267
|
-
|
|
268
|
-
if (version >= 3) {
|
|
269
|
-
parseVertexAttrExtension(r, vp);
|
|
270
|
-
const descript = r.string();
|
|
271
|
-
vp.customVertexAttribute = descript ? JSON.parse(descript) : undefined;
|
|
272
|
-
r.align4();
|
|
273
|
-
parseTangent(r);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function parseCompressedSkeleton(r: BinaryReader, vp: VertexPackage, version: number): void {
|
|
278
|
-
const flags = r.u32();
|
|
279
|
-
vp.compressOptions = flags;
|
|
280
|
-
|
|
281
|
-
if (flags & CompressOption.VERTEX) throw new Error('[s3m] 顶点量化压缩(v2 旧式)暂未实现');
|
|
282
|
-
parseVertex(r, vp);
|
|
283
|
-
|
|
284
|
-
if (flags & CompressOption.NORMAL) throw new Error('[s3m] 法线压缩(v2 旧式)暂未实现');
|
|
285
|
-
parseNormal(r, vp);
|
|
286
|
-
|
|
287
|
-
parseVertexColor(r, vp);
|
|
288
|
-
parseSecondColor(r, vp);
|
|
289
|
-
|
|
290
|
-
if (flags & CompressOption.TEXCOORD) throw new Error('[s3m] 纹理坐标压缩(v2 旧式)暂未实现');
|
|
291
|
-
parseTexCoord(r, vp);
|
|
292
|
-
|
|
293
|
-
if (flags & CompressOption.TEXCOORD_IS_W) vp.textureCoordIsW = true;
|
|
294
|
-
parseInstanceInfo(r, vp, version);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/* ---------------------------------------------------------------- 索引包 */
|
|
298
|
-
|
|
299
|
-
function parseIndexPackages(r: BinaryReader, version: number): IndexPackage[] {
|
|
300
|
-
const count = r.u32();
|
|
301
|
-
const packs: IndexPackage[] = [];
|
|
302
|
-
|
|
303
|
-
for (let i = 0; i < count; i++) {
|
|
304
|
-
if (version >= 3) r.u32(); // streamSize
|
|
305
|
-
|
|
306
|
-
const indexCount = r.u32();
|
|
307
|
-
const indexType = r.u8();
|
|
308
|
-
const useIndex = r.u8();
|
|
309
|
-
const primitiveType = r.u8();
|
|
310
|
-
r.skip(1);
|
|
311
|
-
|
|
312
|
-
let indices: Uint8Array | null = null;
|
|
313
|
-
if (indexCount > 0) {
|
|
314
|
-
const wide = indexType === 1 || indexType === 3;
|
|
315
|
-
const size = wide ? 4 : 2;
|
|
316
|
-
indices = r.bytes(indexCount * size);
|
|
317
|
-
if (!wide && indexCount % 2 !== 0) r.skip(2); // 16 位奇数索引补齐
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const passNames: string[] = [];
|
|
321
|
-
const passCount = r.u32();
|
|
322
|
-
for (let j = 0; j < passCount; j++) passNames.push(r.string());
|
|
323
|
-
|
|
324
|
-
r.align4();
|
|
325
|
-
|
|
326
|
-
packs.push({ indexCount, indexType, useIndex, primitiveType, indices, passNames });
|
|
327
|
-
}
|
|
328
|
-
return packs;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/* ------------------------------------------------------------------ 骨架 */
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* v3 骨架末尾的包围盒。
|
|
335
|
-
*
|
|
336
|
-
* v3.01 的实际数据在索引包之后、包围盒之前多了一个 4 字节字段(实测恒为 0,
|
|
337
|
-
* 推测是线框几何的长度)。规范里没有写,标准骨架是否也有无法验证,
|
|
338
|
-
* 所以这里做一次「读一遍看像不像包围盒」的判断,不像就跳 4 字节重读。
|
|
339
|
-
*/
|
|
340
|
-
function readV3BoundingBox(r: BinaryReader): void {
|
|
341
|
-
const start = r.offset;
|
|
342
|
-
const box = r.boundingBox();
|
|
343
|
-
if (isPlausibleBox(box)) return;
|
|
344
|
-
r.offset = start + 4;
|
|
345
|
-
r.boundingBox();
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function isPlausibleBox(box: { center: Vec3; xExtent: Vec3; yExtent: Vec3; zExtent: Vec3 }): boolean {
|
|
349
|
-
const all = [box.center, box.xExtent, box.yExtent, box.zExtent];
|
|
350
|
-
for (const v of all) {
|
|
351
|
-
if (!Number.isFinite(v.x) || !Number.isFinite(v.y) || !Number.isFinite(v.z)) return false;
|
|
352
|
-
if (Math.abs(v.x) > 1e7 || Math.abs(v.y) > 1e7 || Math.abs(v.z) > 1e7) return false;
|
|
353
|
-
}
|
|
354
|
-
const len = (v: Vec3): number => Math.hypot(v.x, v.y, v.z);
|
|
355
|
-
return len(box.xExtent) > 1e-6 && len(box.yExtent) > 1e-6 && len(box.zExtent) > 1e-6;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
async function parseSkeletons(
|
|
359
|
-
r: BinaryReader,
|
|
360
|
-
version: number,
|
|
361
|
-
trace?: ParseOptions['trace']
|
|
362
|
-
): Promise<Record<string, GeoPackageEntry>> {
|
|
363
|
-
r.u32(); // streamSize
|
|
364
|
-
const count = r.u32();
|
|
365
|
-
const geoPackage: Record<string, GeoPackageEntry> = {};
|
|
366
|
-
|
|
367
|
-
for (let i = 0; i < count; i++) {
|
|
368
|
-
if (version >= 3) r.u32(); // skeletonStreamSize
|
|
369
|
-
|
|
370
|
-
const name = r.string();
|
|
371
|
-
r.align4();
|
|
372
|
-
|
|
373
|
-
let tag: number = r.u32();
|
|
374
|
-
if (version >= 3) {
|
|
375
|
-
tag =
|
|
376
|
-
tag === VertexTagV3.STANDARD ? VertexTag.STANDARD
|
|
377
|
-
: tag === VertexTagV3.DRACO ? VertexTag.DRACO
|
|
378
|
-
: tag === VertexTagV3.MESHOPT ? VertexTag.COMPRESSED
|
|
379
|
-
: VertexTag.UNKNOWN;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
const vp = newVertexPackage();
|
|
383
|
-
let indexPackages: IndexPackage[] = [];
|
|
384
|
-
|
|
385
|
-
if (tag === VertexTag.STANDARD) {
|
|
386
|
-
parseStandardSkeleton(r, vp, version);
|
|
387
|
-
indexPackages = parseIndexPackages(r, version);
|
|
388
|
-
} else if (tag === VertexTag.COMPRESSED) {
|
|
389
|
-
if (version >= 3) {
|
|
390
|
-
await parseMeshOptSkeleton(r, vp);
|
|
391
|
-
indexPackages = await parseMeshOptIndexPackages(r);
|
|
392
|
-
} else {
|
|
393
|
-
parseCompressedSkeleton(r, vp, version);
|
|
394
|
-
indexPackages = parseIndexPackages(r, version);
|
|
395
|
-
}
|
|
396
|
-
} else if (tag === VertexTag.DRACO) {
|
|
397
|
-
throw new Error(`[s3m] 骨架 "${name}" 使用 Draco 几何压缩,需接入 draco3d 解码器`);
|
|
398
|
-
} else {
|
|
399
|
-
throw new Error(`[s3m] 未知骨架标记 ${tag}(${name})`);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
if (version >= 3) readV3BoundingBox(r);
|
|
403
|
-
trace?.(' skel', name, r.offset);
|
|
404
|
-
|
|
405
|
-
geoPackage[name] = { name, vertexPackage: vp, indexPackages };
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// v1/v2 在骨架数组之后还有一段附加流。官方写法是
|
|
409
|
-
// bytesOffset += secColorSize; bytesOffset += 4;
|
|
410
|
-
// 其中读取 secColorSize 时不推进指针,等价于「起点 + 长度 + 4」。
|
|
411
|
-
// 此时 r.offset 已在长度字段之后,所以只补长度即可。
|
|
412
|
-
if (version < 3) {
|
|
413
|
-
const extra = r.u32();
|
|
414
|
-
r.skip(extra);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
return geoPackage;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/* ------------------------------------------------------------------ 纹理 */
|
|
421
|
-
|
|
422
|
-
function parseTextures(r: BinaryReader): Record<string, S3MTexture> {
|
|
423
|
-
const streamSize = r.u32();
|
|
424
|
-
const start = r.offset;
|
|
425
|
-
const end = start + streamSize;
|
|
426
|
-
|
|
427
|
-
const count = r.u32();
|
|
428
|
-
const textures: Record<string, S3MTexture> = {};
|
|
429
|
-
|
|
430
|
-
for (let i = 0; i < count; i++) {
|
|
431
|
-
const code = r.string();
|
|
432
|
-
r.align4(start);
|
|
433
|
-
|
|
434
|
-
const level = r.u32();
|
|
435
|
-
const width = r.u32();
|
|
436
|
-
const height = r.u32();
|
|
437
|
-
const compressType = r.u32();
|
|
438
|
-
const dataSize = r.u32();
|
|
439
|
-
const pixelFormat = r.u32();
|
|
440
|
-
const data = r.bytesCopy(dataSize);
|
|
441
|
-
|
|
442
|
-
textures[code] = { code, level, width, height, compressType, pixelFormat, data };
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
r.offset = end;
|
|
446
|
-
return textures;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
/* ------------------------------------------------------------ 材质与拾取 */
|
|
450
|
-
|
|
451
|
-
function parseMaterials(r: BinaryReader): unknown {
|
|
452
|
-
const len = r.u32();
|
|
453
|
-
const str = new TextDecoder('utf-8').decode(r.bytes(len));
|
|
454
|
-
return str ? JSON.parse(str) : [];
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* 单体化信息。非实例化数据里每个单体对应一段连续的顶点区间,
|
|
459
|
-
* 这里把它展开成逐顶点的 batchId 属性,渲染层可直接做 GPU picking。
|
|
460
|
-
*/
|
|
461
|
-
function parsePickInfo(
|
|
462
|
-
r: BinaryReader,
|
|
463
|
-
options: number,
|
|
464
|
-
geoPackage: Record<string, GeoPackageEntry>,
|
|
465
|
-
version: number
|
|
466
|
-
): Record<string, unknown> | null {
|
|
467
|
-
let flags = options;
|
|
468
|
-
if ((flags & 1) !== 1) return null;
|
|
469
|
-
|
|
470
|
-
if (version >= 3) flags = r.u32();
|
|
471
|
-
if ((flags & 1) !== 1) return null;
|
|
472
|
-
|
|
473
|
-
r.u32(); // streamSize
|
|
474
|
-
const count = r.u32();
|
|
475
|
-
const result: Record<string, unknown> = {};
|
|
476
|
-
|
|
477
|
-
for (let i = 0; i < count; i++) {
|
|
478
|
-
const geometryName = r.string();
|
|
479
|
-
const geo = geoPackage[geometryName];
|
|
480
|
-
const selectCount = r.u32();
|
|
481
|
-
const pickInfo: Record<number, PickEntry> = {};
|
|
482
|
-
if (!geo) continue;
|
|
483
|
-
|
|
484
|
-
const vp = geo.vertexPackage;
|
|
485
|
-
const instanced = vp.instanceIndex !== -1;
|
|
486
|
-
|
|
487
|
-
if (!instanced) {
|
|
488
|
-
const batchIds = new Float32Array(vp.verticesCount || 0);
|
|
489
|
-
for (let j = 0; j < selectCount; j++) {
|
|
490
|
-
const id = r.u32();
|
|
491
|
-
const segCount = r.u32();
|
|
492
|
-
const entry: PickEntry = { batchId: j, segments: [] };
|
|
493
|
-
pickInfo[id] = entry;
|
|
494
|
-
for (let k = 0; k < segCount; k++) {
|
|
495
|
-
const vertexOffset = r.u32();
|
|
496
|
-
const vertexCount = r.u32();
|
|
497
|
-
entry.segments!.push({ vertexOffset, vertexCount });
|
|
498
|
-
batchIds.fill(j, vertexOffset, vertexOffset + vertexCount);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
pushAttr(vp, 'batchId', new Uint8Array(batchIds.buffer), 1, 4, 0, undefined, 'f32');
|
|
502
|
-
} else {
|
|
503
|
-
const instanceCount = vp.instanceCount;
|
|
504
|
-
const instanceBatchIds = new Float32Array(instanceCount).fill(-1);
|
|
505
|
-
for (let j = 0; j < selectCount; j++) {
|
|
506
|
-
const id = r.u32();
|
|
507
|
-
const n = r.u32();
|
|
508
|
-
const entry: PickEntry = { instanceIds: [], instanceBatchId: j };
|
|
509
|
-
pickInfo[id] = entry;
|
|
510
|
-
for (let k = 0; k < n; k++) {
|
|
511
|
-
const instanceId = r.u32();
|
|
512
|
-
if (version >= 3) r.u32();
|
|
513
|
-
if (instanceId >= 0 && instanceId < instanceCount) {
|
|
514
|
-
entry.instanceIds!.push(instanceId);
|
|
515
|
-
instanceBatchIds[instanceId] = j;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
// 实例化的单体粒度是「逐实例」的,不能塞进逐顶点属性:
|
|
520
|
-
// 顶点数(base mesh)和实例数根本不是一个量级,长度对不上会被 three 当成坏 buffer。
|
|
521
|
-
vp.instanceBatchIds = instanceBatchIds;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
geo.pickInfo = pickInfo;
|
|
525
|
-
result[geometryName] = pickInfo;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
return result;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
/* ------------------------------------------------------------------ 入口 */
|
|
532
|
-
|
|
533
|
-
export async function parseS3MB(arrayBuffer: ArrayBuffer, options: ParseOptions = {}): Promise<ParsedTile> {
|
|
534
|
-
const inflate = options.inflate ?? defaultInflate;
|
|
535
|
-
const trace = options.trace;
|
|
536
|
-
const r = new BinaryReader(arrayBuffer);
|
|
537
|
-
|
|
538
|
-
const version = Number(r.f32().toFixed(2));
|
|
539
|
-
|
|
540
|
-
let compressedType = 0;
|
|
541
|
-
if (version >= 3) compressedType = r.u32();
|
|
542
|
-
if (version >= 2) r.u32(); // uncompressedSize
|
|
543
|
-
const compressedSize = r.u32();
|
|
544
|
-
|
|
545
|
-
const payload = new Uint8Array(arrayBuffer, r.base + r.offset, compressedSize);
|
|
546
|
-
|
|
547
|
-
let raw: Uint8Array;
|
|
548
|
-
if (version >= 3 && compressedType === 0) {
|
|
549
|
-
raw = payload;
|
|
550
|
-
} else {
|
|
551
|
-
raw = await inflate(payload);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
const t = new BinaryReader(raw.buffer as ArrayBuffer, raw.byteOffset ?? 0, raw.byteLength);
|
|
555
|
-
|
|
556
|
-
const fileOptions = t.u32();
|
|
557
|
-
trace?.('fileOptions', fileOptions, t.offset);
|
|
558
|
-
const groupNode = parseGroupNode(t, version);
|
|
559
|
-
trace?.('groupNode', groupNode.pageLods.length, t.offset);
|
|
560
|
-
const geoPackage = await parseSkeletons(t, version, trace);
|
|
561
|
-
trace?.('skeletons', Object.keys(geoPackage).length, t.offset);
|
|
562
|
-
const textures = parseTextures(t);
|
|
563
|
-
trace?.('textures', Object.keys(textures).length, t.offset);
|
|
564
|
-
const materials = parseMaterials(t);
|
|
565
|
-
trace?.('materials', t.offset);
|
|
566
|
-
const pickInfo = parsePickInfo(t, fileOptions, geoPackage, version);
|
|
567
|
-
trace?.('pickInfo', t.offset);
|
|
568
|
-
|
|
569
|
-
let extensions: unknown;
|
|
570
|
-
if (version >= 3) {
|
|
571
|
-
const s = t.string();
|
|
572
|
-
extensions = s ? JSON.parse(s) : undefined;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
return {
|
|
576
|
-
version,
|
|
577
|
-
compressedType,
|
|
578
|
-
options: fileOptions,
|
|
579
|
-
groupNode,
|
|
580
|
-
geoPackage,
|
|
581
|
-
textures,
|
|
582
|
-
materials,
|
|
583
|
-
pickInfo,
|
|
584
|
-
extensions,
|
|
585
|
-
bytesRead: t.offset,
|
|
586
|
-
bytesTotal: t.view.byteLength,
|
|
587
|
-
};
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
export type { VertexAttribute };
|
|
1
|
+
/**
|
|
2
|
+
* S3MB 瓦片解析器(S3M 1.0 / 2.0 / 3.0)
|
|
3
|
+
*
|
|
4
|
+
* 实现依据:SuperMap/s3m-spec 官方 S3M_SDK/S3M_JS/S3M_module/S3MParser/S3ModelParser.js
|
|
5
|
+
* (Apache-2.0)+ Specification/S3MB/S3MBBinary.md。
|
|
6
|
+
*
|
|
7
|
+
* 与官方实现的差异:
|
|
8
|
+
* 1. 不依赖 Cesium —— 输出纯数据对象,几何上传交给渲染层。
|
|
9
|
+
* 2. 修复了官方 v3 非压缩分支把整个 ArrayBuffer(含文件头)当数据流的缺陷。
|
|
10
|
+
* 3. Draco / MeshOpt 几何压缩只做识别,不做解码(需要额外的 wasm 解码器)。
|
|
11
|
+
*/
|
|
12
|
+
import { BinaryReader } from './BinaryReader.js';
|
|
13
|
+
import { parseMeshOptSkeleton, parseMeshOptIndexPackages } from './MeshOpt.js';
|
|
14
|
+
import type {
|
|
15
|
+
AttrDType,
|
|
16
|
+
GeoPackageEntry,
|
|
17
|
+
Geode,
|
|
18
|
+
GroupNode,
|
|
19
|
+
IndexPackage,
|
|
20
|
+
PageLOD,
|
|
21
|
+
ParsedTile,
|
|
22
|
+
PickEntry,
|
|
23
|
+
S3MTexture,
|
|
24
|
+
Vec3,
|
|
25
|
+
VertexAttribute,
|
|
26
|
+
VertexPackage,
|
|
27
|
+
} from './types.js';
|
|
28
|
+
|
|
29
|
+
export const S3M_VERSION = { V1: 1.0, V2: 2.0, V3: 3.0 } as const;
|
|
30
|
+
|
|
31
|
+
export const VertexTag = { UNKNOWN: 0, STANDARD: 1, COMPRESSED: 2, DRACO: 3 } as const;
|
|
32
|
+
export const VertexTagV3 = { STANDARD: 0, DRACO: 1, MESHOPT: 2 } as const;
|
|
33
|
+
|
|
34
|
+
const CompressOption = {
|
|
35
|
+
VERTEX: 1,
|
|
36
|
+
NORMAL: 2,
|
|
37
|
+
VERTEX_COLOR: 4,
|
|
38
|
+
SECOND_COLOR: 8,
|
|
39
|
+
TEXCOORD: 16,
|
|
40
|
+
TEXCOORD_IS_W: 32,
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
const AttrTypeSize: Record<number, number> = { 0: 4, 1: 4, 2: 8, 3: 2 };
|
|
44
|
+
|
|
45
|
+
export type InflateFn = (data: Uint8Array) => Promise<Uint8Array> | Uint8Array;
|
|
46
|
+
|
|
47
|
+
export interface ParseOptions {
|
|
48
|
+
/** 自定义解压实现,默认用 DecompressionStream('deflate');Node 下传 zlib.inflateSync */
|
|
49
|
+
inflate?: InflateFn;
|
|
50
|
+
/** 逐段追踪偏移,调试解析用 */
|
|
51
|
+
trace?: (section: string, detail?: unknown, offset?: number) => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 默认解压:zlib(文件头 78 9C / 78 DA)。浏览器与 Node 18+ 均可用。 */
|
|
55
|
+
async function defaultInflate(bytes: Uint8Array): Promise<Uint8Array> {
|
|
56
|
+
if (typeof DecompressionStream === 'undefined') {
|
|
57
|
+
throw new Error('[s3m] 当前环境无 DecompressionStream,请通过 options.inflate 注入解压实现');
|
|
58
|
+
}
|
|
59
|
+
const stream = new Blob([bytes as BlobPart]).stream().pipeThrough(new DecompressionStream('deflate'));
|
|
60
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function pushAttr(
|
|
64
|
+
vp: VertexPackage,
|
|
65
|
+
semantic: string,
|
|
66
|
+
data: Uint8Array,
|
|
67
|
+
components: number,
|
|
68
|
+
stride: number,
|
|
69
|
+
offset = 0,
|
|
70
|
+
divisor?: number,
|
|
71
|
+
dtype: AttrDType = 'f32'
|
|
72
|
+
): void {
|
|
73
|
+
vp.attrLocation[semantic] = vp.vertexAttributes.length;
|
|
74
|
+
vp.vertexAttributes.push({
|
|
75
|
+
semantic,
|
|
76
|
+
data,
|
|
77
|
+
components,
|
|
78
|
+
stride: stride || components * 4,
|
|
79
|
+
offsetInBytes: offset,
|
|
80
|
+
divisor,
|
|
81
|
+
dtype,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function newVertexPackage(): VertexPackage {
|
|
86
|
+
return { vertexAttributes: [], attrLocation: {}, instanceCount: 0, instanceMode: 0, instanceIndex: -1, verticesCount: 0 };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ---------------------------------------------------------------- LOD 树 */
|
|
90
|
+
|
|
91
|
+
function parseGeode(r: BinaryReader): Geode {
|
|
92
|
+
const matrix: number[] = new Array(16);
|
|
93
|
+
for (let i = 0; i < 16; i++) matrix[i] = r.f64();
|
|
94
|
+
const n = r.u32();
|
|
95
|
+
const skeletonNames: string[] = [];
|
|
96
|
+
for (let i = 0; i < n; i++) skeletonNames.push(r.string());
|
|
97
|
+
return { matrix, skeletonNames };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parsePageLOD(r: BinaryReader, version: number): PageLOD {
|
|
101
|
+
const rangeValue = r.f32();
|
|
102
|
+
const rangeMode = r.u16();
|
|
103
|
+
const boundingSphere = { center: r.vec3f64(), radius: r.f64() };
|
|
104
|
+
|
|
105
|
+
const pageLod: PageLOD = { rangeValue, rangeMode, boundingSphere, childTile: '', geodes: [] };
|
|
106
|
+
|
|
107
|
+
if (version >= 3) {
|
|
108
|
+
pageLod.obb = {
|
|
109
|
+
center: r.vec3f64(),
|
|
110
|
+
xExtent: r.vec3f64(),
|
|
111
|
+
yExtent: r.vec3f64(),
|
|
112
|
+
zExtent: r.vec3f64(),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// childTile 里可能带 "Geometry" 后缀,官方在切分时去掉
|
|
117
|
+
let childTile = r.string();
|
|
118
|
+
const gi = childTile.indexOf('Geometry');
|
|
119
|
+
if (gi !== -1) childTile = childTile.substring(0, gi);
|
|
120
|
+
pageLod.childTile = childTile;
|
|
121
|
+
|
|
122
|
+
const geodeCount = r.u32();
|
|
123
|
+
for (let i = 0; i < geodeCount; i++) pageLod.geodes.push(parseGeode(r));
|
|
124
|
+
|
|
125
|
+
if (version >= 3) r.string(); // 动画 JSON,暂不解析
|
|
126
|
+
|
|
127
|
+
return pageLod;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function parseGroupNode(r: BinaryReader, version: number): GroupNode {
|
|
131
|
+
r.u32(); // streamSize
|
|
132
|
+
const count = r.u32();
|
|
133
|
+
const pageLods: PageLOD[] = [];
|
|
134
|
+
for (let i = 0; i < count; i++) pageLods.push(parsePageLOD(r, version));
|
|
135
|
+
r.align4();
|
|
136
|
+
return { pageLods };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* ------------------------------------------------------------ 顶点数据包 */
|
|
140
|
+
|
|
141
|
+
function parseVertex(r: BinaryReader, vp: VertexPackage): void {
|
|
142
|
+
const count = r.u32();
|
|
143
|
+
vp.verticesCount = count;
|
|
144
|
+
if (count === 0) return;
|
|
145
|
+
const dim = r.u16();
|
|
146
|
+
r.u16(); // stride 字段不可信,官方也忽略它
|
|
147
|
+
pushAttr(vp, 'position', r.bytes(count * dim * 4), dim, dim * 4);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseNormal(r: BinaryReader, vp: VertexPackage): void {
|
|
151
|
+
const count = r.u32();
|
|
152
|
+
if (count <= 0) return;
|
|
153
|
+
const dim = r.u16();
|
|
154
|
+
const stride = r.u16();
|
|
155
|
+
pushAttr(vp, 'normal', r.bytes(count * dim * 4), dim, stride || dim * 4);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function parseVertexColor(r: BinaryReader, vp: VertexPackage): void {
|
|
159
|
+
const count = r.u32();
|
|
160
|
+
let data: Uint8Array;
|
|
161
|
+
if (count > 0) {
|
|
162
|
+
r.u16(); // stride
|
|
163
|
+
r.skip(2);
|
|
164
|
+
data = r.bytes(count * 4);
|
|
165
|
+
} else {
|
|
166
|
+
// 缺省白色,保证 shader 拿得到 aColor
|
|
167
|
+
data = new Uint8Array((vp.verticesCount || 0) * 4).fill(255);
|
|
168
|
+
}
|
|
169
|
+
pushAttr(vp, 'color', data, 4, 4, 0, undefined, 'u8');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function parseSecondColor(r: BinaryReader, vp: VertexPackage): void {
|
|
173
|
+
const count = r.u32();
|
|
174
|
+
if (count <= 0) return;
|
|
175
|
+
r.u16();
|
|
176
|
+
r.skip(2);
|
|
177
|
+
pushAttr(vp, 'secondColor', r.bytes(count * 4), 4, 4, 0, undefined, 'u8');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parseTexCoord(r: BinaryReader, vp: VertexPackage): void {
|
|
181
|
+
const count = r.u16();
|
|
182
|
+
r.skip(2);
|
|
183
|
+
for (let i = 0; i < count; i++) {
|
|
184
|
+
const n = r.u32();
|
|
185
|
+
const dim = r.u16();
|
|
186
|
+
const stride = r.u16();
|
|
187
|
+
pushAttr(vp, i === 0 ? 'uv' : `uv${i + 1}`, r.bytes(n * dim * 4), dim, stride || dim * 4);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 实例化信息。官方按 dimension 分派:
|
|
193
|
+
* 16 → 每实例一个 4x4 矩阵(另有 2/4 字节 per-instance 前缀,v3 与 v1/v2 不同)
|
|
194
|
+
* 17/29 → 每实例一个矩阵 + 颜色
|
|
195
|
+
* 其余情况按 dimension * count * 4 字节整体跳过。
|
|
196
|
+
*/
|
|
197
|
+
function parseInstanceInfo(r: BinaryReader, vp: VertexPackage, version: number): void {
|
|
198
|
+
const count = r.u16();
|
|
199
|
+
r.skip(2);
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < count; i++) {
|
|
202
|
+
const instanceCount = r.u32();
|
|
203
|
+
const dim = r.u16();
|
|
204
|
+
|
|
205
|
+
if (dim === 16) {
|
|
206
|
+
r.offset -= 2; // 官方此处回退 2 字节,perInstance 前缀直接计入行长
|
|
207
|
+
const prefix = version === 3 ? 2 : 4;
|
|
208
|
+
const rowLen = dim * 4 + prefix;
|
|
209
|
+
const total = instanceCount * rowLen;
|
|
210
|
+
const raw = r.bytes(total);
|
|
211
|
+
const clean = new Uint8Array(instanceCount * dim * 4);
|
|
212
|
+
for (let k = 0; k < instanceCount; k++) {
|
|
213
|
+
clean.set(raw.subarray(k * rowLen + prefix, k * rowLen + rowLen), k * dim * 4);
|
|
214
|
+
}
|
|
215
|
+
vp.instanceCount = instanceCount;
|
|
216
|
+
vp.instanceMode = dim;
|
|
217
|
+
vp.instanceBuffer = clean;
|
|
218
|
+
vp.instanceIndex = 1;
|
|
219
|
+
const byteStride = 64;
|
|
220
|
+
pushAttr(vp, 'instanceMatrix0', clean, 4, byteStride, 0, 1);
|
|
221
|
+
pushAttr(vp, 'instanceMatrix1', clean, 4, byteStride, 16, 1);
|
|
222
|
+
pushAttr(vp, 'instanceMatrix2', clean, 4, byteStride, 32, 1);
|
|
223
|
+
pushAttr(vp, 'instanceColor', clean, 4, byteStride, 48, 1);
|
|
224
|
+
} else {
|
|
225
|
+
r.u16(); // stride
|
|
226
|
+
const total = instanceCount * dim * 4;
|
|
227
|
+
if (dim === 17 || dim === 29) {
|
|
228
|
+
vp.instanceCount = instanceCount;
|
|
229
|
+
vp.instanceMode = dim;
|
|
230
|
+
vp.instanceBuffer = r.bytesCopy(total);
|
|
231
|
+
vp.instanceIndex = 1;
|
|
232
|
+
} else {
|
|
233
|
+
r.skip(total);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function parseVertexAttrExtension(r: BinaryReader, vp: VertexPackage): void {
|
|
240
|
+
const n = r.u32();
|
|
241
|
+
for (let i = 0; i < n; i++) {
|
|
242
|
+
const count = r.u32();
|
|
243
|
+
const dim = r.u16();
|
|
244
|
+
const type = r.u16();
|
|
245
|
+
const size = AttrTypeSize[type] ?? 4;
|
|
246
|
+
pushAttr(vp, `custom${i}`, r.bytes(count * dim * size), dim, dim * size);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function parseTangent(r: BinaryReader): void {
|
|
251
|
+
const n = r.u32();
|
|
252
|
+
if (n <= 0) return;
|
|
253
|
+
const dim = r.u16();
|
|
254
|
+
r.u16();
|
|
255
|
+
r.skip(n * dim * 4);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function parseStandardSkeleton(r: BinaryReader, vp: VertexPackage, version: number): void {
|
|
259
|
+
if (version >= 3) r.u32(); // streamSize
|
|
260
|
+
|
|
261
|
+
parseVertex(r, vp);
|
|
262
|
+
parseNormal(r, vp);
|
|
263
|
+
parseVertexColor(r, vp);
|
|
264
|
+
if (version < 3) parseSecondColor(r, vp);
|
|
265
|
+
parseTexCoord(r, vp);
|
|
266
|
+
parseInstanceInfo(r, vp, version);
|
|
267
|
+
|
|
268
|
+
if (version >= 3) {
|
|
269
|
+
parseVertexAttrExtension(r, vp);
|
|
270
|
+
const descript = r.string();
|
|
271
|
+
vp.customVertexAttribute = descript ? JSON.parse(descript) : undefined;
|
|
272
|
+
r.align4();
|
|
273
|
+
parseTangent(r);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function parseCompressedSkeleton(r: BinaryReader, vp: VertexPackage, version: number): void {
|
|
278
|
+
const flags = r.u32();
|
|
279
|
+
vp.compressOptions = flags;
|
|
280
|
+
|
|
281
|
+
if (flags & CompressOption.VERTEX) throw new Error('[s3m] 顶点量化压缩(v2 旧式)暂未实现');
|
|
282
|
+
parseVertex(r, vp);
|
|
283
|
+
|
|
284
|
+
if (flags & CompressOption.NORMAL) throw new Error('[s3m] 法线压缩(v2 旧式)暂未实现');
|
|
285
|
+
parseNormal(r, vp);
|
|
286
|
+
|
|
287
|
+
parseVertexColor(r, vp);
|
|
288
|
+
parseSecondColor(r, vp);
|
|
289
|
+
|
|
290
|
+
if (flags & CompressOption.TEXCOORD) throw new Error('[s3m] 纹理坐标压缩(v2 旧式)暂未实现');
|
|
291
|
+
parseTexCoord(r, vp);
|
|
292
|
+
|
|
293
|
+
if (flags & CompressOption.TEXCOORD_IS_W) vp.textureCoordIsW = true;
|
|
294
|
+
parseInstanceInfo(r, vp, version);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* ---------------------------------------------------------------- 索引包 */
|
|
298
|
+
|
|
299
|
+
function parseIndexPackages(r: BinaryReader, version: number): IndexPackage[] {
|
|
300
|
+
const count = r.u32();
|
|
301
|
+
const packs: IndexPackage[] = [];
|
|
302
|
+
|
|
303
|
+
for (let i = 0; i < count; i++) {
|
|
304
|
+
if (version >= 3) r.u32(); // streamSize
|
|
305
|
+
|
|
306
|
+
const indexCount = r.u32();
|
|
307
|
+
const indexType = r.u8();
|
|
308
|
+
const useIndex = r.u8();
|
|
309
|
+
const primitiveType = r.u8();
|
|
310
|
+
r.skip(1);
|
|
311
|
+
|
|
312
|
+
let indices: Uint8Array | null = null;
|
|
313
|
+
if (indexCount > 0) {
|
|
314
|
+
const wide = indexType === 1 || indexType === 3;
|
|
315
|
+
const size = wide ? 4 : 2;
|
|
316
|
+
indices = r.bytes(indexCount * size);
|
|
317
|
+
if (!wide && indexCount % 2 !== 0) r.skip(2); // 16 位奇数索引补齐
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const passNames: string[] = [];
|
|
321
|
+
const passCount = r.u32();
|
|
322
|
+
for (let j = 0; j < passCount; j++) passNames.push(r.string());
|
|
323
|
+
|
|
324
|
+
r.align4();
|
|
325
|
+
|
|
326
|
+
packs.push({ indexCount, indexType, useIndex, primitiveType, indices, passNames });
|
|
327
|
+
}
|
|
328
|
+
return packs;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/* ------------------------------------------------------------------ 骨架 */
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* v3 骨架末尾的包围盒。
|
|
335
|
+
*
|
|
336
|
+
* v3.01 的实际数据在索引包之后、包围盒之前多了一个 4 字节字段(实测恒为 0,
|
|
337
|
+
* 推测是线框几何的长度)。规范里没有写,标准骨架是否也有无法验证,
|
|
338
|
+
* 所以这里做一次「读一遍看像不像包围盒」的判断,不像就跳 4 字节重读。
|
|
339
|
+
*/
|
|
340
|
+
function readV3BoundingBox(r: BinaryReader): void {
|
|
341
|
+
const start = r.offset;
|
|
342
|
+
const box = r.boundingBox();
|
|
343
|
+
if (isPlausibleBox(box)) return;
|
|
344
|
+
r.offset = start + 4;
|
|
345
|
+
r.boundingBox();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function isPlausibleBox(box: { center: Vec3; xExtent: Vec3; yExtent: Vec3; zExtent: Vec3 }): boolean {
|
|
349
|
+
const all = [box.center, box.xExtent, box.yExtent, box.zExtent];
|
|
350
|
+
for (const v of all) {
|
|
351
|
+
if (!Number.isFinite(v.x) || !Number.isFinite(v.y) || !Number.isFinite(v.z)) return false;
|
|
352
|
+
if (Math.abs(v.x) > 1e7 || Math.abs(v.y) > 1e7 || Math.abs(v.z) > 1e7) return false;
|
|
353
|
+
}
|
|
354
|
+
const len = (v: Vec3): number => Math.hypot(v.x, v.y, v.z);
|
|
355
|
+
return len(box.xExtent) > 1e-6 && len(box.yExtent) > 1e-6 && len(box.zExtent) > 1e-6;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function parseSkeletons(
|
|
359
|
+
r: BinaryReader,
|
|
360
|
+
version: number,
|
|
361
|
+
trace?: ParseOptions['trace']
|
|
362
|
+
): Promise<Record<string, GeoPackageEntry>> {
|
|
363
|
+
r.u32(); // streamSize
|
|
364
|
+
const count = r.u32();
|
|
365
|
+
const geoPackage: Record<string, GeoPackageEntry> = {};
|
|
366
|
+
|
|
367
|
+
for (let i = 0; i < count; i++) {
|
|
368
|
+
if (version >= 3) r.u32(); // skeletonStreamSize
|
|
369
|
+
|
|
370
|
+
const name = r.string();
|
|
371
|
+
r.align4();
|
|
372
|
+
|
|
373
|
+
let tag: number = r.u32();
|
|
374
|
+
if (version >= 3) {
|
|
375
|
+
tag =
|
|
376
|
+
tag === VertexTagV3.STANDARD ? VertexTag.STANDARD
|
|
377
|
+
: tag === VertexTagV3.DRACO ? VertexTag.DRACO
|
|
378
|
+
: tag === VertexTagV3.MESHOPT ? VertexTag.COMPRESSED
|
|
379
|
+
: VertexTag.UNKNOWN;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const vp = newVertexPackage();
|
|
383
|
+
let indexPackages: IndexPackage[] = [];
|
|
384
|
+
|
|
385
|
+
if (tag === VertexTag.STANDARD) {
|
|
386
|
+
parseStandardSkeleton(r, vp, version);
|
|
387
|
+
indexPackages = parseIndexPackages(r, version);
|
|
388
|
+
} else if (tag === VertexTag.COMPRESSED) {
|
|
389
|
+
if (version >= 3) {
|
|
390
|
+
await parseMeshOptSkeleton(r, vp);
|
|
391
|
+
indexPackages = await parseMeshOptIndexPackages(r);
|
|
392
|
+
} else {
|
|
393
|
+
parseCompressedSkeleton(r, vp, version);
|
|
394
|
+
indexPackages = parseIndexPackages(r, version);
|
|
395
|
+
}
|
|
396
|
+
} else if (tag === VertexTag.DRACO) {
|
|
397
|
+
throw new Error(`[s3m] 骨架 "${name}" 使用 Draco 几何压缩,需接入 draco3d 解码器`);
|
|
398
|
+
} else {
|
|
399
|
+
throw new Error(`[s3m] 未知骨架标记 ${tag}(${name})`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (version >= 3) readV3BoundingBox(r);
|
|
403
|
+
trace?.(' skel', name, r.offset);
|
|
404
|
+
|
|
405
|
+
geoPackage[name] = { name, vertexPackage: vp, indexPackages };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// v1/v2 在骨架数组之后还有一段附加流。官方写法是
|
|
409
|
+
// bytesOffset += secColorSize; bytesOffset += 4;
|
|
410
|
+
// 其中读取 secColorSize 时不推进指针,等价于「起点 + 长度 + 4」。
|
|
411
|
+
// 此时 r.offset 已在长度字段之后,所以只补长度即可。
|
|
412
|
+
if (version < 3) {
|
|
413
|
+
const extra = r.u32();
|
|
414
|
+
r.skip(extra);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return geoPackage;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/* ------------------------------------------------------------------ 纹理 */
|
|
421
|
+
|
|
422
|
+
function parseTextures(r: BinaryReader): Record<string, S3MTexture> {
|
|
423
|
+
const streamSize = r.u32();
|
|
424
|
+
const start = r.offset;
|
|
425
|
+
const end = start + streamSize;
|
|
426
|
+
|
|
427
|
+
const count = r.u32();
|
|
428
|
+
const textures: Record<string, S3MTexture> = {};
|
|
429
|
+
|
|
430
|
+
for (let i = 0; i < count; i++) {
|
|
431
|
+
const code = r.string();
|
|
432
|
+
r.align4(start);
|
|
433
|
+
|
|
434
|
+
const level = r.u32();
|
|
435
|
+
const width = r.u32();
|
|
436
|
+
const height = r.u32();
|
|
437
|
+
const compressType = r.u32();
|
|
438
|
+
const dataSize = r.u32();
|
|
439
|
+
const pixelFormat = r.u32();
|
|
440
|
+
const data = r.bytesCopy(dataSize);
|
|
441
|
+
|
|
442
|
+
textures[code] = { code, level, width, height, compressType, pixelFormat, data };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
r.offset = end;
|
|
446
|
+
return textures;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/* ------------------------------------------------------------ 材质与拾取 */
|
|
450
|
+
|
|
451
|
+
function parseMaterials(r: BinaryReader): unknown {
|
|
452
|
+
const len = r.u32();
|
|
453
|
+
const str = new TextDecoder('utf-8').decode(r.bytes(len));
|
|
454
|
+
return str ? JSON.parse(str) : [];
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* 单体化信息。非实例化数据里每个单体对应一段连续的顶点区间,
|
|
459
|
+
* 这里把它展开成逐顶点的 batchId 属性,渲染层可直接做 GPU picking。
|
|
460
|
+
*/
|
|
461
|
+
function parsePickInfo(
|
|
462
|
+
r: BinaryReader,
|
|
463
|
+
options: number,
|
|
464
|
+
geoPackage: Record<string, GeoPackageEntry>,
|
|
465
|
+
version: number
|
|
466
|
+
): Record<string, unknown> | null {
|
|
467
|
+
let flags = options;
|
|
468
|
+
if ((flags & 1) !== 1) return null;
|
|
469
|
+
|
|
470
|
+
if (version >= 3) flags = r.u32();
|
|
471
|
+
if ((flags & 1) !== 1) return null;
|
|
472
|
+
|
|
473
|
+
r.u32(); // streamSize
|
|
474
|
+
const count = r.u32();
|
|
475
|
+
const result: Record<string, unknown> = {};
|
|
476
|
+
|
|
477
|
+
for (let i = 0; i < count; i++) {
|
|
478
|
+
const geometryName = r.string();
|
|
479
|
+
const geo = geoPackage[geometryName];
|
|
480
|
+
const selectCount = r.u32();
|
|
481
|
+
const pickInfo: Record<number, PickEntry> = {};
|
|
482
|
+
if (!geo) continue;
|
|
483
|
+
|
|
484
|
+
const vp = geo.vertexPackage;
|
|
485
|
+
const instanced = vp.instanceIndex !== -1;
|
|
486
|
+
|
|
487
|
+
if (!instanced) {
|
|
488
|
+
const batchIds = new Float32Array(vp.verticesCount || 0);
|
|
489
|
+
for (let j = 0; j < selectCount; j++) {
|
|
490
|
+
const id = r.u32();
|
|
491
|
+
const segCount = r.u32();
|
|
492
|
+
const entry: PickEntry = { batchId: j, segments: [] };
|
|
493
|
+
pickInfo[id] = entry;
|
|
494
|
+
for (let k = 0; k < segCount; k++) {
|
|
495
|
+
const vertexOffset = r.u32();
|
|
496
|
+
const vertexCount = r.u32();
|
|
497
|
+
entry.segments!.push({ vertexOffset, vertexCount });
|
|
498
|
+
batchIds.fill(j, vertexOffset, vertexOffset + vertexCount);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
pushAttr(vp, 'batchId', new Uint8Array(batchIds.buffer), 1, 4, 0, undefined, 'f32');
|
|
502
|
+
} else {
|
|
503
|
+
const instanceCount = vp.instanceCount;
|
|
504
|
+
const instanceBatchIds = new Float32Array(instanceCount).fill(-1);
|
|
505
|
+
for (let j = 0; j < selectCount; j++) {
|
|
506
|
+
const id = r.u32();
|
|
507
|
+
const n = r.u32();
|
|
508
|
+
const entry: PickEntry = { instanceIds: [], instanceBatchId: j };
|
|
509
|
+
pickInfo[id] = entry;
|
|
510
|
+
for (let k = 0; k < n; k++) {
|
|
511
|
+
const instanceId = r.u32();
|
|
512
|
+
if (version >= 3) r.u32();
|
|
513
|
+
if (instanceId >= 0 && instanceId < instanceCount) {
|
|
514
|
+
entry.instanceIds!.push(instanceId);
|
|
515
|
+
instanceBatchIds[instanceId] = j;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
// 实例化的单体粒度是「逐实例」的,不能塞进逐顶点属性:
|
|
520
|
+
// 顶点数(base mesh)和实例数根本不是一个量级,长度对不上会被 three 当成坏 buffer。
|
|
521
|
+
vp.instanceBatchIds = instanceBatchIds;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
geo.pickInfo = pickInfo;
|
|
525
|
+
result[geometryName] = pickInfo;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return result;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/* ------------------------------------------------------------------ 入口 */
|
|
532
|
+
|
|
533
|
+
export async function parseS3MB(arrayBuffer: ArrayBuffer, options: ParseOptions = {}): Promise<ParsedTile> {
|
|
534
|
+
const inflate = options.inflate ?? defaultInflate;
|
|
535
|
+
const trace = options.trace;
|
|
536
|
+
const r = new BinaryReader(arrayBuffer);
|
|
537
|
+
|
|
538
|
+
const version = Number(r.f32().toFixed(2));
|
|
539
|
+
|
|
540
|
+
let compressedType = 0;
|
|
541
|
+
if (version >= 3) compressedType = r.u32();
|
|
542
|
+
if (version >= 2) r.u32(); // uncompressedSize
|
|
543
|
+
const compressedSize = r.u32();
|
|
544
|
+
|
|
545
|
+
const payload = new Uint8Array(arrayBuffer, r.base + r.offset, compressedSize);
|
|
546
|
+
|
|
547
|
+
let raw: Uint8Array;
|
|
548
|
+
if (version >= 3 && compressedType === 0) {
|
|
549
|
+
raw = payload;
|
|
550
|
+
} else {
|
|
551
|
+
raw = await inflate(payload);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const t = new BinaryReader(raw.buffer as ArrayBuffer, raw.byteOffset ?? 0, raw.byteLength);
|
|
555
|
+
|
|
556
|
+
const fileOptions = t.u32();
|
|
557
|
+
trace?.('fileOptions', fileOptions, t.offset);
|
|
558
|
+
const groupNode = parseGroupNode(t, version);
|
|
559
|
+
trace?.('groupNode', groupNode.pageLods.length, t.offset);
|
|
560
|
+
const geoPackage = await parseSkeletons(t, version, trace);
|
|
561
|
+
trace?.('skeletons', Object.keys(geoPackage).length, t.offset);
|
|
562
|
+
const textures = parseTextures(t);
|
|
563
|
+
trace?.('textures', Object.keys(textures).length, t.offset);
|
|
564
|
+
const materials = parseMaterials(t);
|
|
565
|
+
trace?.('materials', t.offset);
|
|
566
|
+
const pickInfo = parsePickInfo(t, fileOptions, geoPackage, version);
|
|
567
|
+
trace?.('pickInfo', t.offset);
|
|
568
|
+
|
|
569
|
+
let extensions: unknown;
|
|
570
|
+
if (version >= 3) {
|
|
571
|
+
const s = t.string();
|
|
572
|
+
extensions = s ? JSON.parse(s) : undefined;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return {
|
|
576
|
+
version,
|
|
577
|
+
compressedType,
|
|
578
|
+
options: fileOptions,
|
|
579
|
+
groupNode,
|
|
580
|
+
geoPackage,
|
|
581
|
+
textures,
|
|
582
|
+
materials,
|
|
583
|
+
pickInfo,
|
|
584
|
+
extensions,
|
|
585
|
+
bytesRead: t.offset,
|
|
586
|
+
bytesTotal: t.view.byteLength,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export type { VertexAttribute };
|