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/MeshOpt.ts
CHANGED
|
@@ -1,401 +1,401 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* S3M v3 MeshOpt 几何压缩(Skeleton 的 MeshOpt 分支)
|
|
3
|
-
*
|
|
4
|
-
* 结构不是照着规范写的 —— 规范里的 CompressedDataPackage 描述已经落后于 v3.01 的实际数据,
|
|
5
|
-
* 这里的布局是从真实瓦片逐字节标定出来的(见下方字段注释里的实测值)。
|
|
6
|
-
*
|
|
7
|
-
* 与官方 S3M_JS SDK 的差异:
|
|
8
|
-
* 1. 官方 `parseMeshOptSkeleton` 在描述字符串后会多推进 4 字节,导致后续索引包全部错位
|
|
9
|
-
* (实测 v3.01 数据必然崩在 parseMeshOpIndexPackage)。这里按数据实际布局读。
|
|
10
|
-
* 2. 官方把「源字节长度」当 meshopt 的 filter 参数传进去,等于不滤波;
|
|
11
|
-
* 这里 normals 走八面体解码,UV/位置按各自的量化常量还原成 float。
|
|
12
|
-
*
|
|
13
|
-
* 量化公式(用 OBB 反推验证过,误差 0):
|
|
14
|
-
* position = minVerticesValue + uint16 * vertCompressConstant
|
|
15
|
-
* texcoord = minTexCoordValue[i] + uint16 * texCoordCompressConstant[i]
|
|
16
|
-
* normal = octDecode(int16 / 32767)
|
|
17
|
-
*/
|
|
18
|
-
import type { BinaryReader } from './BinaryReader.js';
|
|
19
|
-
import type { IndexPackage, VertexAttribute, VertexPackage } from './types.js';
|
|
20
|
-
import type { MeshoptDecoder } from '../vendor/meshopt_decoder.module.js';
|
|
21
|
-
|
|
22
|
-
/** 顶点语义是位标记,不是枚举序号 */
|
|
23
|
-
export const MeshOptSemantic = {
|
|
24
|
-
POSITION: 0x1,
|
|
25
|
-
POSITION_W: 0x2,
|
|
26
|
-
NORMAL: 0x4,
|
|
27
|
-
TANGENT: 0x8,
|
|
28
|
-
TEXCOORD0: 0x10,
|
|
29
|
-
TEXCOORD1: 0x20,
|
|
30
|
-
COLOR: 0x40,
|
|
31
|
-
COLOR1: 0x80,
|
|
32
|
-
CUSTOM0: 0x200,
|
|
33
|
-
CUSTOM1: 0x400,
|
|
34
|
-
GS_SCALE: 0x20001,
|
|
35
|
-
GS_ROTATION: 0x20002,
|
|
36
|
-
} as const;
|
|
37
|
-
|
|
38
|
-
/** 索引包里 primitiveType = 13 表示索引未压缩(直接 uint32 数组) */
|
|
39
|
-
const UNCOMPRESSED_INDEX = 0x0d;
|
|
40
|
-
|
|
41
|
-
let decoderPromise: Promise<MeshoptDecoder> | null = null;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* 懒加载 meshoptimizer(wasm 内联,约 22KB)。
|
|
45
|
-
* 只有真正遇到 MeshOpt 瓦片时才会拉起来,v1/v2 与标准 v3 不受影响。
|
|
46
|
-
*/
|
|
47
|
-
export function loadMeshoptDecoder(): Promise<MeshoptDecoder> {
|
|
48
|
-
if (!decoderPromise) {
|
|
49
|
-
decoderPromise = import('../vendor/meshopt_decoder.module.js').then(async (mod) => {
|
|
50
|
-
const decoder = mod.default;
|
|
51
|
-
if (!decoder?.supported) throw new Error('[s3m] 当前环境不支持 WebAssembly,无法解码 MeshOpt 几何');
|
|
52
|
-
await decoder.ready;
|
|
53
|
-
return decoder;
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
return decoderPromise;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/* ------------------------------------------------------------------ 八面体法线 */
|
|
60
|
-
|
|
61
|
-
function sign(v: number): number {
|
|
62
|
-
return v < 0 ? -1 : 1;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** meshopt 的八面体解码,把 [-1,1]² 还原成单位球面上的方向 */
|
|
66
|
-
function octDecode(x: number, y: number, out: Float32Array, o: number): void {
|
|
67
|
-
let px = x;
|
|
68
|
-
let py = y;
|
|
69
|
-
const z = 1 - Math.abs(px) - Math.abs(py);
|
|
70
|
-
if (z < 0) {
|
|
71
|
-
const t = (1 - Math.abs(py)) * sign(px);
|
|
72
|
-
py = (1 - Math.abs(px)) * sign(py);
|
|
73
|
-
px = t;
|
|
74
|
-
}
|
|
75
|
-
const len = Math.hypot(px, py, z) || 1;
|
|
76
|
-
out[o] = px / len;
|
|
77
|
-
out[o + 1] = py / len;
|
|
78
|
-
out[o + 2] = z / len;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/* ------------------------------------------------------------------ 工具 */
|
|
82
|
-
|
|
83
|
-
function f32Attribute(semantic: string, data: Float32Array, components: number): VertexAttribute {
|
|
84
|
-
return {
|
|
85
|
-
semantic,
|
|
86
|
-
data: new Uint8Array(data.buffer, data.byteOffset, data.byteLength),
|
|
87
|
-
components,
|
|
88
|
-
stride: components * 4,
|
|
89
|
-
offsetInBytes: 0,
|
|
90
|
-
dtype: 'f32',
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function u8Attribute(semantic: string, data: Uint8Array, components: number): VertexAttribute {
|
|
95
|
-
return {
|
|
96
|
-
semantic,
|
|
97
|
-
data,
|
|
98
|
-
components,
|
|
99
|
-
stride: components,
|
|
100
|
-
offsetInBytes: 0,
|
|
101
|
-
dtype: 'u8',
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** 每顶点字节数,对应 meshopt 解码目标的 stride */
|
|
106
|
-
function strideFor(semantic: number, compressOptions: number, customId: number): number {
|
|
107
|
-
switch (semantic) {
|
|
108
|
-
case MeshOptSemantic.NORMAL:
|
|
109
|
-
case MeshOptSemantic.TEXCOORD0:
|
|
110
|
-
case MeshOptSemantic.TEXCOORD1:
|
|
111
|
-
return 4; // 2 × int16 / uint16
|
|
112
|
-
case MeshOptSemantic.COLOR:
|
|
113
|
-
case MeshOptSemantic.COLOR1:
|
|
114
|
-
return 4; // RGBA8
|
|
115
|
-
case MeshOptSemantic.CUSTOM0:
|
|
116
|
-
case MeshOptSemantic.CUSTOM1:
|
|
117
|
-
return 4 * Math.max(1, customId);
|
|
118
|
-
case MeshOptSemantic.GS_SCALE:
|
|
119
|
-
case MeshOptSemantic.GS_ROTATION:
|
|
120
|
-
return compressOptions & 0x40 ? 4 : 4 * Math.max(1, customId);
|
|
121
|
-
default:
|
|
122
|
-
return 8; // Position:4 × uint16
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function decode(
|
|
127
|
-
decoder: MeshoptDecoder,
|
|
128
|
-
vertexCount: number,
|
|
129
|
-
stride: number,
|
|
130
|
-
source: Uint8Array
|
|
131
|
-
): Uint8Array {
|
|
132
|
-
const size = vertexCount * stride;
|
|
133
|
-
// 头读错时 vertexCount 会是天文数字,这里先拦一道,免得报「Invalid typed array length」这种没法定位的错
|
|
134
|
-
if (!Number.isSafeInteger(size) || size > 64 * 1024 * 1024) {
|
|
135
|
-
throw new Error(`[s3m] MeshOpt 流尺寸异常:${vertexCount} 顶点 × ${stride}B = ${size}B`);
|
|
136
|
-
}
|
|
137
|
-
const out = new Uint8Array(size);
|
|
138
|
-
decoder.decodeVertexBuffer(out, vertexCount, stride, source, source.length);
|
|
139
|
-
return out;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/* ------------------------------------------------------------------ 骨架 */
|
|
143
|
-
|
|
144
|
-
export interface MeshOptBlockInfo {
|
|
145
|
-
vertexCount: number;
|
|
146
|
-
compressOptions: number;
|
|
147
|
-
/** 位置量化:value = min + uint16 * scale */
|
|
148
|
-
minVertices: [number, number, number];
|
|
149
|
-
vertScale: number;
|
|
150
|
-
/** 两套纹理坐标量化常量 */
|
|
151
|
-
texCoordScale: Array<[number, number]>;
|
|
152
|
-
texCoordOffset: Array<[number, number]>;
|
|
153
|
-
/** 各语义的解码字节数,便于排查 */
|
|
154
|
-
streams: Array<{ semantic: string; bytes: number; stride: number }>;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const SEMANTIC_NAME: Record<number, string> = {
|
|
158
|
-
[MeshOptSemantic.POSITION]: 'position',
|
|
159
|
-
[MeshOptSemantic.POSITION_W]: 'positionW',
|
|
160
|
-
[MeshOptSemantic.NORMAL]: 'normal',
|
|
161
|
-
[MeshOptSemantic.TANGENT]: 'tangent',
|
|
162
|
-
[MeshOptSemantic.TEXCOORD0]: 'uv',
|
|
163
|
-
[MeshOptSemantic.TEXCOORD1]: 'uv2',
|
|
164
|
-
[MeshOptSemantic.COLOR]: 'color',
|
|
165
|
-
[MeshOptSemantic.COLOR1]: 'color2',
|
|
166
|
-
[MeshOptSemantic.CUSTOM0]: 'custom0',
|
|
167
|
-
[MeshOptSemantic.CUSTOM1]: 'custom1',
|
|
168
|
-
[MeshOptSemantic.GS_SCALE]: 'gsScale',
|
|
169
|
-
[MeshOptSemantic.GS_ROTATION]: 'gsRotation',
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* 读取一个 MeshOpt 压缩的 Skeleton。
|
|
174
|
-
*
|
|
175
|
-
* 二进制布局(v3.01 实测):
|
|
176
|
-
* ```
|
|
177
|
-
* uint32 compressOptions // 0x13 = Position | Normal | TexCoord
|
|
178
|
-
* uint32 blockCount // 实测恒为 1
|
|
179
|
-
* uint32 vertexCount
|
|
180
|
-
* float32 vertCompressConstant
|
|
181
|
-
* float64 minVerticesValue.xyz
|
|
182
|
-
* float64 texCoordCompressConstant[0].xy
|
|
183
|
-
* float64 minTexCoordValue[0].xy
|
|
184
|
-
* float64 texCoordCompressConstant[1].xy
|
|
185
|
-
* float64 minTexCoordValue[1].xy
|
|
186
|
-
* int32 attrCount
|
|
187
|
-
* int32 semantic // 位标记
|
|
188
|
-
* int32 customId // 仅 Custom0/Custom1/GSScale/GSRotation
|
|
189
|
-
* int32 compressedSize
|
|
190
|
-
* byte data[compressedSize]
|
|
191
|
-
* pad4
|
|
192
|
-
* String descript // {"TextureCoordMatrix":1,"VertexWeight":0}
|
|
193
|
-
* pad4
|
|
194
|
-
* ```
|
|
195
|
-
*/
|
|
196
|
-
export async function parseMeshOptSkeleton(r: BinaryReader, vp: VertexPackage): Promise<MeshOptBlockInfo> {
|
|
197
|
-
const decoder = await loadMeshoptDecoder();
|
|
198
|
-
|
|
199
|
-
const compressOptions = r.u32();
|
|
200
|
-
vp.compressOptions = compressOptions;
|
|
201
|
-
const blockCount = r.u32();
|
|
202
|
-
|
|
203
|
-
const info: MeshOptBlockInfo = {
|
|
204
|
-
vertexCount: 0,
|
|
205
|
-
compressOptions,
|
|
206
|
-
minVertices: [0, 0, 0],
|
|
207
|
-
vertScale: 1,
|
|
208
|
-
texCoordScale: [
|
|
209
|
-
[0, 0],
|
|
210
|
-
[0, 0],
|
|
211
|
-
],
|
|
212
|
-
texCoordOffset: [
|
|
213
|
-
[0, 0],
|
|
214
|
-
[0, 0],
|
|
215
|
-
],
|
|
216
|
-
streams: [],
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
for (let bi = 0; bi < blockCount; bi++) {
|
|
220
|
-
const vertexCount = r.u32();
|
|
221
|
-
const vertScale = r.f32();
|
|
222
|
-
const minX = r.f64();
|
|
223
|
-
const minY = r.f64();
|
|
224
|
-
const minZ = r.f64();
|
|
225
|
-
|
|
226
|
-
const texCoordScale: Array<[number, number]> = [];
|
|
227
|
-
const texCoordOffset: Array<[number, number]> = [];
|
|
228
|
-
for (let i = 0; i < 4; i++) {
|
|
229
|
-
const a = r.f64();
|
|
230
|
-
const b = r.f64();
|
|
231
|
-
// 顺序是 scale0, offset0, scale1, offset1
|
|
232
|
-
if (i % 2 === 0) texCoordScale.push([a, b]);
|
|
233
|
-
else texCoordOffset.push([a, b]);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const attrCount = r.i32();
|
|
237
|
-
const decodeThisBlock = bi === 0;
|
|
238
|
-
|
|
239
|
-
if (decodeThisBlock) {
|
|
240
|
-
info.vertexCount = vertexCount;
|
|
241
|
-
info.vertScale = vertScale;
|
|
242
|
-
info.minVertices = [minX, minY, minZ];
|
|
243
|
-
info.texCoordScale = texCoordScale;
|
|
244
|
-
info.texCoordOffset = texCoordOffset;
|
|
245
|
-
vp.verticesCount = vertexCount;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
for (let ai = 0; ai < attrCount; ai++) {
|
|
249
|
-
const semantic = r.i32();
|
|
250
|
-
let customId = 0;
|
|
251
|
-
if (
|
|
252
|
-
semantic === MeshOptSemantic.CUSTOM0 ||
|
|
253
|
-
semantic === MeshOptSemantic.CUSTOM1 ||
|
|
254
|
-
semantic === MeshOptSemantic.GS_SCALE ||
|
|
255
|
-
semantic === MeshOptSemantic.GS_ROTATION
|
|
256
|
-
) {
|
|
257
|
-
customId = r.i32();
|
|
258
|
-
}
|
|
259
|
-
const compressedSize = r.i32();
|
|
260
|
-
const data = r.bytes(compressedSize);
|
|
261
|
-
r.align4();
|
|
262
|
-
|
|
263
|
-
if (!decodeThisBlock) continue;
|
|
264
|
-
|
|
265
|
-
// 切线流我们不用(渲染里没有法线贴图通道),解不出来也不该拖垮整个瓦片:
|
|
266
|
-
// 真实服务里带 PBR 的数据(如 SubwayStationCombined)都带这条流,之前直接抛错导致整套数据不可见
|
|
267
|
-
if (semantic === MeshOptSemantic.TANGENT) {
|
|
268
|
-
info.streams.push({ semantic: 'tangent', bytes: compressedSize, stride: 0 });
|
|
269
|
-
continue;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const stride = strideFor(semantic, compressOptions, customId);
|
|
273
|
-
let out: Uint8Array;
|
|
274
|
-
try {
|
|
275
|
-
out = decode(decoder, vertexCount, stride, data);
|
|
276
|
-
} catch {
|
|
277
|
-
// 纹理坐标偶尔是 float32 存储(stride 8),按量化格式解失败就退回
|
|
278
|
-
if (semantic === MeshOptSemantic.TEXCOORD0 || semantic === MeshOptSemantic.TEXCOORD1) {
|
|
279
|
-
out = decode(decoder, vertexCount, 8, data);
|
|
280
|
-
} else {
|
|
281
|
-
throw new Error(`[s3m] MeshOpt 属性解码失败(semantic=0x${semantic.toString(16)})`);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const name = SEMANTIC_NAME[semantic] ?? `semantic_0x${semantic.toString(16)}`;
|
|
286
|
-
info.streams.push({
|
|
287
|
-
semantic: name,
|
|
288
|
-
bytes: compressedSize,
|
|
289
|
-
stride: vertexCount > 0 ? out.byteLength / vertexCount : 0,
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
if (semantic === MeshOptSemantic.POSITION) {
|
|
293
|
-
const u16 = new Uint16Array(out.buffer, out.byteOffset, vertexCount * 4);
|
|
294
|
-
const pos = new Float32Array(vertexCount * 3);
|
|
295
|
-
for (let i = 0; i < vertexCount; i++) {
|
|
296
|
-
pos[i * 3] = minX + u16[i * 4] * vertScale;
|
|
297
|
-
pos[i * 3 + 1] = minY + u16[i * 4 + 1] * vertScale;
|
|
298
|
-
pos[i * 3 + 2] = minZ + u16[i * 4 + 2] * vertScale;
|
|
299
|
-
}
|
|
300
|
-
vp.vertexAttributes.push(f32Attribute('position', pos, 3));
|
|
301
|
-
} else if (semantic === MeshOptSemantic.NORMAL) {
|
|
302
|
-
const i16 = new Int16Array(out.buffer, out.byteOffset, vertexCount * 2);
|
|
303
|
-
const nrm = new Float32Array(vertexCount * 3);
|
|
304
|
-
for (let i = 0; i < vertexCount; i++) {
|
|
305
|
-
octDecode(i16[i * 2] / 32767, i16[i * 2 + 1] / 32767, nrm, i * 3);
|
|
306
|
-
}
|
|
307
|
-
vp.vertexAttributes.push(f32Attribute('normal', nrm, 3));
|
|
308
|
-
} else if (semantic === MeshOptSemantic.COLOR) {
|
|
309
|
-
vp.vertexAttributes.push(u8Attribute('color', out, 4));
|
|
310
|
-
} else if (semantic === MeshOptSemantic.COLOR1) {
|
|
311
|
-
vp.vertexAttributes.push(u8Attribute('color2', out, 4));
|
|
312
|
-
} else if (semantic === MeshOptSemantic.TEXCOORD0 || semantic === MeshOptSemantic.TEXCOORD1) {
|
|
313
|
-
const slot = semantic === MeshOptSemantic.TEXCOORD0 ? 0 : 1;
|
|
314
|
-
const scale = texCoordScale[slot] ?? [0, 0];
|
|
315
|
-
const offset = texCoordOffset[slot] ?? [0, 0];
|
|
316
|
-
const uv = new Float32Array(vertexCount * 2);
|
|
317
|
-
if (out.byteLength === vertexCount * 8) {
|
|
318
|
-
// 未量化:直接是 float32 对
|
|
319
|
-
const src = new Float32Array(out.buffer, out.byteOffset, vertexCount * 2);
|
|
320
|
-
uv.set(src);
|
|
321
|
-
} else {
|
|
322
|
-
const u16 = new Uint16Array(out.buffer, out.byteOffset, vertexCount * 2);
|
|
323
|
-
for (let i = 0; i < vertexCount; i++) {
|
|
324
|
-
uv[i * 2] = offset[0] + u16[i * 2] * scale[0];
|
|
325
|
-
uv[i * 2 + 1] = offset[1] + u16[i * 2 + 1] * scale[1];
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
vp.vertexAttributes.push(f32Attribute(slot === 0 ? 'uv' : 'uv2', uv, 2));
|
|
329
|
-
}
|
|
330
|
-
// tangent / custom / GS 系列暂不使用
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const descript = r.string();
|
|
334
|
-
r.align4();
|
|
335
|
-
if (decodeThisBlock && descript) {
|
|
336
|
-
try {
|
|
337
|
-
vp.customVertexAttribute = JSON.parse(descript);
|
|
338
|
-
} catch {
|
|
339
|
-
vp.customVertexAttribute = descript;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// attrLocation 供老接口按名字查索引
|
|
345
|
-
for (let i = 0; i < vp.vertexAttributes.length; i++) {
|
|
346
|
-
vp.attrLocation[`a${vp.vertexAttributes[i].semantic}`] = i;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
return info;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/* ------------------------------------------------------------------ 索引包 */
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* MeshOpt 骨架的索引包。
|
|
356
|
-
* 与标准骨架相比只多了一个 `compressedSize`,其余字段顺序一致;
|
|
357
|
-
* primitiveType = 13 时索引未压缩,直接是 uint32 数组。
|
|
358
|
-
*/
|
|
359
|
-
export async function parseMeshOptIndexPackages(r: BinaryReader): Promise<IndexPackage[]> {
|
|
360
|
-
const decoder = await loadMeshoptDecoder();
|
|
361
|
-
const count = r.i32();
|
|
362
|
-
const packs: IndexPackage[] = [];
|
|
363
|
-
|
|
364
|
-
for (let i = 0; i < count; i++) {
|
|
365
|
-
const indexCount = r.i32();
|
|
366
|
-
if (indexCount < 1) continue;
|
|
367
|
-
|
|
368
|
-
const declaredIndexType = r.u8();
|
|
369
|
-
const useIndex = r.u8();
|
|
370
|
-
const primitiveType = r.u8();
|
|
371
|
-
r.skip(1);
|
|
372
|
-
|
|
373
|
-
const size = r.i32();
|
|
374
|
-
let indices: Uint8Array;
|
|
375
|
-
|
|
376
|
-
if (primitiveType === UNCOMPRESSED_INDEX) {
|
|
377
|
-
indices = r.bytes(size * 4);
|
|
378
|
-
r.align4();
|
|
379
|
-
} else {
|
|
380
|
-
const src = r.bytes(size);
|
|
381
|
-
r.align4();
|
|
382
|
-
// meshopt 永远解成 uint32
|
|
383
|
-
indices = new Uint8Array(indexCount * 4);
|
|
384
|
-
decoder.decodeIndexBuffer(indices, indexCount, 4, src);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const passCount = r.i32();
|
|
388
|
-
const passNames: string[] = [];
|
|
389
|
-
for (let p = 0; p < passCount; p++) passNames.push(r.string());
|
|
390
|
-
r.align4();
|
|
391
|
-
|
|
392
|
-
// MeshOpt 包里的 indexType 字段不可信(实测恒为 0,而数据是 4 字节/索引)。
|
|
393
|
-
// 以实际字节宽度为准写回 indexType,否则下游会按 uint16 读,索引值全错。
|
|
394
|
-
const width = indexCount > 0 ? indices.byteLength / indexCount : 0;
|
|
395
|
-
const indexType =
|
|
396
|
-
width === 2 ? 0 : width === 4 ? 3 : declaredIndexType === 1 || declaredIndexType === 3 ? declaredIndexType : 0;
|
|
397
|
-
packs.push({ indexCount, indexType, useIndex, primitiveType, indices, passNames });
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
return packs;
|
|
401
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* S3M v3 MeshOpt 几何压缩(Skeleton 的 MeshOpt 分支)
|
|
3
|
+
*
|
|
4
|
+
* 结构不是照着规范写的 —— 规范里的 CompressedDataPackage 描述已经落后于 v3.01 的实际数据,
|
|
5
|
+
* 这里的布局是从真实瓦片逐字节标定出来的(见下方字段注释里的实测值)。
|
|
6
|
+
*
|
|
7
|
+
* 与官方 S3M_JS SDK 的差异:
|
|
8
|
+
* 1. 官方 `parseMeshOptSkeleton` 在描述字符串后会多推进 4 字节,导致后续索引包全部错位
|
|
9
|
+
* (实测 v3.01 数据必然崩在 parseMeshOpIndexPackage)。这里按数据实际布局读。
|
|
10
|
+
* 2. 官方把「源字节长度」当 meshopt 的 filter 参数传进去,等于不滤波;
|
|
11
|
+
* 这里 normals 走八面体解码,UV/位置按各自的量化常量还原成 float。
|
|
12
|
+
*
|
|
13
|
+
* 量化公式(用 OBB 反推验证过,误差 0):
|
|
14
|
+
* position = minVerticesValue + uint16 * vertCompressConstant
|
|
15
|
+
* texcoord = minTexCoordValue[i] + uint16 * texCoordCompressConstant[i]
|
|
16
|
+
* normal = octDecode(int16 / 32767)
|
|
17
|
+
*/
|
|
18
|
+
import type { BinaryReader } from './BinaryReader.js';
|
|
19
|
+
import type { IndexPackage, VertexAttribute, VertexPackage } from './types.js';
|
|
20
|
+
import type { MeshoptDecoder } from '../vendor/meshopt_decoder.module.js';
|
|
21
|
+
|
|
22
|
+
/** 顶点语义是位标记,不是枚举序号 */
|
|
23
|
+
export const MeshOptSemantic = {
|
|
24
|
+
POSITION: 0x1,
|
|
25
|
+
POSITION_W: 0x2,
|
|
26
|
+
NORMAL: 0x4,
|
|
27
|
+
TANGENT: 0x8,
|
|
28
|
+
TEXCOORD0: 0x10,
|
|
29
|
+
TEXCOORD1: 0x20,
|
|
30
|
+
COLOR: 0x40,
|
|
31
|
+
COLOR1: 0x80,
|
|
32
|
+
CUSTOM0: 0x200,
|
|
33
|
+
CUSTOM1: 0x400,
|
|
34
|
+
GS_SCALE: 0x20001,
|
|
35
|
+
GS_ROTATION: 0x20002,
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
/** 索引包里 primitiveType = 13 表示索引未压缩(直接 uint32 数组) */
|
|
39
|
+
const UNCOMPRESSED_INDEX = 0x0d;
|
|
40
|
+
|
|
41
|
+
let decoderPromise: Promise<MeshoptDecoder> | null = null;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 懒加载 meshoptimizer(wasm 内联,约 22KB)。
|
|
45
|
+
* 只有真正遇到 MeshOpt 瓦片时才会拉起来,v1/v2 与标准 v3 不受影响。
|
|
46
|
+
*/
|
|
47
|
+
export function loadMeshoptDecoder(): Promise<MeshoptDecoder> {
|
|
48
|
+
if (!decoderPromise) {
|
|
49
|
+
decoderPromise = import('../vendor/meshopt_decoder.module.js').then(async (mod) => {
|
|
50
|
+
const decoder = mod.default;
|
|
51
|
+
if (!decoder?.supported) throw new Error('[s3m] 当前环境不支持 WebAssembly,无法解码 MeshOpt 几何');
|
|
52
|
+
await decoder.ready;
|
|
53
|
+
return decoder;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return decoderPromise;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* ------------------------------------------------------------------ 八面体法线 */
|
|
60
|
+
|
|
61
|
+
function sign(v: number): number {
|
|
62
|
+
return v < 0 ? -1 : 1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** meshopt 的八面体解码,把 [-1,1]² 还原成单位球面上的方向 */
|
|
66
|
+
function octDecode(x: number, y: number, out: Float32Array, o: number): void {
|
|
67
|
+
let px = x;
|
|
68
|
+
let py = y;
|
|
69
|
+
const z = 1 - Math.abs(px) - Math.abs(py);
|
|
70
|
+
if (z < 0) {
|
|
71
|
+
const t = (1 - Math.abs(py)) * sign(px);
|
|
72
|
+
py = (1 - Math.abs(px)) * sign(py);
|
|
73
|
+
px = t;
|
|
74
|
+
}
|
|
75
|
+
const len = Math.hypot(px, py, z) || 1;
|
|
76
|
+
out[o] = px / len;
|
|
77
|
+
out[o + 1] = py / len;
|
|
78
|
+
out[o + 2] = z / len;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* ------------------------------------------------------------------ 工具 */
|
|
82
|
+
|
|
83
|
+
function f32Attribute(semantic: string, data: Float32Array, components: number): VertexAttribute {
|
|
84
|
+
return {
|
|
85
|
+
semantic,
|
|
86
|
+
data: new Uint8Array(data.buffer, data.byteOffset, data.byteLength),
|
|
87
|
+
components,
|
|
88
|
+
stride: components * 4,
|
|
89
|
+
offsetInBytes: 0,
|
|
90
|
+
dtype: 'f32',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function u8Attribute(semantic: string, data: Uint8Array, components: number): VertexAttribute {
|
|
95
|
+
return {
|
|
96
|
+
semantic,
|
|
97
|
+
data,
|
|
98
|
+
components,
|
|
99
|
+
stride: components,
|
|
100
|
+
offsetInBytes: 0,
|
|
101
|
+
dtype: 'u8',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 每顶点字节数,对应 meshopt 解码目标的 stride */
|
|
106
|
+
function strideFor(semantic: number, compressOptions: number, customId: number): number {
|
|
107
|
+
switch (semantic) {
|
|
108
|
+
case MeshOptSemantic.NORMAL:
|
|
109
|
+
case MeshOptSemantic.TEXCOORD0:
|
|
110
|
+
case MeshOptSemantic.TEXCOORD1:
|
|
111
|
+
return 4; // 2 × int16 / uint16
|
|
112
|
+
case MeshOptSemantic.COLOR:
|
|
113
|
+
case MeshOptSemantic.COLOR1:
|
|
114
|
+
return 4; // RGBA8
|
|
115
|
+
case MeshOptSemantic.CUSTOM0:
|
|
116
|
+
case MeshOptSemantic.CUSTOM1:
|
|
117
|
+
return 4 * Math.max(1, customId);
|
|
118
|
+
case MeshOptSemantic.GS_SCALE:
|
|
119
|
+
case MeshOptSemantic.GS_ROTATION:
|
|
120
|
+
return compressOptions & 0x40 ? 4 : 4 * Math.max(1, customId);
|
|
121
|
+
default:
|
|
122
|
+
return 8; // Position:4 × uint16
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function decode(
|
|
127
|
+
decoder: MeshoptDecoder,
|
|
128
|
+
vertexCount: number,
|
|
129
|
+
stride: number,
|
|
130
|
+
source: Uint8Array
|
|
131
|
+
): Uint8Array {
|
|
132
|
+
const size = vertexCount * stride;
|
|
133
|
+
// 头读错时 vertexCount 会是天文数字,这里先拦一道,免得报「Invalid typed array length」这种没法定位的错
|
|
134
|
+
if (!Number.isSafeInteger(size) || size > 64 * 1024 * 1024) {
|
|
135
|
+
throw new Error(`[s3m] MeshOpt 流尺寸异常:${vertexCount} 顶点 × ${stride}B = ${size}B`);
|
|
136
|
+
}
|
|
137
|
+
const out = new Uint8Array(size);
|
|
138
|
+
decoder.decodeVertexBuffer(out, vertexCount, stride, source, source.length);
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* ------------------------------------------------------------------ 骨架 */
|
|
143
|
+
|
|
144
|
+
export interface MeshOptBlockInfo {
|
|
145
|
+
vertexCount: number;
|
|
146
|
+
compressOptions: number;
|
|
147
|
+
/** 位置量化:value = min + uint16 * scale */
|
|
148
|
+
minVertices: [number, number, number];
|
|
149
|
+
vertScale: number;
|
|
150
|
+
/** 两套纹理坐标量化常量 */
|
|
151
|
+
texCoordScale: Array<[number, number]>;
|
|
152
|
+
texCoordOffset: Array<[number, number]>;
|
|
153
|
+
/** 各语义的解码字节数,便于排查 */
|
|
154
|
+
streams: Array<{ semantic: string; bytes: number; stride: number }>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const SEMANTIC_NAME: Record<number, string> = {
|
|
158
|
+
[MeshOptSemantic.POSITION]: 'position',
|
|
159
|
+
[MeshOptSemantic.POSITION_W]: 'positionW',
|
|
160
|
+
[MeshOptSemantic.NORMAL]: 'normal',
|
|
161
|
+
[MeshOptSemantic.TANGENT]: 'tangent',
|
|
162
|
+
[MeshOptSemantic.TEXCOORD0]: 'uv',
|
|
163
|
+
[MeshOptSemantic.TEXCOORD1]: 'uv2',
|
|
164
|
+
[MeshOptSemantic.COLOR]: 'color',
|
|
165
|
+
[MeshOptSemantic.COLOR1]: 'color2',
|
|
166
|
+
[MeshOptSemantic.CUSTOM0]: 'custom0',
|
|
167
|
+
[MeshOptSemantic.CUSTOM1]: 'custom1',
|
|
168
|
+
[MeshOptSemantic.GS_SCALE]: 'gsScale',
|
|
169
|
+
[MeshOptSemantic.GS_ROTATION]: 'gsRotation',
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 读取一个 MeshOpt 压缩的 Skeleton。
|
|
174
|
+
*
|
|
175
|
+
* 二进制布局(v3.01 实测):
|
|
176
|
+
* ```
|
|
177
|
+
* uint32 compressOptions // 0x13 = Position | Normal | TexCoord
|
|
178
|
+
* uint32 blockCount // 实测恒为 1
|
|
179
|
+
* uint32 vertexCount
|
|
180
|
+
* float32 vertCompressConstant
|
|
181
|
+
* float64 minVerticesValue.xyz
|
|
182
|
+
* float64 texCoordCompressConstant[0].xy
|
|
183
|
+
* float64 minTexCoordValue[0].xy
|
|
184
|
+
* float64 texCoordCompressConstant[1].xy
|
|
185
|
+
* float64 minTexCoordValue[1].xy
|
|
186
|
+
* int32 attrCount
|
|
187
|
+
* int32 semantic // 位标记
|
|
188
|
+
* int32 customId // 仅 Custom0/Custom1/GSScale/GSRotation
|
|
189
|
+
* int32 compressedSize
|
|
190
|
+
* byte data[compressedSize]
|
|
191
|
+
* pad4
|
|
192
|
+
* String descript // {"TextureCoordMatrix":1,"VertexWeight":0}
|
|
193
|
+
* pad4
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
export async function parseMeshOptSkeleton(r: BinaryReader, vp: VertexPackage): Promise<MeshOptBlockInfo> {
|
|
197
|
+
const decoder = await loadMeshoptDecoder();
|
|
198
|
+
|
|
199
|
+
const compressOptions = r.u32();
|
|
200
|
+
vp.compressOptions = compressOptions;
|
|
201
|
+
const blockCount = r.u32();
|
|
202
|
+
|
|
203
|
+
const info: MeshOptBlockInfo = {
|
|
204
|
+
vertexCount: 0,
|
|
205
|
+
compressOptions,
|
|
206
|
+
minVertices: [0, 0, 0],
|
|
207
|
+
vertScale: 1,
|
|
208
|
+
texCoordScale: [
|
|
209
|
+
[0, 0],
|
|
210
|
+
[0, 0],
|
|
211
|
+
],
|
|
212
|
+
texCoordOffset: [
|
|
213
|
+
[0, 0],
|
|
214
|
+
[0, 0],
|
|
215
|
+
],
|
|
216
|
+
streams: [],
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
for (let bi = 0; bi < blockCount; bi++) {
|
|
220
|
+
const vertexCount = r.u32();
|
|
221
|
+
const vertScale = r.f32();
|
|
222
|
+
const minX = r.f64();
|
|
223
|
+
const minY = r.f64();
|
|
224
|
+
const minZ = r.f64();
|
|
225
|
+
|
|
226
|
+
const texCoordScale: Array<[number, number]> = [];
|
|
227
|
+
const texCoordOffset: Array<[number, number]> = [];
|
|
228
|
+
for (let i = 0; i < 4; i++) {
|
|
229
|
+
const a = r.f64();
|
|
230
|
+
const b = r.f64();
|
|
231
|
+
// 顺序是 scale0, offset0, scale1, offset1
|
|
232
|
+
if (i % 2 === 0) texCoordScale.push([a, b]);
|
|
233
|
+
else texCoordOffset.push([a, b]);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const attrCount = r.i32();
|
|
237
|
+
const decodeThisBlock = bi === 0;
|
|
238
|
+
|
|
239
|
+
if (decodeThisBlock) {
|
|
240
|
+
info.vertexCount = vertexCount;
|
|
241
|
+
info.vertScale = vertScale;
|
|
242
|
+
info.minVertices = [minX, minY, minZ];
|
|
243
|
+
info.texCoordScale = texCoordScale;
|
|
244
|
+
info.texCoordOffset = texCoordOffset;
|
|
245
|
+
vp.verticesCount = vertexCount;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
for (let ai = 0; ai < attrCount; ai++) {
|
|
249
|
+
const semantic = r.i32();
|
|
250
|
+
let customId = 0;
|
|
251
|
+
if (
|
|
252
|
+
semantic === MeshOptSemantic.CUSTOM0 ||
|
|
253
|
+
semantic === MeshOptSemantic.CUSTOM1 ||
|
|
254
|
+
semantic === MeshOptSemantic.GS_SCALE ||
|
|
255
|
+
semantic === MeshOptSemantic.GS_ROTATION
|
|
256
|
+
) {
|
|
257
|
+
customId = r.i32();
|
|
258
|
+
}
|
|
259
|
+
const compressedSize = r.i32();
|
|
260
|
+
const data = r.bytes(compressedSize);
|
|
261
|
+
r.align4();
|
|
262
|
+
|
|
263
|
+
if (!decodeThisBlock) continue;
|
|
264
|
+
|
|
265
|
+
// 切线流我们不用(渲染里没有法线贴图通道),解不出来也不该拖垮整个瓦片:
|
|
266
|
+
// 真实服务里带 PBR 的数据(如 SubwayStationCombined)都带这条流,之前直接抛错导致整套数据不可见
|
|
267
|
+
if (semantic === MeshOptSemantic.TANGENT) {
|
|
268
|
+
info.streams.push({ semantic: 'tangent', bytes: compressedSize, stride: 0 });
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const stride = strideFor(semantic, compressOptions, customId);
|
|
273
|
+
let out: Uint8Array;
|
|
274
|
+
try {
|
|
275
|
+
out = decode(decoder, vertexCount, stride, data);
|
|
276
|
+
} catch {
|
|
277
|
+
// 纹理坐标偶尔是 float32 存储(stride 8),按量化格式解失败就退回
|
|
278
|
+
if (semantic === MeshOptSemantic.TEXCOORD0 || semantic === MeshOptSemantic.TEXCOORD1) {
|
|
279
|
+
out = decode(decoder, vertexCount, 8, data);
|
|
280
|
+
} else {
|
|
281
|
+
throw new Error(`[s3m] MeshOpt 属性解码失败(semantic=0x${semantic.toString(16)})`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const name = SEMANTIC_NAME[semantic] ?? `semantic_0x${semantic.toString(16)}`;
|
|
286
|
+
info.streams.push({
|
|
287
|
+
semantic: name,
|
|
288
|
+
bytes: compressedSize,
|
|
289
|
+
stride: vertexCount > 0 ? out.byteLength / vertexCount : 0,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
if (semantic === MeshOptSemantic.POSITION) {
|
|
293
|
+
const u16 = new Uint16Array(out.buffer, out.byteOffset, vertexCount * 4);
|
|
294
|
+
const pos = new Float32Array(vertexCount * 3);
|
|
295
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
296
|
+
pos[i * 3] = minX + u16[i * 4] * vertScale;
|
|
297
|
+
pos[i * 3 + 1] = minY + u16[i * 4 + 1] * vertScale;
|
|
298
|
+
pos[i * 3 + 2] = minZ + u16[i * 4 + 2] * vertScale;
|
|
299
|
+
}
|
|
300
|
+
vp.vertexAttributes.push(f32Attribute('position', pos, 3));
|
|
301
|
+
} else if (semantic === MeshOptSemantic.NORMAL) {
|
|
302
|
+
const i16 = new Int16Array(out.buffer, out.byteOffset, vertexCount * 2);
|
|
303
|
+
const nrm = new Float32Array(vertexCount * 3);
|
|
304
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
305
|
+
octDecode(i16[i * 2] / 32767, i16[i * 2 + 1] / 32767, nrm, i * 3);
|
|
306
|
+
}
|
|
307
|
+
vp.vertexAttributes.push(f32Attribute('normal', nrm, 3));
|
|
308
|
+
} else if (semantic === MeshOptSemantic.COLOR) {
|
|
309
|
+
vp.vertexAttributes.push(u8Attribute('color', out, 4));
|
|
310
|
+
} else if (semantic === MeshOptSemantic.COLOR1) {
|
|
311
|
+
vp.vertexAttributes.push(u8Attribute('color2', out, 4));
|
|
312
|
+
} else if (semantic === MeshOptSemantic.TEXCOORD0 || semantic === MeshOptSemantic.TEXCOORD1) {
|
|
313
|
+
const slot = semantic === MeshOptSemantic.TEXCOORD0 ? 0 : 1;
|
|
314
|
+
const scale = texCoordScale[slot] ?? [0, 0];
|
|
315
|
+
const offset = texCoordOffset[slot] ?? [0, 0];
|
|
316
|
+
const uv = new Float32Array(vertexCount * 2);
|
|
317
|
+
if (out.byteLength === vertexCount * 8) {
|
|
318
|
+
// 未量化:直接是 float32 对
|
|
319
|
+
const src = new Float32Array(out.buffer, out.byteOffset, vertexCount * 2);
|
|
320
|
+
uv.set(src);
|
|
321
|
+
} else {
|
|
322
|
+
const u16 = new Uint16Array(out.buffer, out.byteOffset, vertexCount * 2);
|
|
323
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
324
|
+
uv[i * 2] = offset[0] + u16[i * 2] * scale[0];
|
|
325
|
+
uv[i * 2 + 1] = offset[1] + u16[i * 2 + 1] * scale[1];
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
vp.vertexAttributes.push(f32Attribute(slot === 0 ? 'uv' : 'uv2', uv, 2));
|
|
329
|
+
}
|
|
330
|
+
// tangent / custom / GS 系列暂不使用
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const descript = r.string();
|
|
334
|
+
r.align4();
|
|
335
|
+
if (decodeThisBlock && descript) {
|
|
336
|
+
try {
|
|
337
|
+
vp.customVertexAttribute = JSON.parse(descript);
|
|
338
|
+
} catch {
|
|
339
|
+
vp.customVertexAttribute = descript;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// attrLocation 供老接口按名字查索引
|
|
345
|
+
for (let i = 0; i < vp.vertexAttributes.length; i++) {
|
|
346
|
+
vp.attrLocation[`a${vp.vertexAttributes[i].semantic}`] = i;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return info;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/* ------------------------------------------------------------------ 索引包 */
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* MeshOpt 骨架的索引包。
|
|
356
|
+
* 与标准骨架相比只多了一个 `compressedSize`,其余字段顺序一致;
|
|
357
|
+
* primitiveType = 13 时索引未压缩,直接是 uint32 数组。
|
|
358
|
+
*/
|
|
359
|
+
export async function parseMeshOptIndexPackages(r: BinaryReader): Promise<IndexPackage[]> {
|
|
360
|
+
const decoder = await loadMeshoptDecoder();
|
|
361
|
+
const count = r.i32();
|
|
362
|
+
const packs: IndexPackage[] = [];
|
|
363
|
+
|
|
364
|
+
for (let i = 0; i < count; i++) {
|
|
365
|
+
const indexCount = r.i32();
|
|
366
|
+
if (indexCount < 1) continue;
|
|
367
|
+
|
|
368
|
+
const declaredIndexType = r.u8();
|
|
369
|
+
const useIndex = r.u8();
|
|
370
|
+
const primitiveType = r.u8();
|
|
371
|
+
r.skip(1);
|
|
372
|
+
|
|
373
|
+
const size = r.i32();
|
|
374
|
+
let indices: Uint8Array;
|
|
375
|
+
|
|
376
|
+
if (primitiveType === UNCOMPRESSED_INDEX) {
|
|
377
|
+
indices = r.bytes(size * 4);
|
|
378
|
+
r.align4();
|
|
379
|
+
} else {
|
|
380
|
+
const src = r.bytes(size);
|
|
381
|
+
r.align4();
|
|
382
|
+
// meshopt 永远解成 uint32
|
|
383
|
+
indices = new Uint8Array(indexCount * 4);
|
|
384
|
+
decoder.decodeIndexBuffer(indices, indexCount, 4, src);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const passCount = r.i32();
|
|
388
|
+
const passNames: string[] = [];
|
|
389
|
+
for (let p = 0; p < passCount; p++) passNames.push(r.string());
|
|
390
|
+
r.align4();
|
|
391
|
+
|
|
392
|
+
// MeshOpt 包里的 indexType 字段不可信(实测恒为 0,而数据是 4 字节/索引)。
|
|
393
|
+
// 以实际字节宽度为准写回 indexType,否则下游会按 uint16 读,索引值全错。
|
|
394
|
+
const width = indexCount > 0 ? indices.byteLength / indexCount : 0;
|
|
395
|
+
const indexType =
|
|
396
|
+
width === 2 ? 0 : width === 4 ? 3 : declaredIndexType === 1 || declaredIndexType === 3 ? declaredIndexType : 0;
|
|
397
|
+
packs.push({ indexCount, indexType, useIndex, primitiveType, indices, passNames });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return packs;
|
|
401
|
+
}
|