@threenative/raw-unreal 0.1.1
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 +21 -0
- package/README.md +102 -0
- package/dist/index.d.ts +428 -0
- package/dist/index.js +2329 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2329 @@
|
|
|
1
|
+
import { BufferGeometry, BufferAttribute, Mesh, Loader, FileLoader, MeshStandardMaterial } from 'three';
|
|
2
|
+
|
|
3
|
+
// src/errors.ts
|
|
4
|
+
var UAssetError = class extends Error {
|
|
5
|
+
code;
|
|
6
|
+
details;
|
|
7
|
+
constructor(code, message, details = {}) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "UAssetError";
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.details = details;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
function assertUAsset(condition, code, message, details = {}) {
|
|
15
|
+
if (!condition) throw new UAssetError(code, message, details);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/binary.ts
|
|
19
|
+
var utf8 = new TextDecoder("utf-8");
|
|
20
|
+
var utf16 = new TextDecoder("utf-16le");
|
|
21
|
+
var BinaryReader = class {
|
|
22
|
+
bytes;
|
|
23
|
+
view;
|
|
24
|
+
pos = 0;
|
|
25
|
+
constructor(input, offset = 0, length = input.byteLength - offset) {
|
|
26
|
+
if (offset < 0 || length < 0 || offset + length > input.byteLength) {
|
|
27
|
+
throw new UAssetError("TRUNCATED_PACKAGE", "Invalid binary reader range", {
|
|
28
|
+
offset,
|
|
29
|
+
length,
|
|
30
|
+
byteLength: input.byteLength
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
this.bytes = input.subarray(offset, offset + length);
|
|
34
|
+
this.view = new DataView(this.bytes.buffer, this.bytes.byteOffset, this.bytes.byteLength);
|
|
35
|
+
}
|
|
36
|
+
get byteLength() {
|
|
37
|
+
return this.bytes.byteLength;
|
|
38
|
+
}
|
|
39
|
+
get remaining() {
|
|
40
|
+
return this.byteLength - this.pos;
|
|
41
|
+
}
|
|
42
|
+
ensure(size, context) {
|
|
43
|
+
if (!Number.isInteger(size) || size < 0 || this.pos + size > this.byteLength) {
|
|
44
|
+
throw new UAssetError("TRUNCATED_PACKAGE", `Unexpected end of ${context}`, {
|
|
45
|
+
offset: this.pos,
|
|
46
|
+
requested: size,
|
|
47
|
+
remaining: this.remaining
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
seek(offset) {
|
|
52
|
+
if (!Number.isInteger(offset) || offset < 0 || offset > this.byteLength) {
|
|
53
|
+
throw new UAssetError("TRUNCATED_PACKAGE", "Invalid binary seek", {
|
|
54
|
+
offset,
|
|
55
|
+
byteLength: this.byteLength
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
this.pos = offset;
|
|
59
|
+
}
|
|
60
|
+
skip(size, context) {
|
|
61
|
+
this.ensure(size, context);
|
|
62
|
+
this.pos += size;
|
|
63
|
+
}
|
|
64
|
+
uint8(context) {
|
|
65
|
+
this.ensure(1, context);
|
|
66
|
+
return this.view.getUint8(this.pos++);
|
|
67
|
+
}
|
|
68
|
+
int32(context) {
|
|
69
|
+
this.ensure(4, context);
|
|
70
|
+
const value = this.view.getInt32(this.pos, true);
|
|
71
|
+
this.pos += 4;
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
uint32(context) {
|
|
75
|
+
this.ensure(4, context);
|
|
76
|
+
const value = this.view.getUint32(this.pos, true);
|
|
77
|
+
this.pos += 4;
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
uint32BE(context) {
|
|
81
|
+
this.ensure(4, context);
|
|
82
|
+
const value = this.view.getUint32(this.pos, false);
|
|
83
|
+
this.pos += 4;
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
uint64BE(context) {
|
|
87
|
+
this.ensure(8, context);
|
|
88
|
+
const value = this.view.getBigUint64(this.pos, false);
|
|
89
|
+
this.pos += 8;
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
float32(context) {
|
|
93
|
+
this.ensure(4, context);
|
|
94
|
+
const value = this.view.getFloat32(this.pos, true);
|
|
95
|
+
this.pos += 4;
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
raw(size, context) {
|
|
99
|
+
this.ensure(size, context);
|
|
100
|
+
const value = this.bytes.subarray(this.pos, this.pos + size);
|
|
101
|
+
this.pos += size;
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
/** An FString: positive length is UTF-8 bytes, negative is UTF-16 code units, zero is empty. */
|
|
105
|
+
fstring(context) {
|
|
106
|
+
const start = this.pos;
|
|
107
|
+
const length = this.int32(`${context} length`);
|
|
108
|
+
if (length === 0) return "";
|
|
109
|
+
if (Math.abs(length) > 16777216) {
|
|
110
|
+
throw new UAssetError("TRUNCATED_PACKAGE", `Unreasonable ${context} length`, {
|
|
111
|
+
offset: start,
|
|
112
|
+
length
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (length > 0) {
|
|
116
|
+
const data2 = this.raw(length, context);
|
|
117
|
+
const end = data2.length > 0 && data2[data2.length - 1] === 0 ? data2.length - 1 : data2.length;
|
|
118
|
+
return utf8.decode(data2.subarray(0, end));
|
|
119
|
+
}
|
|
120
|
+
const charCount = -length;
|
|
121
|
+
const data = this.raw(charCount * 2, context);
|
|
122
|
+
const hasNull = data.length >= 2 && data[data.length - 1] === 0 && data[data.length - 2] === 0;
|
|
123
|
+
return utf16.decode(data.subarray(0, hasNull ? data.length - 2 : data.length));
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// src/package-summary.ts
|
|
128
|
+
var PACKAGE_FILE_TAG = 2653586369;
|
|
129
|
+
var EDITOR_OBJECT_VERSION_GUID_BYTES = Uint8Array.from([
|
|
130
|
+
237,
|
|
131
|
+
104,
|
|
132
|
+
176,
|
|
133
|
+
228,
|
|
134
|
+
// E4B068ED
|
|
135
|
+
233,
|
|
136
|
+
66,
|
|
137
|
+
148,
|
|
138
|
+
244,
|
|
139
|
+
// F49442E9
|
|
140
|
+
11,
|
|
141
|
+
218,
|
|
142
|
+
49,
|
|
143
|
+
162,
|
|
144
|
+
// A231DA0B
|
|
145
|
+
65,
|
|
146
|
+
187,
|
|
147
|
+
70,
|
|
148
|
+
46
|
|
149
|
+
// 2E46BB41
|
|
150
|
+
]);
|
|
151
|
+
var SUMMARY_SCAN_BYTES = 4096;
|
|
152
|
+
function findEditorObjectVersion(bytes, from) {
|
|
153
|
+
const limit = Math.min(bytes.byteLength - 20, from + SUMMARY_SCAN_BYTES);
|
|
154
|
+
for (let position = from; position >= 0 && position < limit; position += 1) {
|
|
155
|
+
let matched = true;
|
|
156
|
+
for (let index = 0; index < 16; index += 1) {
|
|
157
|
+
if (bytes[position + index] !== EDITOR_OBJECT_VERSION_GUID_BYTES[index]) {
|
|
158
|
+
matched = false;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (matched) {
|
|
163
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
164
|
+
return view.getInt32(position + 16, true);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return void 0;
|
|
168
|
+
}
|
|
169
|
+
function readPackageSummary(bytes) {
|
|
170
|
+
if (bytes.byteLength < 4) {
|
|
171
|
+
throw new UAssetError("TRUNCATED_PACKAGE", "Unreal package is too small", {
|
|
172
|
+
byteLength: bytes.byteLength
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
176
|
+
const packageTag = view.getUint32(0, true);
|
|
177
|
+
if (packageTag !== PACKAGE_FILE_TAG) {
|
|
178
|
+
throw new UAssetError(
|
|
179
|
+
"INVALID_PACKAGE_TAG",
|
|
180
|
+
"Input is not a legacy Unreal package (.uasset/.umap)",
|
|
181
|
+
{
|
|
182
|
+
tag: `0x${packageTag.toString(16)}`
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
if (bytes.byteLength < 16) {
|
|
187
|
+
throw new UAssetError("TRUNCATED_PACKAGE", "Unreal package summary is truncated", {
|
|
188
|
+
byteLength: bytes.byteLength
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
const legacyFileVersion = view.getInt32(4, true);
|
|
192
|
+
const legacyUE3Version = view.getInt32(8, true);
|
|
193
|
+
const fileVersionUE4 = view.getInt32(12, true);
|
|
194
|
+
const fileVersionUE5 = legacyFileVersion <= -8 ? view.getInt32(16, true) : void 0;
|
|
195
|
+
const licenseeOffset = legacyFileVersion <= -8 ? 20 : 16;
|
|
196
|
+
const licenseeVersion = view.getInt32(licenseeOffset, true);
|
|
197
|
+
const afterLicensee = licenseeOffset + 4;
|
|
198
|
+
const editorObjectVersion = findEditorObjectVersion(bytes, afterLicensee);
|
|
199
|
+
return {
|
|
200
|
+
packageTag,
|
|
201
|
+
legacyFileVersion,
|
|
202
|
+
legacyUE3Version,
|
|
203
|
+
fileVersionUE4,
|
|
204
|
+
...fileVersionUE5 === void 0 ? {} : { fileVersionUE5 },
|
|
205
|
+
...licenseeVersion === void 0 ? {} : { licenseeVersion },
|
|
206
|
+
...editorObjectVersion === void 0 ? {} : { editorObjectVersion }
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
var VER_WORLD_LEVEL_INFO = 224;
|
|
210
|
+
var VER_CHUNKID_ARRAY = 326;
|
|
211
|
+
var VER_ENGINE_VERSION_OBJECT = 336;
|
|
212
|
+
var VER_STRING_ASSET_REFERENCES_MAP = 384;
|
|
213
|
+
var VER_COMPATIBLE_ENGINE_VERSION = 444;
|
|
214
|
+
var VER_SERIALIZE_TEXT_IN_PACKAGES = 459;
|
|
215
|
+
var VER_PRELOAD_DEPENDENCIES = 507;
|
|
216
|
+
var VER_ADDED_SEARCHABLE_NAMES = 510;
|
|
217
|
+
var VER_PACKAGE_SUMMARY_LOCALIZATION_ID = 516;
|
|
218
|
+
var VER_ADDED_PACKAGE_OWNER = 518;
|
|
219
|
+
var VER_REMOVE_LINKER_PACKAGE_OWNER = 520;
|
|
220
|
+
var UE5_NAMES_REFERENCED_FROM_EXPORT_DATA = 1001;
|
|
221
|
+
var UE5_PAYLOAD_TOC = 1002;
|
|
222
|
+
var UE5_ADD_SOFTOBJECTPATH_LIST = 1008;
|
|
223
|
+
var UE5_DATA_RESOURCES = 1009;
|
|
224
|
+
var OLDEST_MODELLED_LEGACY_VERSION = -8;
|
|
225
|
+
function readEngineVersion(reader) {
|
|
226
|
+
reader.skip(2 + 2 + 2 + 4, "FEngineVersion");
|
|
227
|
+
reader.fstring("FEngineVersion branch");
|
|
228
|
+
}
|
|
229
|
+
function walkSummary(bytes, summary) {
|
|
230
|
+
const reader = new BinaryReader(bytes);
|
|
231
|
+
const { legacyFileVersion, fileVersionUE4 = 0, fileVersionUE5 } = summary;
|
|
232
|
+
reader.seek(legacyFileVersion <= -8 ? 24 : 20);
|
|
233
|
+
if (legacyFileVersion <= -2) {
|
|
234
|
+
const customVersionCount = reader.int32("custom-version count");
|
|
235
|
+
assertUAsset(
|
|
236
|
+
customVersionCount >= 0 && customVersionCount <= 512,
|
|
237
|
+
"INVALID_PACKAGE_SUMMARY",
|
|
238
|
+
"Invalid custom-version count",
|
|
239
|
+
{ customVersionCount }
|
|
240
|
+
);
|
|
241
|
+
reader.skip(customVersionCount * 20, "custom-version list");
|
|
242
|
+
}
|
|
243
|
+
const totalHeaderSize = reader.int32("TotalHeaderSize");
|
|
244
|
+
reader.fstring("FolderName");
|
|
245
|
+
reader.skip(4, "PackageFlags");
|
|
246
|
+
const nameCount = reader.int32("NameCount");
|
|
247
|
+
const nameOffset = reader.int32("NameOffset");
|
|
248
|
+
if (fileVersionUE5 !== void 0 && fileVersionUE5 >= UE5_ADD_SOFTOBJECTPATH_LIST) {
|
|
249
|
+
reader.skip(8, "SoftObjectPaths");
|
|
250
|
+
}
|
|
251
|
+
if (fileVersionUE4 >= VER_PACKAGE_SUMMARY_LOCALIZATION_ID) reader.fstring("LocalizationId");
|
|
252
|
+
if (fileVersionUE4 >= VER_SERIALIZE_TEXT_IN_PACKAGES) reader.skip(8, "GatherableTextData");
|
|
253
|
+
const exportCount = reader.int32("ExportCount");
|
|
254
|
+
const exportOffset = reader.int32("ExportOffset");
|
|
255
|
+
reader.skip(8, "ImportCount and ImportOffset");
|
|
256
|
+
reader.skip(4, "DependsOffset");
|
|
257
|
+
if (fileVersionUE4 >= VER_STRING_ASSET_REFERENCES_MAP) reader.skip(8, "SoftPackageReferences");
|
|
258
|
+
if (fileVersionUE4 >= VER_ADDED_SEARCHABLE_NAMES) reader.skip(4, "SearchableNamesOffset");
|
|
259
|
+
reader.skip(4, "ThumbnailTableOffset");
|
|
260
|
+
reader.skip(16, "package Guid");
|
|
261
|
+
if (fileVersionUE4 >= VER_ADDED_PACKAGE_OWNER) {
|
|
262
|
+
reader.skip(16, "PersistentGuid");
|
|
263
|
+
if (fileVersionUE4 < VER_REMOVE_LINKER_PACKAGE_OWNER) reader.skip(16, "OwnerPersistentGuid");
|
|
264
|
+
}
|
|
265
|
+
const generationCount = reader.int32("GenerationCount");
|
|
266
|
+
assertUAsset(
|
|
267
|
+
generationCount >= 0 && generationCount <= 4096,
|
|
268
|
+
"INVALID_PACKAGE_SUMMARY",
|
|
269
|
+
"Invalid generation count",
|
|
270
|
+
{ generationCount }
|
|
271
|
+
);
|
|
272
|
+
reader.skip(generationCount * 8, "generation list");
|
|
273
|
+
if (fileVersionUE4 >= VER_ENGINE_VERSION_OBJECT) readEngineVersion(reader);
|
|
274
|
+
else reader.skip(4, "EngineChangelist");
|
|
275
|
+
if (fileVersionUE4 >= VER_COMPATIBLE_ENGINE_VERSION) readEngineVersion(reader);
|
|
276
|
+
reader.skip(4, "CompressionFlags");
|
|
277
|
+
const compressedChunkCount = reader.int32("CompressedChunkCount");
|
|
278
|
+
assertUAsset(
|
|
279
|
+
compressedChunkCount >= 0 && compressedChunkCount <= 65536,
|
|
280
|
+
"INVALID_PACKAGE_SUMMARY",
|
|
281
|
+
"Invalid compressed-chunk count",
|
|
282
|
+
{ compressedChunkCount }
|
|
283
|
+
);
|
|
284
|
+
reader.skip(compressedChunkCount * 16, "compressed-chunk list");
|
|
285
|
+
reader.skip(4, "PackageSource");
|
|
286
|
+
const additionalPackagesToCook = reader.int32("AdditionalPackagesToCook count");
|
|
287
|
+
assertUAsset(
|
|
288
|
+
additionalPackagesToCook >= 0 && additionalPackagesToCook <= 65536,
|
|
289
|
+
"INVALID_PACKAGE_SUMMARY",
|
|
290
|
+
"Invalid additional-packages-to-cook count",
|
|
291
|
+
{ additionalPackagesToCook }
|
|
292
|
+
);
|
|
293
|
+
for (let index = 0; index < additionalPackagesToCook; index += 1) {
|
|
294
|
+
reader.fstring("AdditionalPackagesToCook entry");
|
|
295
|
+
}
|
|
296
|
+
if (legacyFileVersion > -7) reader.skip(4, "NumTextureAllocations");
|
|
297
|
+
reader.skip(4, "AssetRegistryDataOffset");
|
|
298
|
+
const bulkDataStartOffset = Number(
|
|
299
|
+
new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getBigInt64(reader.pos, true)
|
|
300
|
+
);
|
|
301
|
+
reader.skip(8, "BulkDataStartOffset");
|
|
302
|
+
if (fileVersionUE4 >= VER_WORLD_LEVEL_INFO) reader.skip(4, "WorldTileInfoDataOffset");
|
|
303
|
+
if (fileVersionUE4 >= VER_CHUNKID_ARRAY) {
|
|
304
|
+
const chunkIdCount = reader.int32("ChunkID count");
|
|
305
|
+
assertUAsset(
|
|
306
|
+
chunkIdCount >= 0 && chunkIdCount <= 65536,
|
|
307
|
+
"INVALID_PACKAGE_SUMMARY",
|
|
308
|
+
"Invalid chunk-id count",
|
|
309
|
+
{ chunkIdCount }
|
|
310
|
+
);
|
|
311
|
+
reader.skip(chunkIdCount * 4, "ChunkID list");
|
|
312
|
+
}
|
|
313
|
+
if (fileVersionUE4 >= VER_PRELOAD_DEPENDENCIES) reader.skip(8, "PreloadDependencies");
|
|
314
|
+
if (fileVersionUE5 !== void 0) {
|
|
315
|
+
if (fileVersionUE5 >= UE5_NAMES_REFERENCED_FROM_EXPORT_DATA) {
|
|
316
|
+
reader.skip(4, "NamesReferencedFromExportDataCount");
|
|
317
|
+
}
|
|
318
|
+
if (fileVersionUE5 >= UE5_PAYLOAD_TOC) reader.skip(8, "PayloadTocOffset");
|
|
319
|
+
if (fileVersionUE5 >= UE5_DATA_RESOURCES) reader.skip(4, "DataResourceOffset");
|
|
320
|
+
}
|
|
321
|
+
assertUAsset(
|
|
322
|
+
reader.pos === nameOffset,
|
|
323
|
+
"INVALID_PACKAGE_SUMMARY",
|
|
324
|
+
"Package summary does not end where its name table begins",
|
|
325
|
+
{ summaryEnd: reader.pos, nameOffset, fileVersionUE4, fileVersionUE5, legacyFileVersion }
|
|
326
|
+
);
|
|
327
|
+
assertUAsset(
|
|
328
|
+
totalHeaderSize > 0 && totalHeaderSize <= bytes.byteLength && bulkDataStartOffset >= totalHeaderSize && bulkDataStartOffset <= bytes.byteLength,
|
|
329
|
+
"INVALID_PACKAGE_SUMMARY",
|
|
330
|
+
"Package header and bulk-data offsets do not bracket the package",
|
|
331
|
+
{ totalHeaderSize, bulkDataStartOffset, byteLength: bytes.byteLength }
|
|
332
|
+
);
|
|
333
|
+
return {
|
|
334
|
+
...summary,
|
|
335
|
+
totalHeaderSize,
|
|
336
|
+
bulkDataStartOffset,
|
|
337
|
+
nameCount,
|
|
338
|
+
nameOffset,
|
|
339
|
+
exportCount,
|
|
340
|
+
exportOffset
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function readPackageLayout(bytes) {
|
|
344
|
+
let summary;
|
|
345
|
+
try {
|
|
346
|
+
summary = readPackageSummary(bytes);
|
|
347
|
+
} catch {
|
|
348
|
+
return void 0;
|
|
349
|
+
}
|
|
350
|
+
if (summary.legacyFileVersion < OLDEST_MODELLED_LEGACY_VERSION) return void 0;
|
|
351
|
+
try {
|
|
352
|
+
return walkSummary(bytes, summary);
|
|
353
|
+
} catch (error) {
|
|
354
|
+
if (error instanceof UAssetError) return void 0;
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// src/bulk-data.ts
|
|
360
|
+
var BULK_DATA_FLAG = Object.freeze({
|
|
361
|
+
PAYLOAD_AT_END_OF_FILE: 1,
|
|
362
|
+
SERIALIZE_COMPRESSED_ZLIB: 2,
|
|
363
|
+
FORCE_INLINE_PAYLOAD: 64,
|
|
364
|
+
PAYLOAD_IN_SEPARATE_FILE: 256,
|
|
365
|
+
OPTIONAL_PAYLOAD: 2048,
|
|
366
|
+
MEMORY_MAPPED_PAYLOAD: 4096,
|
|
367
|
+
SIZE_64_BIT: 8192,
|
|
368
|
+
NO_OFFSET_FIX_UP: 65536
|
|
369
|
+
});
|
|
370
|
+
var MIN_PAYLOAD_BYTES = 16;
|
|
371
|
+
var MAX_COMPRESSION_CHUNK_BYTES = 64 * 1024 * 1024;
|
|
372
|
+
function bulkError(message, details) {
|
|
373
|
+
return new UAssetError("INVALID_BULK_DATA", message, details);
|
|
374
|
+
}
|
|
375
|
+
function storageOf(flags) {
|
|
376
|
+
const named = [
|
|
377
|
+
BULK_DATA_FLAG.PAYLOAD_IN_SEPARATE_FILE,
|
|
378
|
+
BULK_DATA_FLAG.FORCE_INLINE_PAYLOAD,
|
|
379
|
+
BULK_DATA_FLAG.PAYLOAD_AT_END_OF_FILE
|
|
380
|
+
].filter((bit) => (flags & bit) !== 0);
|
|
381
|
+
if (named.length !== 1) return void 0;
|
|
382
|
+
if (named[0] === BULK_DATA_FLAG.PAYLOAD_IN_SEPARATE_FILE) return "separate-file";
|
|
383
|
+
if (named[0] === BULK_DATA_FLAG.FORCE_INLINE_PAYLOAD) return "inline";
|
|
384
|
+
return "end-of-file";
|
|
385
|
+
}
|
|
386
|
+
function fileOf(flags, storage) {
|
|
387
|
+
if (storage !== "separate-file") return "uasset";
|
|
388
|
+
return (flags & BULK_DATA_FLAG.OPTIONAL_PAYLOAD) !== 0 ? "uptnl" : "ubulk";
|
|
389
|
+
}
|
|
390
|
+
function parseBulkDataHeader(bytes, offset, layout) {
|
|
391
|
+
const reader = new BinaryReader(bytes, offset);
|
|
392
|
+
const flags = reader.uint32("BulkDataFlags");
|
|
393
|
+
if ((flags & -131072) !== 0) {
|
|
394
|
+
throw bulkError("BulkDataFlags carries bits Unreal does not define", { offset, flags });
|
|
395
|
+
}
|
|
396
|
+
const storage = storageOf(flags);
|
|
397
|
+
if (storage === void 0) {
|
|
398
|
+
throw bulkError("BulkDataFlags names no payload storage", { offset, flags });
|
|
399
|
+
}
|
|
400
|
+
const wide = (flags & BULK_DATA_FLAG.SIZE_64_BIT) !== 0;
|
|
401
|
+
const elementCount = wide ? readInt64(reader, "ElementCount") : reader.int32("ElementCount");
|
|
402
|
+
const sizeOnDisk = wide ? readInt64(reader, "BulkDataSizeOnDisk") : reader.int32("BulkDataSizeOnDisk");
|
|
403
|
+
const offsetInFile = readInt64(reader, "BulkDataOffsetInFile");
|
|
404
|
+
const headerEnd = offset + reader.pos;
|
|
405
|
+
if (elementCount <= 0 || sizeOnDisk < MIN_PAYLOAD_BYTES || offsetInFile < 0) {
|
|
406
|
+
throw bulkError("Bulk-data header describes no readable payload", {
|
|
407
|
+
offset,
|
|
408
|
+
flags,
|
|
409
|
+
elementCount,
|
|
410
|
+
sizeOnDisk,
|
|
411
|
+
offsetInFile
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
const compression = (flags & BULK_DATA_FLAG.SERIALIZE_COMPRESSED_ZLIB) !== 0 ? "zlib" : "none";
|
|
415
|
+
if (compression === "none" && elementCount !== sizeOnDisk) {
|
|
416
|
+
throw bulkError("Uncompressed bulk data does not occupy one byte per element", {
|
|
417
|
+
offset,
|
|
418
|
+
flags,
|
|
419
|
+
elementCount,
|
|
420
|
+
sizeOnDisk
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
const fixUp = storage === "end-of-file" && (flags & BULK_DATA_FLAG.NO_OFFSET_FIX_UP) === 0 ? layout.bulkDataStartOffset : 0;
|
|
424
|
+
const payloadOffset = storage === "inline" ? headerEnd : offsetInFile + fixUp;
|
|
425
|
+
return {
|
|
426
|
+
headerOffset: offset,
|
|
427
|
+
headerEnd,
|
|
428
|
+
flags,
|
|
429
|
+
storage,
|
|
430
|
+
file: fileOf(flags, storage),
|
|
431
|
+
compression,
|
|
432
|
+
elementCount,
|
|
433
|
+
sizeOnDisk,
|
|
434
|
+
offsetInFile,
|
|
435
|
+
payloadOffset
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function readInt64(reader, context) {
|
|
439
|
+
reader.ensure(8, context);
|
|
440
|
+
const value = reader.view.getBigInt64(reader.pos, true);
|
|
441
|
+
reader.pos += 8;
|
|
442
|
+
if (value > BigInt(Number.MAX_SAFE_INTEGER) || value < BigInt(Number.MIN_SAFE_INTEGER)) {
|
|
443
|
+
throw bulkError(`${context} exceeds the safe integer range`, { value: value.toString() });
|
|
444
|
+
}
|
|
445
|
+
return Number(value);
|
|
446
|
+
}
|
|
447
|
+
function findBulkDataHeaders(bytes, layout, files = {}) {
|
|
448
|
+
const headers = [];
|
|
449
|
+
const scanEnd = Math.min(layout.bulkDataStartOffset, bytes.byteLength);
|
|
450
|
+
for (let offset = layout.totalHeaderSize; offset + 20 <= scanEnd; offset += 1) {
|
|
451
|
+
let header;
|
|
452
|
+
try {
|
|
453
|
+
header = parseBulkDataHeader(bytes, offset, layout);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
if (!(error instanceof UAssetError)) throw error;
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
if (!payloadFits(header, bytes, files, layout)) continue;
|
|
459
|
+
headers.push(header);
|
|
460
|
+
}
|
|
461
|
+
return headers;
|
|
462
|
+
}
|
|
463
|
+
function payloadFits(header, bytes, files, layout) {
|
|
464
|
+
if (header.storage === "separate-file") {
|
|
465
|
+
const sibling = siblingBytes(files, header.file);
|
|
466
|
+
if (sibling === void 0) return false;
|
|
467
|
+
return header.payloadOffset + header.sizeOnDisk <= sibling.byteLength;
|
|
468
|
+
}
|
|
469
|
+
if (header.storage === "inline") {
|
|
470
|
+
return header.payloadOffset + header.sizeOnDisk <= layout.bulkDataStartOffset;
|
|
471
|
+
}
|
|
472
|
+
return header.payloadOffset >= layout.bulkDataStartOffset && header.payloadOffset + header.sizeOnDisk <= bytes.byteLength;
|
|
473
|
+
}
|
|
474
|
+
function siblingBytes(files, file) {
|
|
475
|
+
const input = file === "uasset" ? void 0 : files[file];
|
|
476
|
+
if (input === void 0) return void 0;
|
|
477
|
+
if (input instanceof ArrayBuffer) return new Uint8Array(input);
|
|
478
|
+
return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
|
|
479
|
+
}
|
|
480
|
+
function decompressBulkData(container, codecs) {
|
|
481
|
+
const reader = new BinaryReader(container);
|
|
482
|
+
const tag = readInt64(reader, "compressed-chunk tag");
|
|
483
|
+
assertUAsset(
|
|
484
|
+
tag === PACKAGE_FILE_TAG,
|
|
485
|
+
"INVALID_BULK_DATA",
|
|
486
|
+
"Compressed bulk data does not start with the package tag",
|
|
487
|
+
{ tag: `0x${tag.toString(16)}` }
|
|
488
|
+
);
|
|
489
|
+
const chunkSize = readInt64(reader, "loading compression chunk size");
|
|
490
|
+
const totalCompressed = readInt64(reader, "total compressed size");
|
|
491
|
+
const totalRaw = readInt64(reader, "total uncompressed size");
|
|
492
|
+
assertUAsset(
|
|
493
|
+
chunkSize > 0 && chunkSize <= MAX_COMPRESSION_CHUNK_BYTES && totalCompressed > 0 && totalRaw > 0,
|
|
494
|
+
"INVALID_BULK_DATA",
|
|
495
|
+
"Compressed bulk-data totals are not readable sizes",
|
|
496
|
+
{ chunkSize, totalCompressed, totalRaw }
|
|
497
|
+
);
|
|
498
|
+
const chunkCount = Math.ceil(totalRaw / chunkSize);
|
|
499
|
+
const chunks = [];
|
|
500
|
+
let compressedSum = 0;
|
|
501
|
+
let rawSum = 0;
|
|
502
|
+
for (let index = 0; index < chunkCount; index += 1) {
|
|
503
|
+
const compressed = readInt64(reader, `chunk ${index} compressed size`);
|
|
504
|
+
const raw = readInt64(reader, `chunk ${index} uncompressed size`);
|
|
505
|
+
assertUAsset(
|
|
506
|
+
compressed > 0 && raw > 0 && raw <= chunkSize,
|
|
507
|
+
"INVALID_BULK_DATA",
|
|
508
|
+
`Compressed bulk-data chunk ${index} has an unreadable size`,
|
|
509
|
+
{ index, compressed, raw, chunkSize }
|
|
510
|
+
);
|
|
511
|
+
compressedSum += compressed;
|
|
512
|
+
rawSum += raw;
|
|
513
|
+
chunks.push({ compressed, raw });
|
|
514
|
+
}
|
|
515
|
+
assertUAsset(
|
|
516
|
+
compressedSum === totalCompressed && rawSum === totalRaw,
|
|
517
|
+
"INVALID_BULK_DATA",
|
|
518
|
+
"Compressed bulk-data chunk sizes disagree with the container totals",
|
|
519
|
+
{ compressedSum, totalCompressed, rawSum, totalRaw }
|
|
520
|
+
);
|
|
521
|
+
if (codecs.zlib === void 0) {
|
|
522
|
+
throw new UAssetError(
|
|
523
|
+
"MISSING_CODEC",
|
|
524
|
+
"This bulk-data payload is zlib-compressed and no `zlib` codec was supplied",
|
|
525
|
+
{ compression: "zlib", totalCompressed, totalRaw }
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
const out = new Uint8Array(totalRaw);
|
|
529
|
+
let written = 0;
|
|
530
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
531
|
+
const block = reader.raw(chunk.compressed, `chunk ${index} data`);
|
|
532
|
+
const inflated = codecs.zlib(block, chunk.raw);
|
|
533
|
+
assertUAsset(
|
|
534
|
+
inflated.byteLength === chunk.raw,
|
|
535
|
+
"CODEC_SIZE_MISMATCH",
|
|
536
|
+
`zlib codec returned ${inflated.byteLength} bytes for chunk ${index}, expected ${chunk.raw}`,
|
|
537
|
+
{ index, expected: chunk.raw, actual: inflated.byteLength }
|
|
538
|
+
);
|
|
539
|
+
out.set(inflated, written);
|
|
540
|
+
written += inflated.byteLength;
|
|
541
|
+
}
|
|
542
|
+
assertUAsset(
|
|
543
|
+
written === totalRaw,
|
|
544
|
+
"INCOMPLETE_DECOMPRESSION",
|
|
545
|
+
"Compressed bulk data did not decompress to its declared size",
|
|
546
|
+
{ written, totalRaw }
|
|
547
|
+
);
|
|
548
|
+
return out;
|
|
549
|
+
}
|
|
550
|
+
function resolveBulkDataPayload(bytes, header, options = {}) {
|
|
551
|
+
const files = options.files ?? {};
|
|
552
|
+
let source = bytes;
|
|
553
|
+
if (header.storage === "separate-file") {
|
|
554
|
+
const sibling = siblingBytes(files, header.file);
|
|
555
|
+
if (sibling === void 0) {
|
|
556
|
+
throw new UAssetError(
|
|
557
|
+
"MISSING_BULK_DATA_FILE",
|
|
558
|
+
`This package's bulk payload was written to its sibling .${header.file} file, whose bytes were not supplied`,
|
|
559
|
+
{
|
|
560
|
+
file: header.file,
|
|
561
|
+
flags: header.flags,
|
|
562
|
+
sizeOnDisk: header.sizeOnDisk,
|
|
563
|
+
offsetInFile: header.offsetInFile
|
|
564
|
+
}
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
source = sibling;
|
|
568
|
+
}
|
|
569
|
+
const end = header.payloadOffset + header.sizeOnDisk;
|
|
570
|
+
assertUAsset(
|
|
571
|
+
header.payloadOffset >= 0 && end <= source.byteLength,
|
|
572
|
+
"INVALID_BULK_DATA",
|
|
573
|
+
"Bulk-data payload runs past the file it was read from",
|
|
574
|
+
{
|
|
575
|
+
file: header.file,
|
|
576
|
+
payloadOffset: header.payloadOffset,
|
|
577
|
+
sizeOnDisk: header.sizeOnDisk,
|
|
578
|
+
byteLength: source.byteLength
|
|
579
|
+
}
|
|
580
|
+
);
|
|
581
|
+
const stored = source.subarray(header.payloadOffset, end);
|
|
582
|
+
if (header.compression === "none") return stored;
|
|
583
|
+
return decompressBulkData(stored, { zlib: options.zlib });
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/geometry.ts
|
|
587
|
+
function convertVector(x, y, z, convertCoordinates) {
|
|
588
|
+
return convertCoordinates ? [x, z, -y] : [x, y, z];
|
|
589
|
+
}
|
|
590
|
+
function computeBounds(positions) {
|
|
591
|
+
const min = [
|
|
592
|
+
Number.POSITIVE_INFINITY,
|
|
593
|
+
Number.POSITIVE_INFINITY,
|
|
594
|
+
Number.POSITIVE_INFINITY
|
|
595
|
+
];
|
|
596
|
+
const max = [
|
|
597
|
+
Number.NEGATIVE_INFINITY,
|
|
598
|
+
Number.NEGATIVE_INFINITY,
|
|
599
|
+
Number.NEGATIVE_INFINITY
|
|
600
|
+
];
|
|
601
|
+
for (let index = 0; index + 2 < positions.length; index += 3) {
|
|
602
|
+
for (let component = 0; component < 3; component += 1) {
|
|
603
|
+
const value = positions[index + component];
|
|
604
|
+
if (value === void 0) {
|
|
605
|
+
throw new UAssetError(
|
|
606
|
+
"INVALID_MESH_REFERENCE",
|
|
607
|
+
"Position data ended before its declared count",
|
|
608
|
+
{ index }
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
min[component] = Math.min(min[component] ?? value, value);
|
|
612
|
+
max[component] = Math.max(max[component] ?? value, value);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return { min, max };
|
|
616
|
+
}
|
|
617
|
+
function assembleGeometry(positions, normals, uvs, trianglesByGroup, materialNameFor, sourceStats) {
|
|
618
|
+
const indexCount = [...trianglesByGroup.values()].reduce((sum, group) => sum + group.length, 0);
|
|
619
|
+
const indices = positions.length / 3 > 65535 ? new Uint32Array(indexCount) : new Uint16Array(indexCount);
|
|
620
|
+
const sections = [];
|
|
621
|
+
let cursor = 0;
|
|
622
|
+
for (const [group, groupIndices] of trianglesByGroup) {
|
|
623
|
+
indices.set(groupIndices, cursor);
|
|
624
|
+
sections.push({
|
|
625
|
+
materialIndex: sections.length,
|
|
626
|
+
sectionIndex: group,
|
|
627
|
+
materialName: materialNameFor(group),
|
|
628
|
+
start: cursor,
|
|
629
|
+
count: groupIndices.length
|
|
630
|
+
});
|
|
631
|
+
cursor += groupIndices.length;
|
|
632
|
+
}
|
|
633
|
+
return { positions, normals, uvs, indices, sections, sourceStats };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// src/mesh-attributes-ue4.ts
|
|
637
|
+
var MAX_MESH_ELEMENTS = 1e7;
|
|
638
|
+
var MAX_ATTRIBUTE_VALUES = 1e8;
|
|
639
|
+
function readInt32Array(reader, count, what) {
|
|
640
|
+
const values = new Int32Array(count);
|
|
641
|
+
for (let index = 0; index < count; index += 1) values[index] = reader.int32(what);
|
|
642
|
+
return values;
|
|
643
|
+
}
|
|
644
|
+
function meshError(message, details) {
|
|
645
|
+
return new UAssetError("INVALID_MESH_DESCRIPTION", message, details);
|
|
646
|
+
}
|
|
647
|
+
var ATTRIBUTE_TYPES = Object.freeze([
|
|
648
|
+
{ name: "FVector4f", components: 4, byteSize: 16, kind: "float" },
|
|
649
|
+
{ name: "FVector3f", components: 3, byteSize: 12, kind: "float" },
|
|
650
|
+
{ name: "FVector2f", components: 2, byteSize: 8, kind: "float" },
|
|
651
|
+
{ name: "float", components: 1, byteSize: 4, kind: "float" },
|
|
652
|
+
{ name: "int32", components: 1, byteSize: 4, kind: "int" },
|
|
653
|
+
{ name: "bool", components: 1, byteSize: 1, kind: "bool" },
|
|
654
|
+
{ name: "FName", components: 1, byteSize: void 0, kind: "name" }
|
|
655
|
+
]);
|
|
656
|
+
function readAttributeEntry(reader, rawName) {
|
|
657
|
+
const name = rawName.trimEnd();
|
|
658
|
+
const typeIndex = reader.uint32(`${name} type`);
|
|
659
|
+
const type = ATTRIBUTE_TYPES[typeIndex];
|
|
660
|
+
assertUAsset(type !== void 0, "INVALID_MESH_DESCRIPTION", "Unknown attribute type", {
|
|
661
|
+
attribute: name,
|
|
662
|
+
typeIndex
|
|
663
|
+
});
|
|
664
|
+
const numElements = reader.int32(`${name} element count`);
|
|
665
|
+
const numChannels = reader.int32(`${name} channel count`);
|
|
666
|
+
if (numElements < 0 || numElements > MAX_MESH_ELEMENTS || numChannels < 0 || numChannels > 128) {
|
|
667
|
+
throw meshError(`Invalid attribute shape for ${name}`, { name, numElements, numChannels });
|
|
668
|
+
}
|
|
669
|
+
let values = new Float32Array(0);
|
|
670
|
+
for (let channel = 0; channel < numChannels; channel += 1) {
|
|
671
|
+
const channelValues2 = readAttributeChannel(reader, type, name, numElements);
|
|
672
|
+
if (channel === 0) values = channelValues2;
|
|
673
|
+
}
|
|
674
|
+
readDefaultValue(reader, type);
|
|
675
|
+
reader.uint32(`${name} flags`);
|
|
676
|
+
return { name, type: type.name, components: type.components, numElements, values };
|
|
677
|
+
}
|
|
678
|
+
function readAttributeChannel(reader, type, name, numElements) {
|
|
679
|
+
if (type.kind === "name") {
|
|
680
|
+
const count2 = reader.int32(`${name} FName count`);
|
|
681
|
+
if (count2 < 0 || count2 > MAX_ATTRIBUTE_VALUES) {
|
|
682
|
+
throw meshError(`Invalid FName count for ${name}`, { name, count: count2 });
|
|
683
|
+
}
|
|
684
|
+
assertUAsset(
|
|
685
|
+
count2 === numElements,
|
|
686
|
+
"INVALID_MESH_DESCRIPTION",
|
|
687
|
+
`Serialized value count mismatch for ${name}`,
|
|
688
|
+
{ name, expected: numElements, actual: count2 }
|
|
689
|
+
);
|
|
690
|
+
const names = new Array(count2);
|
|
691
|
+
for (let index = 0; index < count2; index += 1)
|
|
692
|
+
names[index] = reader.fstring(`${name}[${index}]`);
|
|
693
|
+
return names;
|
|
694
|
+
}
|
|
695
|
+
const elementSize = reader.int32(`${name} bulk element size`);
|
|
696
|
+
const count = reader.int32(`${name} bulk element count`);
|
|
697
|
+
assertUAsset(
|
|
698
|
+
type.byteSize !== void 0 && elementSize === type.byteSize,
|
|
699
|
+
"INVALID_MESH_DESCRIPTION",
|
|
700
|
+
`${name} has an unexpected element size`,
|
|
701
|
+
{ name, expected: type.byteSize, actual: elementSize }
|
|
702
|
+
);
|
|
703
|
+
assertUAsset(
|
|
704
|
+
count === numElements,
|
|
705
|
+
"INVALID_MESH_DESCRIPTION",
|
|
706
|
+
`Serialized value count mismatch for ${name}`,
|
|
707
|
+
{ name, expected: numElements, actual: count }
|
|
708
|
+
);
|
|
709
|
+
const requiredBytes = count * elementSize;
|
|
710
|
+
assertUAsset(
|
|
711
|
+
Number.isSafeInteger(requiredBytes) && requiredBytes <= reader.remaining,
|
|
712
|
+
"INVALID_MESH_DESCRIPTION",
|
|
713
|
+
`${name} exceeds the remaining MeshDescription payload`,
|
|
714
|
+
{ name, requiredBytes, remaining: reader.remaining }
|
|
715
|
+
);
|
|
716
|
+
if (type.kind === "float") {
|
|
717
|
+
const values2 = new Float32Array(count * type.components);
|
|
718
|
+
for (let index = 0; index < values2.length; index += 1) values2[index] = reader.float32(name);
|
|
719
|
+
return values2;
|
|
720
|
+
}
|
|
721
|
+
if (type.kind === "int") return readInt32Array(reader, count, name);
|
|
722
|
+
const values = new Uint8Array(count);
|
|
723
|
+
values.set(reader.raw(count, name));
|
|
724
|
+
return values;
|
|
725
|
+
}
|
|
726
|
+
function readDefaultValue(reader, type) {
|
|
727
|
+
switch (type.kind) {
|
|
728
|
+
case "float":
|
|
729
|
+
for (let index = 0; index < type.components; index += 1) reader.float32(type.name);
|
|
730
|
+
return;
|
|
731
|
+
case "int":
|
|
732
|
+
case "bool":
|
|
733
|
+
reader.int32(type.name);
|
|
734
|
+
return;
|
|
735
|
+
case "name":
|
|
736
|
+
reader.fstring(type.name);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
function readAttributeSet(reader, what, expectedElements) {
|
|
741
|
+
const numElements = reader.int32(`${what} attribute-set element count`);
|
|
742
|
+
const attributeCount = reader.int32(`${what} attribute count`);
|
|
743
|
+
if (numElements < 0 || numElements > MAX_MESH_ELEMENTS || attributeCount < 0 || attributeCount > 1024) {
|
|
744
|
+
throw meshError(`Invalid ${what} attribute set`, { what, numElements, attributeCount });
|
|
745
|
+
}
|
|
746
|
+
assertUAsset(
|
|
747
|
+
numElements === expectedElements,
|
|
748
|
+
"INVALID_MESH_DESCRIPTION",
|
|
749
|
+
`${what} attribute set disagrees with its element container`,
|
|
750
|
+
{ what, numElements, expectedElements }
|
|
751
|
+
);
|
|
752
|
+
const attributes = /* @__PURE__ */ new Map();
|
|
753
|
+
for (let index = 0; index < attributeCount; index += 1) {
|
|
754
|
+
const rawName = reader.fstring(`${what} attribute name ${index}`);
|
|
755
|
+
const entry = readAttributeEntry(reader, rawName);
|
|
756
|
+
assertUAsset(
|
|
757
|
+
entry.numElements === numElements,
|
|
758
|
+
"INVALID_MESH_DESCRIPTION",
|
|
759
|
+
`${what} attribute "${entry.name}" disagrees with its set`,
|
|
760
|
+
{ what, attribute: entry.name, numElements: entry.numElements, expected: numElements }
|
|
761
|
+
);
|
|
762
|
+
attributes.set(entry.name, entry);
|
|
763
|
+
}
|
|
764
|
+
return attributes;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// src/mesh-description-ue4.ts
|
|
768
|
+
var MAX_MESH_ELEMENTS2 = 1e7;
|
|
769
|
+
function meshError2(message, details) {
|
|
770
|
+
return new UAssetError("INVALID_MESH_DESCRIPTION", message, details);
|
|
771
|
+
}
|
|
772
|
+
function readBitArray(reader, what) {
|
|
773
|
+
const offset = reader.pos;
|
|
774
|
+
const numBits = reader.int32(`${what} TBitArray NumBits`);
|
|
775
|
+
if (numBits < 0 || numBits > MAX_MESH_ELEMENTS2) {
|
|
776
|
+
throw meshError2(`Invalid ${what} allocation length`, { offset, numBits });
|
|
777
|
+
}
|
|
778
|
+
const wordCount = Math.ceil(numBits / 32);
|
|
779
|
+
const words = new Uint32Array(wordCount);
|
|
780
|
+
for (let index = 0; index < wordCount; index += 1) {
|
|
781
|
+
words[index] = reader.uint32(`${what} TBitArray word`);
|
|
782
|
+
}
|
|
783
|
+
const validIds = [];
|
|
784
|
+
for (let id = 0; id < numBits; id += 1) {
|
|
785
|
+
const word = words[id >>> 5];
|
|
786
|
+
if (word !== void 0 && (word & 1 << (id & 31)) !== 0) validIds.push(id);
|
|
787
|
+
}
|
|
788
|
+
return { numBits, validIds };
|
|
789
|
+
}
|
|
790
|
+
function parseMeshDescriptionUe4(input, offset = 0) {
|
|
791
|
+
const reader = new BinaryReader(input, offset);
|
|
792
|
+
const vertices = readBitArray(reader, "VertexArray");
|
|
793
|
+
const vertexInstances = readBitArray(reader, "VertexInstanceArray");
|
|
794
|
+
const vertexInstanceVertices = new Int32Array(vertexInstances.numBits).fill(-1);
|
|
795
|
+
for (const instanceId of vertexInstances.validIds) {
|
|
796
|
+
vertexInstanceVertices[instanceId] = reader.int32("FMeshVertexInstance.VertexID");
|
|
797
|
+
}
|
|
798
|
+
const edges = readBitArray(reader, "EdgeArray");
|
|
799
|
+
reader.skip(edges.validIds.length * 8, "FMeshEdge.VertexIDs");
|
|
800
|
+
const polygons = readBitArray(reader, "PolygonArray");
|
|
801
|
+
const polygonGroups = new Int32Array(polygons.numBits).fill(-1);
|
|
802
|
+
for (const polygonId of polygons.validIds) {
|
|
803
|
+
const triangleCount = reader.int32("FMeshPolygon.TriangleIDs count");
|
|
804
|
+
if (triangleCount < 0 || triangleCount > MAX_MESH_ELEMENTS2) {
|
|
805
|
+
throw meshError2("Invalid polygon triangulation count", { polygonId, triangleCount });
|
|
806
|
+
}
|
|
807
|
+
reader.skip(triangleCount * 4, "FMeshPolygon.TriangleIDs");
|
|
808
|
+
polygonGroups[polygonId] = reader.int32("FMeshPolygon.PolygonGroupID");
|
|
809
|
+
}
|
|
810
|
+
const polygonGroupArray = readBitArray(reader, "PolygonGroupArray");
|
|
811
|
+
const vertexAttributes = readAttributeSet(reader, "Vertex", vertices.numBits);
|
|
812
|
+
const vertexInstanceAttributes = readAttributeSet(
|
|
813
|
+
reader,
|
|
814
|
+
"VertexInstance",
|
|
815
|
+
vertexInstances.numBits
|
|
816
|
+
);
|
|
817
|
+
readAttributeSet(reader, "Edge", edges.numBits);
|
|
818
|
+
readAttributeSet(reader, "Polygon", polygons.numBits);
|
|
819
|
+
const polygonGroupAttributes = readAttributeSet(
|
|
820
|
+
reader,
|
|
821
|
+
"PolygonGroup",
|
|
822
|
+
polygonGroupArray.numBits
|
|
823
|
+
);
|
|
824
|
+
const triangles = readBitArray(reader, "TriangleArray");
|
|
825
|
+
const triangleVertexInstances = new Int32Array(triangles.numBits * 3).fill(-1);
|
|
826
|
+
const trianglePolygons = new Int32Array(triangles.numBits).fill(-1);
|
|
827
|
+
for (const triangleId of triangles.validIds) {
|
|
828
|
+
for (let corner = 0; corner < 3; corner += 1) {
|
|
829
|
+
triangleVertexInstances[triangleId * 3 + corner] = reader.int32(
|
|
830
|
+
"FMeshTriangle.VertexInstanceIDs"
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
trianglePolygons[triangleId] = reader.int32("FMeshTriangle.PolygonID");
|
|
834
|
+
}
|
|
835
|
+
readAttributeSet(reader, "Triangle", triangles.numBits);
|
|
836
|
+
validateReferences({
|
|
837
|
+
vertices,
|
|
838
|
+
vertexInstances,
|
|
839
|
+
polygons,
|
|
840
|
+
polygonGroupCount: polygonGroupArray.numBits,
|
|
841
|
+
triangles,
|
|
842
|
+
vertexInstanceVertices,
|
|
843
|
+
polygonGroups,
|
|
844
|
+
triangleVertexInstances,
|
|
845
|
+
trianglePolygons
|
|
846
|
+
});
|
|
847
|
+
assertUAsset(
|
|
848
|
+
reader.remaining === 0,
|
|
849
|
+
"INVALID_MESH_DESCRIPTION",
|
|
850
|
+
"UE4 MeshDescription walk did not consume its payload exactly",
|
|
851
|
+
{ consumed: reader.pos, byteLength: reader.byteLength, trailing: reader.remaining }
|
|
852
|
+
);
|
|
853
|
+
return {
|
|
854
|
+
byteLength: reader.pos,
|
|
855
|
+
vertexCount: vertices.validIds.length,
|
|
856
|
+
vertexSlots: vertices.numBits,
|
|
857
|
+
validVertexIds: vertices.validIds,
|
|
858
|
+
validVertexInstanceIds: vertexInstances.validIds,
|
|
859
|
+
vertexInstanceVertices,
|
|
860
|
+
polygonGroups,
|
|
861
|
+
polygonGroupCount: polygonGroupArray.numBits,
|
|
862
|
+
validTriangleIds: triangles.validIds,
|
|
863
|
+
triangleVertexInstances,
|
|
864
|
+
trianglePolygons,
|
|
865
|
+
vertexAttributes,
|
|
866
|
+
vertexInstanceAttributes,
|
|
867
|
+
polygonGroupAttributes
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
function validateReferences(inputs) {
|
|
871
|
+
assertUAsset(
|
|
872
|
+
inputs.vertices.validIds.length > 0 && inputs.vertexInstances.validIds.length > 0 && inputs.triangles.validIds.length > 0,
|
|
873
|
+
"INVALID_MESH_DESCRIPTION",
|
|
874
|
+
"UE4 MeshDescription contains no renderable geometry",
|
|
875
|
+
{
|
|
876
|
+
vertices: inputs.vertices.validIds.length,
|
|
877
|
+
vertexInstances: inputs.vertexInstances.validIds.length,
|
|
878
|
+
triangles: inputs.triangles.validIds.length
|
|
879
|
+
}
|
|
880
|
+
);
|
|
881
|
+
const allocatedVertices = new Set(inputs.vertices.validIds);
|
|
882
|
+
const allocatedInstances = new Set(inputs.vertexInstances.validIds);
|
|
883
|
+
const allocatedPolygons = new Set(inputs.polygons.validIds);
|
|
884
|
+
for (const instanceId of inputs.vertexInstances.validIds) {
|
|
885
|
+
const vertexId = inputs.vertexInstanceVertices[instanceId];
|
|
886
|
+
if (vertexId === void 0 || !allocatedVertices.has(vertexId)) {
|
|
887
|
+
throw meshError2("Vertex instance references a vertex the file did not allocate", {
|
|
888
|
+
instanceId,
|
|
889
|
+
vertexId
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
for (const polygonId of inputs.polygons.validIds) {
|
|
894
|
+
const groupId = inputs.polygonGroups[polygonId];
|
|
895
|
+
if (groupId === void 0 || groupId < 0 || groupId >= inputs.polygonGroupCount) {
|
|
896
|
+
throw meshError2("Polygon references an invalid polygon group", {
|
|
897
|
+
polygonId,
|
|
898
|
+
groupId,
|
|
899
|
+
polygonGroupCount: inputs.polygonGroupCount
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
for (const triangleId of inputs.triangles.validIds) {
|
|
904
|
+
for (let corner = 0; corner < 3; corner += 1) {
|
|
905
|
+
const instanceId = inputs.triangleVertexInstances[triangleId * 3 + corner];
|
|
906
|
+
if (instanceId === void 0 || !allocatedInstances.has(instanceId)) {
|
|
907
|
+
throw meshError2("Triangle references a vertex instance the file did not allocate", {
|
|
908
|
+
triangleId,
|
|
909
|
+
corner,
|
|
910
|
+
instanceId
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
const polygonId = inputs.trianglePolygons[triangleId];
|
|
915
|
+
if (polygonId === void 0 || !allocatedPolygons.has(polygonId)) {
|
|
916
|
+
throw meshError2("Triangle references a polygon the file did not allocate", {
|
|
917
|
+
triangleId,
|
|
918
|
+
polygonId
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
function looksLikeMeshDescriptionUe4(bytes, offset = 0) {
|
|
924
|
+
if (offset < 0 || offset + 12 > bytes.byteLength) return false;
|
|
925
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
926
|
+
const vertexBits = view.getInt32(offset, true);
|
|
927
|
+
if (vertexBits <= 0 || vertexBits > MAX_MESH_ELEMENTS2) return false;
|
|
928
|
+
const instanceOffset = offset + 4 + Math.ceil(vertexBits / 32) * 4;
|
|
929
|
+
if (instanceOffset + 4 > bytes.byteLength) return false;
|
|
930
|
+
const instanceBits = view.getInt32(instanceOffset, true);
|
|
931
|
+
return instanceBits >= vertexBits && instanceBits <= MAX_MESH_ELEMENTS2;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// src/raw-mesh-geometry.ts
|
|
935
|
+
function rawFloat(values, index, what) {
|
|
936
|
+
const value = values[index];
|
|
937
|
+
if (typeof value !== "number") {
|
|
938
|
+
throw new UAssetError("INVALID_RAW_MESH", `${what} ended before its declared element count`, {
|
|
939
|
+
index
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
return value;
|
|
943
|
+
}
|
|
944
|
+
function rawWedge(values, index) {
|
|
945
|
+
const value = values[index];
|
|
946
|
+
if (typeof value !== "number") {
|
|
947
|
+
throw new UAssetError(
|
|
948
|
+
"INVALID_RAW_MESH",
|
|
949
|
+
"FRawMesh wedge table ended before its declared count",
|
|
950
|
+
{ index }
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
return value;
|
|
954
|
+
}
|
|
955
|
+
function rawInt(values, index, what) {
|
|
956
|
+
const value = values[index];
|
|
957
|
+
if (typeof value !== "number") {
|
|
958
|
+
throw new UAssetError("INVALID_RAW_MESH", `${what} ended before its declared element count`, {
|
|
959
|
+
index
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
return value;
|
|
963
|
+
}
|
|
964
|
+
function buildGeometryFromRawMesh(mesh, options) {
|
|
965
|
+
const {
|
|
966
|
+
vertexCount,
|
|
967
|
+
vertexPositions,
|
|
968
|
+
wedgeIndices,
|
|
969
|
+
wedgeNormals,
|
|
970
|
+
wedgeUvs,
|
|
971
|
+
faceMaterialIndices
|
|
972
|
+
} = mesh;
|
|
973
|
+
const wedgeCount = wedgeIndices.length;
|
|
974
|
+
const uvSet = wedgeUvs[0];
|
|
975
|
+
assertUAsset(uvSet !== void 0, "INVALID_RAW_MESH", "FRawMesh has no texture coordinates");
|
|
976
|
+
const positions = new Float32Array(wedgeCount * 3);
|
|
977
|
+
const normals = wedgeNormals ? new Float32Array(wedgeCount * 3) : void 0;
|
|
978
|
+
const uvs = new Float32Array(wedgeCount * 2);
|
|
979
|
+
for (let wedge = 0; wedge < wedgeCount; wedge += 1) {
|
|
980
|
+
const vertexId = rawWedge(wedgeIndices, wedge);
|
|
981
|
+
const [x, y, z] = convertVector(
|
|
982
|
+
rawFloat(vertexPositions, vertexId * 3, "VertexPositions"),
|
|
983
|
+
rawFloat(vertexPositions, vertexId * 3 + 1, "VertexPositions"),
|
|
984
|
+
rawFloat(vertexPositions, vertexId * 3 + 2, "VertexPositions"),
|
|
985
|
+
options.convertCoordinates
|
|
986
|
+
);
|
|
987
|
+
positions[wedge * 3] = x;
|
|
988
|
+
positions[wedge * 3 + 1] = y;
|
|
989
|
+
positions[wedge * 3 + 2] = z;
|
|
990
|
+
if (normals && wedgeNormals) {
|
|
991
|
+
const [nx, ny, nz] = convertVector(
|
|
992
|
+
rawFloat(wedgeNormals, wedge * 3, "WedgeTangentZ"),
|
|
993
|
+
rawFloat(wedgeNormals, wedge * 3 + 1, "WedgeTangentZ"),
|
|
994
|
+
rawFloat(wedgeNormals, wedge * 3 + 2, "WedgeTangentZ"),
|
|
995
|
+
options.convertCoordinates
|
|
996
|
+
);
|
|
997
|
+
normals[wedge * 3] = nx;
|
|
998
|
+
normals[wedge * 3 + 1] = ny;
|
|
999
|
+
normals[wedge * 3 + 2] = nz;
|
|
1000
|
+
}
|
|
1001
|
+
uvs[wedge * 2] = rawFloat(uvSet, wedge * 2, "WedgeTexCoords");
|
|
1002
|
+
const v = rawFloat(uvSet, wedge * 2 + 1, "WedgeTexCoords");
|
|
1003
|
+
uvs[wedge * 2 + 1] = options.flipV ? 1 - v : v;
|
|
1004
|
+
}
|
|
1005
|
+
const trianglesByGroup = /* @__PURE__ */ new Map();
|
|
1006
|
+
for (let face = 0; face < faceMaterialIndices.length; face += 1) {
|
|
1007
|
+
const base = face * 3;
|
|
1008
|
+
const a = base;
|
|
1009
|
+
const b = options.flipWinding ? base + 2 : base + 1;
|
|
1010
|
+
const c = options.flipWinding ? base + 1 : base + 2;
|
|
1011
|
+
const materialIndex = rawInt(faceMaterialIndices, face, "FaceMaterialIndices");
|
|
1012
|
+
const group = materialIndex >= 0 ? materialIndex : 0;
|
|
1013
|
+
const groupTriangles = trianglesByGroup.get(group) ?? [];
|
|
1014
|
+
groupTriangles.push(a, b, c);
|
|
1015
|
+
trianglesByGroup.set(group, groupTriangles);
|
|
1016
|
+
}
|
|
1017
|
+
return assembleGeometry(positions, normals, uvs, trianglesByGroup, (group) => String(group), {
|
|
1018
|
+
vertices: vertexCount,
|
|
1019
|
+
vertexInstances: wedgeCount,
|
|
1020
|
+
triangles: faceMaterialIndices.length
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// src/raw-mesh.ts
|
|
1025
|
+
var RAW_MESH_VER_INITIAL = 0;
|
|
1026
|
+
var RAW_MESH_VER_REMOVE_ZERO_TRIANGLE_SECTIONS = 1;
|
|
1027
|
+
var MAX_MESH_TEXTURE_COORDS = 8;
|
|
1028
|
+
var MAX_ARRAY_ELEMENTS = 2e8;
|
|
1029
|
+
function parseRawMesh(bytes, offset = 0) {
|
|
1030
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1031
|
+
let pos = offset;
|
|
1032
|
+
function array(elementSize, label) {
|
|
1033
|
+
if (pos + 4 > bytes.byteLength) {
|
|
1034
|
+
throw rawMeshError(`${label} count is truncated`, { offset, position: pos });
|
|
1035
|
+
}
|
|
1036
|
+
const count = view.getInt32(pos, true);
|
|
1037
|
+
pos += 4;
|
|
1038
|
+
if (count < 0 || count > MAX_ARRAY_ELEMENTS) {
|
|
1039
|
+
throw rawMeshError(`${label} has an invalid element count`, { offset, count });
|
|
1040
|
+
}
|
|
1041
|
+
const start = pos;
|
|
1042
|
+
const end = pos + count * elementSize;
|
|
1043
|
+
if (end > bytes.byteLength) {
|
|
1044
|
+
throw rawMeshError(`${label} runs past the payload`, {
|
|
1045
|
+
offset,
|
|
1046
|
+
count,
|
|
1047
|
+
end,
|
|
1048
|
+
byteLength: bytes.byteLength
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
pos = end;
|
|
1052
|
+
return { count, start };
|
|
1053
|
+
}
|
|
1054
|
+
const version = view.getInt32(pos, true);
|
|
1055
|
+
pos += 4;
|
|
1056
|
+
if (version !== RAW_MESH_VER_INITIAL && version !== RAW_MESH_VER_REMOVE_ZERO_TRIANGLE_SECTIONS) {
|
|
1057
|
+
throw rawMeshError("Unsupported FRawMesh version", { offset, version });
|
|
1058
|
+
}
|
|
1059
|
+
const licenseeVersion = view.getInt32(pos, true);
|
|
1060
|
+
pos += 4;
|
|
1061
|
+
if (licenseeVersion !== 0) {
|
|
1062
|
+
throw rawMeshError("FRawMesh licensee version is not the stock layout", {
|
|
1063
|
+
offset,
|
|
1064
|
+
licenseeVersion
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
const faces = array(4, "FaceMaterialIndices");
|
|
1068
|
+
const smoothing = array(4, "FaceSmoothingMasks");
|
|
1069
|
+
const vertices = array(12, "VertexPositions");
|
|
1070
|
+
const wedges = array(4, "WedgeIndices");
|
|
1071
|
+
array(12, "WedgeTangentX");
|
|
1072
|
+
array(12, "WedgeTangentY");
|
|
1073
|
+
const normals = array(12, "WedgeTangentZ");
|
|
1074
|
+
const uvChannels = [];
|
|
1075
|
+
for (let channel = 0; channel < MAX_MESH_TEXTURE_COORDS; channel += 1) {
|
|
1076
|
+
uvChannels.push(array(8, `WedgeTexCoords[${channel}]`));
|
|
1077
|
+
}
|
|
1078
|
+
array(4, "WedgeColors");
|
|
1079
|
+
if (version >= RAW_MESH_VER_REMOVE_ZERO_TRIANGLE_SECTIONS) {
|
|
1080
|
+
array(4, "ImportedMaterialNames");
|
|
1081
|
+
}
|
|
1082
|
+
const byteLength = pos - offset;
|
|
1083
|
+
const faceCount = faces.count;
|
|
1084
|
+
const wedgeCount = wedges.count;
|
|
1085
|
+
const vertexCount = vertices.count;
|
|
1086
|
+
validateRawMeshCounts(offset, {
|
|
1087
|
+
faceCount,
|
|
1088
|
+
wedgeCount,
|
|
1089
|
+
vertexCount,
|
|
1090
|
+
smoothingCount: smoothing.count,
|
|
1091
|
+
normalCount: normals.count,
|
|
1092
|
+
uvChannels
|
|
1093
|
+
});
|
|
1094
|
+
const nonEmptyUvChannels = uvChannels.filter((channel) => channel.count > 0);
|
|
1095
|
+
for (let wedge = 0; wedge < wedgeCount; wedge += 1) {
|
|
1096
|
+
if (view.getUint32(wedges.start + wedge * 4, true) >= vertexCount) {
|
|
1097
|
+
throw rawMeshError("FRawMesh wedge index is out of range for the vertex count", {
|
|
1098
|
+
offset,
|
|
1099
|
+
wedge,
|
|
1100
|
+
vertexCount
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
const faceMaterialIndices = new Int32Array(faceCount);
|
|
1105
|
+
for (let face = 0; face < faceCount; face += 1) {
|
|
1106
|
+
faceMaterialIndices[face] = view.getInt32(faces.start + face * 4, true);
|
|
1107
|
+
}
|
|
1108
|
+
const vertexPositions = new Float32Array(vertexCount * 3);
|
|
1109
|
+
for (let index = 0; index < vertexCount * 3; index += 1) {
|
|
1110
|
+
vertexPositions[index] = view.getFloat32(vertices.start + index * 4, true);
|
|
1111
|
+
}
|
|
1112
|
+
const wedgeIndices = new Uint32Array(wedgeCount);
|
|
1113
|
+
for (let wedge = 0; wedge < wedgeCount; wedge += 1) {
|
|
1114
|
+
wedgeIndices[wedge] = view.getUint32(wedges.start + wedge * 4, true);
|
|
1115
|
+
}
|
|
1116
|
+
const wedgeNormals = normals.count > 0 ? copyFloats(view, normals.start, normals.count * 3) : void 0;
|
|
1117
|
+
const wedgeUvs = nonEmptyUvChannels.map(
|
|
1118
|
+
(channel) => copyFloats(view, channel.start, channel.count * 2)
|
|
1119
|
+
);
|
|
1120
|
+
return {
|
|
1121
|
+
offset,
|
|
1122
|
+
byteLength,
|
|
1123
|
+
mesh: {
|
|
1124
|
+
version,
|
|
1125
|
+
faceMaterialIndices,
|
|
1126
|
+
vertexPositions,
|
|
1127
|
+
vertexCount,
|
|
1128
|
+
wedgeIndices,
|
|
1129
|
+
wedgeNormals,
|
|
1130
|
+
wedgeUvs
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
function validateRawMeshCounts(offset, counts) {
|
|
1135
|
+
const { faceCount, wedgeCount, vertexCount, smoothingCount, normalCount, uvChannels } = counts;
|
|
1136
|
+
if (wedgeCount !== faceCount * 3) {
|
|
1137
|
+
throw rawMeshError("FRawMesh wedge count does not match three per face", {
|
|
1138
|
+
offset,
|
|
1139
|
+
wedgeCount,
|
|
1140
|
+
faceCount
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
if (smoothingCount !== 0 && smoothingCount !== faceCount) {
|
|
1144
|
+
throw rawMeshError("FRawMesh smoothing-mask count does not match the face count", {
|
|
1145
|
+
offset,
|
|
1146
|
+
smoothingCount,
|
|
1147
|
+
faceCount
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
if (normalCount !== 0 && normalCount !== wedgeCount) {
|
|
1151
|
+
throw rawMeshError("FRawMesh normal count does not match the wedge count", {
|
|
1152
|
+
offset,
|
|
1153
|
+
normalCount,
|
|
1154
|
+
wedgeCount
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
if (!uvChannels.some((channel) => channel.count > 0)) {
|
|
1158
|
+
throw rawMeshError("FRawMesh has no texture coordinates", { offset });
|
|
1159
|
+
}
|
|
1160
|
+
for (const channel of uvChannels) {
|
|
1161
|
+
if (channel.count !== 0 && channel.count !== wedgeCount) {
|
|
1162
|
+
throw rawMeshError("FRawMesh UV channel does not match the wedge count", {
|
|
1163
|
+
offset,
|
|
1164
|
+
uvCount: channel.count,
|
|
1165
|
+
wedgeCount
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
if (wedgeCount === 0 || vertexCount === 0) {
|
|
1170
|
+
throw rawMeshError("FRawMesh has no renderable geometry", { offset });
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
function copyFloats(view, start, count) {
|
|
1174
|
+
const values = new Float32Array(count);
|
|
1175
|
+
for (let index = 0; index < count; index += 1) {
|
|
1176
|
+
values[index] = view.getFloat32(start + index * 4, true);
|
|
1177
|
+
}
|
|
1178
|
+
return values;
|
|
1179
|
+
}
|
|
1180
|
+
function rawMeshError(message, details) {
|
|
1181
|
+
return new UAssetError("INVALID_RAW_MESH", message, details);
|
|
1182
|
+
}
|
|
1183
|
+
function findRawMeshBlobs(bytes) {
|
|
1184
|
+
const blobs = [];
|
|
1185
|
+
for (let offset = 0; offset + 20 <= bytes.byteLength; offset += 1) {
|
|
1186
|
+
try {
|
|
1187
|
+
const blob = parseRawMesh(bytes, offset);
|
|
1188
|
+
blobs.push(blob);
|
|
1189
|
+
offset += blob.byteLength - 1;
|
|
1190
|
+
} catch (error) {
|
|
1191
|
+
if (!(error instanceof UAssetError)) throw error;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
return blobs;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// src/bulk-source-model.ts
|
|
1198
|
+
function attributeValues(set, name, required) {
|
|
1199
|
+
const entry = set.get(name);
|
|
1200
|
+
if (entry) return entry.values;
|
|
1201
|
+
if (required) {
|
|
1202
|
+
throw new UAssetError("MISSING_MESH_ATTRIBUTE", `UE4 MeshDescription is missing ${name}`, {
|
|
1203
|
+
attribute: name,
|
|
1204
|
+
available: [...set.keys()]
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
return void 0;
|
|
1208
|
+
}
|
|
1209
|
+
function floatAt(values, index, what) {
|
|
1210
|
+
const value = values[index];
|
|
1211
|
+
if (typeof value !== "number") {
|
|
1212
|
+
throw new UAssetError("INVALID_MESH_REFERENCE", `${what} ended before its declared count`, {
|
|
1213
|
+
index,
|
|
1214
|
+
what
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
return value;
|
|
1218
|
+
}
|
|
1219
|
+
function intAt(values, index, what) {
|
|
1220
|
+
const value = values[index];
|
|
1221
|
+
if (typeof value !== "number") {
|
|
1222
|
+
throw new UAssetError("INVALID_MESH_REFERENCE", `${what} ended before its declared count`, {
|
|
1223
|
+
index,
|
|
1224
|
+
what
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
return value;
|
|
1228
|
+
}
|
|
1229
|
+
function expandUe4Instances(description, options) {
|
|
1230
|
+
const positionsByVertex = attributeValues(
|
|
1231
|
+
description.vertexAttributes,
|
|
1232
|
+
"Position",
|
|
1233
|
+
true
|
|
1234
|
+
);
|
|
1235
|
+
const uvsByInstance = attributeValues(
|
|
1236
|
+
description.vertexInstanceAttributes,
|
|
1237
|
+
"TextureCoordinate",
|
|
1238
|
+
true
|
|
1239
|
+
);
|
|
1240
|
+
const normalsByInstance = attributeValues(
|
|
1241
|
+
description.vertexInstanceAttributes,
|
|
1242
|
+
"Normal",
|
|
1243
|
+
false
|
|
1244
|
+
);
|
|
1245
|
+
const instanceIds = description.validVertexInstanceIds;
|
|
1246
|
+
const positions = new Float32Array(instanceIds.length * 3);
|
|
1247
|
+
const uvs = new Float32Array(instanceIds.length * 2);
|
|
1248
|
+
const normals = normalsByInstance ? new Float32Array(instanceIds.length * 3) : void 0;
|
|
1249
|
+
const instanceToOutput = /* @__PURE__ */ new Map();
|
|
1250
|
+
for (let output = 0; output < instanceIds.length; output += 1) {
|
|
1251
|
+
const instanceId = instanceIds[output] ?? -1;
|
|
1252
|
+
instanceToOutput.set(instanceId, output);
|
|
1253
|
+
const vertexId = intAt(
|
|
1254
|
+
description.vertexInstanceVertices,
|
|
1255
|
+
instanceId,
|
|
1256
|
+
"FMeshVertexInstance.VertexID"
|
|
1257
|
+
);
|
|
1258
|
+
const [x, y, z] = convertVector(
|
|
1259
|
+
floatAt(positionsByVertex, vertexId * 3, "Position"),
|
|
1260
|
+
floatAt(positionsByVertex, vertexId * 3 + 1, "Position"),
|
|
1261
|
+
floatAt(positionsByVertex, vertexId * 3 + 2, "Position"),
|
|
1262
|
+
options.convertCoordinates
|
|
1263
|
+
);
|
|
1264
|
+
positions[output * 3] = x;
|
|
1265
|
+
positions[output * 3 + 1] = y;
|
|
1266
|
+
positions[output * 3 + 2] = z;
|
|
1267
|
+
uvs[output * 2] = floatAt(uvsByInstance, instanceId * 2, "TextureCoordinate");
|
|
1268
|
+
const v = floatAt(uvsByInstance, instanceId * 2 + 1, "TextureCoordinate");
|
|
1269
|
+
uvs[output * 2 + 1] = options.flipV ? 1 - v : v;
|
|
1270
|
+
if (normals && normalsByInstance) {
|
|
1271
|
+
const [nx, ny, nz] = convertVector(
|
|
1272
|
+
floatAt(normalsByInstance, instanceId * 3, "Normal"),
|
|
1273
|
+
floatAt(normalsByInstance, instanceId * 3 + 1, "Normal"),
|
|
1274
|
+
floatAt(normalsByInstance, instanceId * 3 + 2, "Normal"),
|
|
1275
|
+
options.convertCoordinates
|
|
1276
|
+
);
|
|
1277
|
+
normals[output * 3] = nx;
|
|
1278
|
+
normals[output * 3 + 1] = ny;
|
|
1279
|
+
normals[output * 3 + 2] = nz;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
return { positions, normals, uvs, instanceToOutput };
|
|
1283
|
+
}
|
|
1284
|
+
function buildGeometryFromUe4MeshDescription(description, options) {
|
|
1285
|
+
const expanded = expandUe4Instances(description, options);
|
|
1286
|
+
const triangleIds = description.validTriangleIds;
|
|
1287
|
+
assertUAsset(
|
|
1288
|
+
triangleIds.length > 0,
|
|
1289
|
+
"UNSUPPORTED_STATIC_MESH_LAYOUT",
|
|
1290
|
+
"UE4 MeshDescription contains no triangles"
|
|
1291
|
+
);
|
|
1292
|
+
const trianglesByGroup = /* @__PURE__ */ new Map();
|
|
1293
|
+
for (const triangleId of triangleIds) {
|
|
1294
|
+
const corners = [0, 0, 0];
|
|
1295
|
+
for (let corner = 0; corner < 3; corner += 1) {
|
|
1296
|
+
const instanceId = intAt(
|
|
1297
|
+
description.triangleVertexInstances,
|
|
1298
|
+
triangleId * 3 + corner,
|
|
1299
|
+
"FMeshTriangle.VertexInstanceIDs"
|
|
1300
|
+
);
|
|
1301
|
+
const output = expanded.instanceToOutput.get(instanceId);
|
|
1302
|
+
if (output === void 0) {
|
|
1303
|
+
throw new UAssetError(
|
|
1304
|
+
"INVALID_MESH_REFERENCE",
|
|
1305
|
+
"Triangle references a vertex instance with no output slot",
|
|
1306
|
+
{ triangleId, corner, instanceId }
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
corners[corner] = output;
|
|
1310
|
+
}
|
|
1311
|
+
if (options.flipWinding) {
|
|
1312
|
+
const second = corners[1];
|
|
1313
|
+
corners[1] = corners[2];
|
|
1314
|
+
corners[2] = second;
|
|
1315
|
+
}
|
|
1316
|
+
const polygonId = intAt(description.trianglePolygons, triangleId, "FMeshTriangle.PolygonID");
|
|
1317
|
+
const group = intAt(description.polygonGroups, polygonId, "FMeshPolygon.PolygonGroupID");
|
|
1318
|
+
const groupTriangles = trianglesByGroup.get(group) ?? [];
|
|
1319
|
+
groupTriangles.push(corners[0], corners[1], corners[2]);
|
|
1320
|
+
trianglesByGroup.set(group, groupTriangles);
|
|
1321
|
+
}
|
|
1322
|
+
const slotNames = attributeValues(
|
|
1323
|
+
description.polygonGroupAttributes,
|
|
1324
|
+
"ImportedMaterialSlotName",
|
|
1325
|
+
false
|
|
1326
|
+
);
|
|
1327
|
+
return assembleGeometry(
|
|
1328
|
+
expanded.positions,
|
|
1329
|
+
expanded.normals,
|
|
1330
|
+
expanded.uvs,
|
|
1331
|
+
trianglesByGroup,
|
|
1332
|
+
(group) => slotNames?.[group] ?? String(group),
|
|
1333
|
+
{
|
|
1334
|
+
vertices: description.vertexCount,
|
|
1335
|
+
vertexInstances: description.validVertexInstanceIds.length,
|
|
1336
|
+
triangles: triangleIds.length
|
|
1337
|
+
}
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
function bulkInfo(header) {
|
|
1341
|
+
return {
|
|
1342
|
+
headerOffset: header.headerOffset,
|
|
1343
|
+
flags: header.flags,
|
|
1344
|
+
storage: header.storage,
|
|
1345
|
+
file: header.file,
|
|
1346
|
+
compression: header.compression,
|
|
1347
|
+
elementCount: header.elementCount,
|
|
1348
|
+
sizeOnDisk: header.sizeOnDisk,
|
|
1349
|
+
offsetInFile: header.offsetInFile,
|
|
1350
|
+
payloadOffset: header.payloadOffset
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
function decodeSourceModel(payload, options) {
|
|
1354
|
+
if (looksLikeMeshDescriptionUe4(payload)) {
|
|
1355
|
+
try {
|
|
1356
|
+
const description = parseMeshDescriptionUe4(payload);
|
|
1357
|
+
return {
|
|
1358
|
+
layout: "mesh-description-ue4",
|
|
1359
|
+
build: buildGeometryFromUe4MeshDescription(description, options),
|
|
1360
|
+
byteLength: description.byteLength
|
|
1361
|
+
};
|
|
1362
|
+
} catch (error) {
|
|
1363
|
+
if (!(error instanceof UAssetError)) throw error;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
try {
|
|
1367
|
+
const blob = parseRawMesh(payload, 0);
|
|
1368
|
+
return {
|
|
1369
|
+
layout: "raw-mesh",
|
|
1370
|
+
build: buildGeometryFromRawMesh(blob.mesh, options),
|
|
1371
|
+
byteLength: blob.byteLength
|
|
1372
|
+
};
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
if (!(error instanceof UAssetError)) throw error;
|
|
1375
|
+
}
|
|
1376
|
+
return void 0;
|
|
1377
|
+
}
|
|
1378
|
+
function selectBulkSourceModel(bytes, layout, parseOptions, options) {
|
|
1379
|
+
let blockingError;
|
|
1380
|
+
for (const header of findBulkDataHeaders(bytes, layout, parseOptions.bulkDataFiles)) {
|
|
1381
|
+
let payload;
|
|
1382
|
+
try {
|
|
1383
|
+
payload = resolveBulkDataPayload(bytes, header, {
|
|
1384
|
+
...parseOptions.bulkDataFiles === void 0 ? {} : { files: parseOptions.bulkDataFiles },
|
|
1385
|
+
...parseOptions.zlib === void 0 ? {} : { zlib: parseOptions.zlib }
|
|
1386
|
+
});
|
|
1387
|
+
} catch (error) {
|
|
1388
|
+
if (!(error instanceof UAssetError)) throw error;
|
|
1389
|
+
if (error.code === "MISSING_CODEC" || error.code === "MISSING_BULK_DATA_FILE") {
|
|
1390
|
+
blockingError ??= error;
|
|
1391
|
+
}
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
1394
|
+
const decoded = decodeSourceModel(payload, options);
|
|
1395
|
+
if (decoded === void 0) continue;
|
|
1396
|
+
return {
|
|
1397
|
+
selection: {
|
|
1398
|
+
layout: decoded.layout,
|
|
1399
|
+
build: decoded.build,
|
|
1400
|
+
bulkData: bulkInfo(header),
|
|
1401
|
+
payloadByteLength: decoded.byteLength
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
return blockingError === void 0 ? {} : { blockingError };
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// src/compressed-buffer.ts
|
|
1409
|
+
var COMPRESSED_BUFFER_MAGIC = 3077923682;
|
|
1410
|
+
var COMPRESSION_METHOD = Object.freeze({
|
|
1411
|
+
NONE: 0,
|
|
1412
|
+
OODLE: 3,
|
|
1413
|
+
LZ4: 4
|
|
1414
|
+
});
|
|
1415
|
+
function findCompressedBufferOffsets(bytes) {
|
|
1416
|
+
const offsets = [];
|
|
1417
|
+
for (let offset = 0; offset + 4 <= bytes.byteLength; offset += 1) {
|
|
1418
|
+
if (bytes[offset] === 183 && bytes[offset + 1] === 117 && bytes[offset + 2] === 99 && bytes[offset + 3] === 98) {
|
|
1419
|
+
offsets.push(offset);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
return offsets;
|
|
1423
|
+
}
|
|
1424
|
+
function parseCompressedBuffer(bytes, offset = 0) {
|
|
1425
|
+
const reader = new BinaryReader(bytes, offset);
|
|
1426
|
+
const magic = reader.uint32BE("FCompressedBuffer magic");
|
|
1427
|
+
assertUAsset(
|
|
1428
|
+
magic === COMPRESSED_BUFFER_MAGIC,
|
|
1429
|
+
"INVALID_COMPRESSED_BUFFER",
|
|
1430
|
+
"Invalid FCompressedBuffer magic",
|
|
1431
|
+
{
|
|
1432
|
+
offset,
|
|
1433
|
+
magic: `0x${magic.toString(16)}`
|
|
1434
|
+
}
|
|
1435
|
+
);
|
|
1436
|
+
const crc32 = reader.uint32BE("FCompressedBuffer CRC32");
|
|
1437
|
+
const method = reader.uint8("compression method");
|
|
1438
|
+
const compressor = reader.uint8("compressor");
|
|
1439
|
+
const compressionLevel = reader.uint8("compression level");
|
|
1440
|
+
const blockSizeExponent = reader.uint8("block size exponent");
|
|
1441
|
+
const blockCount = reader.uint32BE("block count");
|
|
1442
|
+
const totalRawSize = safeNumber(reader.uint64BE("total raw size"), "Total raw size", offset);
|
|
1443
|
+
const totalCompressedSize = safeNumber(
|
|
1444
|
+
reader.uint64BE("total compressed size"),
|
|
1445
|
+
"Total compressed size",
|
|
1446
|
+
offset
|
|
1447
|
+
);
|
|
1448
|
+
const rawHash = reader.raw(32, "raw BLAKE3 hash");
|
|
1449
|
+
assertUAsset(
|
|
1450
|
+
blockCount <= 1e6,
|
|
1451
|
+
"INVALID_COMPRESSED_BUFFER",
|
|
1452
|
+
"Invalid compressed block count",
|
|
1453
|
+
{
|
|
1454
|
+
offset,
|
|
1455
|
+
blockCount
|
|
1456
|
+
}
|
|
1457
|
+
);
|
|
1458
|
+
assertUAsset(
|
|
1459
|
+
totalCompressedSize >= 64,
|
|
1460
|
+
"INVALID_COMPRESSED_BUFFER",
|
|
1461
|
+
"Compressed buffer is smaller than its header",
|
|
1462
|
+
{ offset, totalCompressedSize }
|
|
1463
|
+
);
|
|
1464
|
+
assertUAsset(
|
|
1465
|
+
totalCompressedSize <= Number.MAX_SAFE_INTEGER && offset + totalCompressedSize <= bytes.byteLength,
|
|
1466
|
+
"TRUNCATED_PACKAGE",
|
|
1467
|
+
"Compressed buffer extends past the package",
|
|
1468
|
+
{ offset, totalCompressedSize, packageSize: bytes.byteLength }
|
|
1469
|
+
);
|
|
1470
|
+
assertUAsset(
|
|
1471
|
+
method === COMPRESSION_METHOD.NONE || method === COMPRESSION_METHOD.OODLE || method === COMPRESSION_METHOD.LZ4,
|
|
1472
|
+
"UNSUPPORTED_COMPRESSION_METHOD",
|
|
1473
|
+
`Unsupported Unreal compressed-buffer method ${method}`,
|
|
1474
|
+
{ offset, method }
|
|
1475
|
+
);
|
|
1476
|
+
assertUAsset(
|
|
1477
|
+
method === COMPRESSION_METHOD.NONE || blockCount > 0 || totalRawSize === 0,
|
|
1478
|
+
"INVALID_COMPRESSED_BUFFER",
|
|
1479
|
+
"Compressed Unreal payload declares no blocks",
|
|
1480
|
+
{ offset, method, blockCount, totalRawSize }
|
|
1481
|
+
);
|
|
1482
|
+
const blockSizes = [];
|
|
1483
|
+
if (method === COMPRESSION_METHOD.OODLE || method === COMPRESSION_METHOD.LZ4) {
|
|
1484
|
+
for (let index = 0; index < blockCount; index += 1) {
|
|
1485
|
+
blockSizes.push(reader.uint32BE(`compressed block ${index} size`));
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
const dataOffset = offset + reader.pos;
|
|
1489
|
+
const payloadSize = method === COMPRESSION_METHOD.NONE ? totalRawSize : blockSizes.reduce((sum, size) => sum + size, 0);
|
|
1490
|
+
assertUAsset(
|
|
1491
|
+
reader.pos + payloadSize <= totalCompressedSize,
|
|
1492
|
+
"INVALID_COMPRESSED_BUFFER",
|
|
1493
|
+
"Compressed payload exceeds its declared buffer size",
|
|
1494
|
+
{ offset, headerAndTableSize: reader.pos, payloadSize, totalCompressedSize }
|
|
1495
|
+
);
|
|
1496
|
+
assertUAsset(
|
|
1497
|
+
dataOffset + payloadSize <= bytes.byteLength,
|
|
1498
|
+
"TRUNCATED_PACKAGE",
|
|
1499
|
+
"Compressed payload bytes are truncated",
|
|
1500
|
+
{ offset, dataOffset, payloadSize, packageSize: bytes.byteLength }
|
|
1501
|
+
);
|
|
1502
|
+
return {
|
|
1503
|
+
offset,
|
|
1504
|
+
crc32,
|
|
1505
|
+
method,
|
|
1506
|
+
compressor,
|
|
1507
|
+
compressionLevel,
|
|
1508
|
+
blockSizeExponent,
|
|
1509
|
+
blockCount,
|
|
1510
|
+
totalRawSize,
|
|
1511
|
+
totalCompressedSize,
|
|
1512
|
+
rawHash: Uint8Array.from(rawHash),
|
|
1513
|
+
blockSizes,
|
|
1514
|
+
payloadBytes: bytes.subarray(dataOffset, dataOffset + payloadSize)
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
function safeNumber(value, label, offset) {
|
|
1518
|
+
const number = Number(value);
|
|
1519
|
+
assertUAsset(Number.isSafeInteger(number), "INVALID_COMPRESSED_BUFFER", `${label} is too large`, {
|
|
1520
|
+
offset,
|
|
1521
|
+
value: value.toString()
|
|
1522
|
+
});
|
|
1523
|
+
return number;
|
|
1524
|
+
}
|
|
1525
|
+
function decompressCompressedBuffer(buffer, codecs) {
|
|
1526
|
+
if (buffer.method === COMPRESSION_METHOD.NONE) {
|
|
1527
|
+
return Uint8Array.from(buffer.payloadBytes.subarray(0, buffer.totalRawSize));
|
|
1528
|
+
}
|
|
1529
|
+
const isOodle = buffer.method === COMPRESSION_METHOD.OODLE;
|
|
1530
|
+
const codec = isOodle ? codecs.oodle : codecs.lz4;
|
|
1531
|
+
const codecName = isOodle ? "Oodle" : "LZ4";
|
|
1532
|
+
if (typeof codec !== "function") {
|
|
1533
|
+
throw new UAssetError(
|
|
1534
|
+
"MISSING_CODEC",
|
|
1535
|
+
`${codecName} decompression is required for this Unreal payload`,
|
|
1536
|
+
{
|
|
1537
|
+
method: buffer.method,
|
|
1538
|
+
offset: buffer.offset
|
|
1539
|
+
}
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
const output = new Uint8Array(buffer.totalRawSize);
|
|
1543
|
+
const maximumBlockSize = buffer.blockSizeExponent > 0 && buffer.blockSizeExponent < 31 ? 2 ** buffer.blockSizeExponent : buffer.totalRawSize;
|
|
1544
|
+
let sourceOffset = 0;
|
|
1545
|
+
let targetOffset = 0;
|
|
1546
|
+
for (let index = 0; index < buffer.blockSizes.length; index += 1) {
|
|
1547
|
+
const compressedSize = buffer.blockSizes[index];
|
|
1548
|
+
if (compressedSize === void 0) {
|
|
1549
|
+
throw new UAssetError("INVALID_COMPRESSED_BUFFER", "Compressed block table ended early", {
|
|
1550
|
+
block: index
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
const rawBlockSize = Math.min(maximumBlockSize, buffer.totalRawSize - targetOffset);
|
|
1554
|
+
const compressed = buffer.payloadBytes.subarray(sourceOffset, sourceOffset + compressedSize);
|
|
1555
|
+
const decompressed = codec(compressed, rawBlockSize);
|
|
1556
|
+
assertUAsset(
|
|
1557
|
+
decompressed.byteLength === rawBlockSize,
|
|
1558
|
+
"CODEC_SIZE_MISMATCH",
|
|
1559
|
+
`${codecName} returned the wrong size`,
|
|
1560
|
+
{ block: index, expected: rawBlockSize, actual: decompressed.byteLength }
|
|
1561
|
+
);
|
|
1562
|
+
output.set(decompressed, targetOffset);
|
|
1563
|
+
sourceOffset += compressedSize;
|
|
1564
|
+
targetOffset += rawBlockSize;
|
|
1565
|
+
}
|
|
1566
|
+
assertUAsset(
|
|
1567
|
+
targetOffset === buffer.totalRawSize,
|
|
1568
|
+
"INCOMPLETE_DECOMPRESSION",
|
|
1569
|
+
"Compressed payload did not produce the declared raw size",
|
|
1570
|
+
{ expected: buffer.totalRawSize, actual: targetOffset }
|
|
1571
|
+
);
|
|
1572
|
+
return output;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// src/mesh-description.ts
|
|
1576
|
+
var MAX_MESH_ELEMENTS3 = 1e7;
|
|
1577
|
+
var MAX_ATTRIBUTE_VALUES2 = 1e8;
|
|
1578
|
+
var ATTRIBUTE_TYPES2 = Object.freeze([
|
|
1579
|
+
{ name: "FVector4f", components: 4, byteSize: 16, kind: "float" },
|
|
1580
|
+
{ name: "FVector3f", components: 3, byteSize: 12, kind: "float" },
|
|
1581
|
+
{ name: "FVector2f", components: 2, byteSize: 8, kind: "float" },
|
|
1582
|
+
{ name: "float", components: 1, byteSize: 4, kind: "float" },
|
|
1583
|
+
{ name: "int32", components: 1, byteSize: 4, kind: "int" },
|
|
1584
|
+
{ name: "bool", components: 1, byteSize: 1, kind: "bool" },
|
|
1585
|
+
{ name: "FName", components: 1, byteSize: void 0, kind: "name" }
|
|
1586
|
+
]);
|
|
1587
|
+
function readBitArray2(reader) {
|
|
1588
|
+
const numBits = reader.int32("TBitArray NumBits");
|
|
1589
|
+
assertUAsset(
|
|
1590
|
+
numBits >= 0 && numBits <= MAX_MESH_ELEMENTS3,
|
|
1591
|
+
"INVALID_MESH_DESCRIPTION",
|
|
1592
|
+
"Invalid Unreal bit-array length",
|
|
1593
|
+
{ offset: reader.pos - 4, numBits }
|
|
1594
|
+
);
|
|
1595
|
+
const wordCount = Math.ceil(numBits / 32);
|
|
1596
|
+
const words = new Uint32Array(wordCount);
|
|
1597
|
+
for (let index = 0; index < wordCount; index += 1) {
|
|
1598
|
+
words[index] = reader.uint32("TBitArray word");
|
|
1599
|
+
}
|
|
1600
|
+
const validIds = [];
|
|
1601
|
+
for (let id = 0; id < numBits; id += 1) {
|
|
1602
|
+
const word = words[id >>> 5];
|
|
1603
|
+
if (word !== void 0 && (word & 1 << (id & 31)) !== 0) validIds.push(id);
|
|
1604
|
+
}
|
|
1605
|
+
return { numBits, validIds };
|
|
1606
|
+
}
|
|
1607
|
+
function readBulkValues(reader, type, context) {
|
|
1608
|
+
const serializedElementSize = reader.int32(`${context} bulk element size`);
|
|
1609
|
+
const serializedCount = reader.int32(`${context} bulk element count`);
|
|
1610
|
+
assertUAsset(
|
|
1611
|
+
type.byteSize !== void 0 && serializedElementSize === type.byteSize,
|
|
1612
|
+
"INVALID_MESH_DESCRIPTION",
|
|
1613
|
+
`${context} has an unexpected element size`,
|
|
1614
|
+
{ expected: type.byteSize, actual: serializedElementSize, type: type.name }
|
|
1615
|
+
);
|
|
1616
|
+
assertUAsset(
|
|
1617
|
+
serializedCount >= 0 && serializedCount <= MAX_ATTRIBUTE_VALUES2,
|
|
1618
|
+
"INVALID_MESH_DESCRIPTION",
|
|
1619
|
+
`${context} has an invalid value count`,
|
|
1620
|
+
{ count: serializedCount }
|
|
1621
|
+
);
|
|
1622
|
+
const requiredBytes = serializedCount * serializedElementSize;
|
|
1623
|
+
assertUAsset(
|
|
1624
|
+
Number.isSafeInteger(requiredBytes) && requiredBytes <= reader.remaining,
|
|
1625
|
+
"INVALID_MESH_DESCRIPTION",
|
|
1626
|
+
`${context} exceeds the remaining MeshDescription payload`,
|
|
1627
|
+
{ requiredBytes, remaining: reader.remaining, serializedCount, serializedElementSize }
|
|
1628
|
+
);
|
|
1629
|
+
if (type.kind === "float") {
|
|
1630
|
+
const values2 = new Float32Array(serializedCount * type.components);
|
|
1631
|
+
for (let index = 0; index < values2.length; index += 1) {
|
|
1632
|
+
values2[index] = reader.float32(context);
|
|
1633
|
+
}
|
|
1634
|
+
return { values: values2, serializedCount };
|
|
1635
|
+
}
|
|
1636
|
+
if (type.kind === "int") {
|
|
1637
|
+
const values2 = new Int32Array(serializedCount);
|
|
1638
|
+
for (let index = 0; index < values2.length; index += 1) {
|
|
1639
|
+
values2[index] = reader.int32(context);
|
|
1640
|
+
}
|
|
1641
|
+
return { values: values2, serializedCount };
|
|
1642
|
+
}
|
|
1643
|
+
const values = new Uint8Array(serializedCount);
|
|
1644
|
+
values.set(reader.raw(serializedCount, context));
|
|
1645
|
+
return { values, serializedCount };
|
|
1646
|
+
}
|
|
1647
|
+
function readAttributeEntry2(reader, rawName) {
|
|
1648
|
+
const name = rawName.trimEnd();
|
|
1649
|
+
const typeIndex = reader.uint32(`${name} type`);
|
|
1650
|
+
const type = ATTRIBUTE_TYPES2[typeIndex];
|
|
1651
|
+
assertUAsset(
|
|
1652
|
+
type !== void 0,
|
|
1653
|
+
"INVALID_MESH_DESCRIPTION",
|
|
1654
|
+
"Unknown MeshDescription attribute type",
|
|
1655
|
+
{
|
|
1656
|
+
attribute: name,
|
|
1657
|
+
typeIndex
|
|
1658
|
+
}
|
|
1659
|
+
);
|
|
1660
|
+
const extent = reader.uint32(`${name} extent`);
|
|
1661
|
+
assertUAsset(
|
|
1662
|
+
extent >= 1 && extent <= 64,
|
|
1663
|
+
"INVALID_MESH_DESCRIPTION",
|
|
1664
|
+
`Invalid extent for ${name}`,
|
|
1665
|
+
{
|
|
1666
|
+
extent
|
|
1667
|
+
}
|
|
1668
|
+
);
|
|
1669
|
+
const numElements = reader.int32(`${name} number of elements`);
|
|
1670
|
+
const numChannels = reader.int32(`${name} channel count`);
|
|
1671
|
+
assertUAsset(
|
|
1672
|
+
numElements >= 0 && numElements <= MAX_MESH_ELEMENTS3,
|
|
1673
|
+
"INVALID_MESH_DESCRIPTION",
|
|
1674
|
+
`Invalid element count for ${name}`,
|
|
1675
|
+
{ numElements }
|
|
1676
|
+
);
|
|
1677
|
+
assertUAsset(
|
|
1678
|
+
numChannels >= 0 && numChannels <= 128,
|
|
1679
|
+
"INVALID_MESH_DESCRIPTION",
|
|
1680
|
+
`Invalid channel count for ${name}`,
|
|
1681
|
+
{ numChannels }
|
|
1682
|
+
);
|
|
1683
|
+
const channels = [];
|
|
1684
|
+
for (let channelIndex = 0; channelIndex < numChannels; channelIndex += 1) {
|
|
1685
|
+
const channelExtent = reader.uint32(`${name} channel extent`);
|
|
1686
|
+
assertUAsset(
|
|
1687
|
+
channelExtent === extent,
|
|
1688
|
+
"INVALID_MESH_DESCRIPTION",
|
|
1689
|
+
`Channel extent mismatch for ${name}`,
|
|
1690
|
+
{ extent, channelExtent }
|
|
1691
|
+
);
|
|
1692
|
+
let channel;
|
|
1693
|
+
if (type.kind === "name") {
|
|
1694
|
+
const count = reader.int32(`${name} FName count`);
|
|
1695
|
+
assertUAsset(
|
|
1696
|
+
count >= 0 && count <= MAX_ATTRIBUTE_VALUES2,
|
|
1697
|
+
"INVALID_MESH_DESCRIPTION",
|
|
1698
|
+
`Invalid FName count for ${name}`,
|
|
1699
|
+
{ count }
|
|
1700
|
+
);
|
|
1701
|
+
const values = new Array(count);
|
|
1702
|
+
for (let index = 0; index < count; index += 1) {
|
|
1703
|
+
values[index] = reader.fstring(`${name}[${index}]`);
|
|
1704
|
+
}
|
|
1705
|
+
channel = { extent: channelExtent, values, serializedCount: count };
|
|
1706
|
+
} else {
|
|
1707
|
+
channel = {
|
|
1708
|
+
extent: channelExtent,
|
|
1709
|
+
...readBulkValues(reader, type, `${name}[${channelIndex}]`)
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
const expectedCount = numElements * extent;
|
|
1713
|
+
assertUAsset(
|
|
1714
|
+
channel.serializedCount === expectedCount,
|
|
1715
|
+
"INVALID_MESH_DESCRIPTION",
|
|
1716
|
+
`Serialized value count mismatch for ${name}`,
|
|
1717
|
+
{
|
|
1718
|
+
channelIndex,
|
|
1719
|
+
expected: expectedCount,
|
|
1720
|
+
actual: channel.serializedCount,
|
|
1721
|
+
numElements,
|
|
1722
|
+
extent
|
|
1723
|
+
}
|
|
1724
|
+
);
|
|
1725
|
+
channels.push(channel);
|
|
1726
|
+
}
|
|
1727
|
+
readDefaultValue2(reader, type);
|
|
1728
|
+
reader.uint32(`${name} flags`);
|
|
1729
|
+
return {
|
|
1730
|
+
name,
|
|
1731
|
+
type: type.name,
|
|
1732
|
+
components: type.components,
|
|
1733
|
+
extent,
|
|
1734
|
+
numElements,
|
|
1735
|
+
channels
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
function readDefaultValue2(reader, type) {
|
|
1739
|
+
switch (type.kind) {
|
|
1740
|
+
case "float": {
|
|
1741
|
+
const values = [];
|
|
1742
|
+
for (let index = 0; index < type.components; index += 1) {
|
|
1743
|
+
values.push(reader.float32(type.name));
|
|
1744
|
+
}
|
|
1745
|
+
return type.components === 1 ? values[0] ?? 0 : values;
|
|
1746
|
+
}
|
|
1747
|
+
case "int":
|
|
1748
|
+
return reader.int32(type.name);
|
|
1749
|
+
case "bool":
|
|
1750
|
+
return reader.uint32(type.name) !== 0;
|
|
1751
|
+
case "name":
|
|
1752
|
+
return reader.fstring(type.name);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
function readAttributesSet(reader) {
|
|
1756
|
+
const numElements = reader.int32("attribute-set number of elements");
|
|
1757
|
+
const attributeCount = reader.int32("attribute map count");
|
|
1758
|
+
assertUAsset(
|
|
1759
|
+
numElements >= 0 && numElements <= MAX_MESH_ELEMENTS3 && attributeCount >= 0 && attributeCount <= 1024,
|
|
1760
|
+
"INVALID_MESH_DESCRIPTION",
|
|
1761
|
+
"Invalid attribute-set size",
|
|
1762
|
+
{ numElements, attributeCount }
|
|
1763
|
+
);
|
|
1764
|
+
const attributes = /* @__PURE__ */ new Map();
|
|
1765
|
+
for (let index = 0; index < attributeCount; index += 1) {
|
|
1766
|
+
const rawName = reader.fstring(`attribute name ${index}`);
|
|
1767
|
+
const entry = readAttributeEntry2(reader, rawName);
|
|
1768
|
+
attributes.set(entry.name, entry);
|
|
1769
|
+
}
|
|
1770
|
+
return { numElements, attributes };
|
|
1771
|
+
}
|
|
1772
|
+
function readElementContainer(reader, elementName, channelIndex) {
|
|
1773
|
+
const allocation = readBitArray2(reader);
|
|
1774
|
+
const numHoles = reader.int32(`${elementName} NumHoles`);
|
|
1775
|
+
const attributeSet = readAttributesSet(reader);
|
|
1776
|
+
assertUAsset(
|
|
1777
|
+
attributeSet.numElements === allocation.numBits,
|
|
1778
|
+
"INVALID_MESH_DESCRIPTION",
|
|
1779
|
+
`Allocation and attribute count disagree for ${elementName}`,
|
|
1780
|
+
{
|
|
1781
|
+
channelIndex,
|
|
1782
|
+
allocatedSlots: allocation.numBits,
|
|
1783
|
+
attributeElements: attributeSet.numElements
|
|
1784
|
+
}
|
|
1785
|
+
);
|
|
1786
|
+
assertUAsset(
|
|
1787
|
+
numHoles >= 0 && numHoles <= allocation.numBits,
|
|
1788
|
+
"INVALID_MESH_DESCRIPTION",
|
|
1789
|
+
`Invalid hole count for ${elementName}`,
|
|
1790
|
+
{ channelIndex, numHoles, numBits: allocation.numBits }
|
|
1791
|
+
);
|
|
1792
|
+
assertUAsset(
|
|
1793
|
+
allocation.validIds.length === allocation.numBits - numHoles,
|
|
1794
|
+
"INVALID_MESH_DESCRIPTION",
|
|
1795
|
+
`Bit array and hole count disagree for ${elementName}`,
|
|
1796
|
+
{ validIds: allocation.validIds.length, numBits: allocation.numBits, numHoles }
|
|
1797
|
+
);
|
|
1798
|
+
return { allocation, attributeSet };
|
|
1799
|
+
}
|
|
1800
|
+
function parseMeshDescription(input, offset = 0) {
|
|
1801
|
+
const reader = new BinaryReader(input, offset);
|
|
1802
|
+
const start = reader.pos;
|
|
1803
|
+
const elementTypeCount = reader.int32("MeshDescription element type count");
|
|
1804
|
+
assertUAsset(
|
|
1805
|
+
elementTypeCount >= 1 && elementTypeCount <= 32,
|
|
1806
|
+
"INVALID_MESH_DESCRIPTION",
|
|
1807
|
+
"Invalid MeshDescription element-type count",
|
|
1808
|
+
{ offset, elementTypeCount }
|
|
1809
|
+
);
|
|
1810
|
+
const elements = /* @__PURE__ */ new Map();
|
|
1811
|
+
for (let elementIndex = 0; elementIndex < elementTypeCount; elementIndex += 1) {
|
|
1812
|
+
const name = reader.fstring(`element type ${elementIndex}`);
|
|
1813
|
+
assertUAsset(
|
|
1814
|
+
name.length > 0 && name.length < 128,
|
|
1815
|
+
"INVALID_MESH_DESCRIPTION",
|
|
1816
|
+
"Invalid MeshDescription element name",
|
|
1817
|
+
{ elementIndex, name }
|
|
1818
|
+
);
|
|
1819
|
+
const channelCount = reader.int32(`${name} channel count`);
|
|
1820
|
+
assertUAsset(
|
|
1821
|
+
channelCount >= 1 && channelCount <= 128,
|
|
1822
|
+
"INVALID_MESH_DESCRIPTION",
|
|
1823
|
+
`Invalid channel count for ${name}`,
|
|
1824
|
+
{ channelCount }
|
|
1825
|
+
);
|
|
1826
|
+
const channels = [];
|
|
1827
|
+
for (let channelIndex = 0; channelIndex < channelCount; channelIndex += 1) {
|
|
1828
|
+
channels.push(readElementContainer(reader, name, channelIndex));
|
|
1829
|
+
}
|
|
1830
|
+
elements.set(name, { name, channels });
|
|
1831
|
+
}
|
|
1832
|
+
return {
|
|
1833
|
+
byteLength: reader.pos - start,
|
|
1834
|
+
trailingBytes: reader.remaining,
|
|
1835
|
+
elements
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
function looksLikeMeshDescription(bytes, offset = 0) {
|
|
1839
|
+
if (offset < 0 || offset + 17 > bytes.byteLength) return false;
|
|
1840
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1841
|
+
const count = view.getInt32(offset, true);
|
|
1842
|
+
const stringLength = view.getInt32(offset + 4, true);
|
|
1843
|
+
if (count < 1 || count > 32 || stringLength !== 9) return false;
|
|
1844
|
+
const expected = "Vertices";
|
|
1845
|
+
for (let index = 0; index < expected.length; index += 1) {
|
|
1846
|
+
if (bytes[offset + 8 + index] !== expected.charCodeAt(index)) return false;
|
|
1847
|
+
}
|
|
1848
|
+
return bytes[offset + 16] === 0;
|
|
1849
|
+
}
|
|
1850
|
+
function findMeshDescriptionOffsets(bytes) {
|
|
1851
|
+
const offsets = [];
|
|
1852
|
+
for (let offset = 0; offset + 17 <= bytes.byteLength; offset += 1) {
|
|
1853
|
+
if (looksLikeMeshDescription(bytes, offset)) offsets.push(offset);
|
|
1854
|
+
}
|
|
1855
|
+
return offsets;
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
// src/static-mesh.ts
|
|
1859
|
+
var latin1 = new TextDecoder("latin1");
|
|
1860
|
+
var METADATA_SCAN_BYTES = 262144;
|
|
1861
|
+
function asBytes(input) {
|
|
1862
|
+
if (input instanceof ArrayBuffer) return new Uint8Array(input);
|
|
1863
|
+
if (ArrayBuffer.isView(input)) {
|
|
1864
|
+
return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
|
|
1865
|
+
}
|
|
1866
|
+
throw new TypeError("Expected an ArrayBuffer or ArrayBufferView");
|
|
1867
|
+
}
|
|
1868
|
+
function scanMetadata(bytes) {
|
|
1869
|
+
const text = latin1.decode(bytes.subarray(0, Math.min(bytes.byteLength, METADATA_SCAN_BYTES)));
|
|
1870
|
+
const branch = /\+\+UE[45]\+Release-[0-9.]+/.exec(text)?.[0] ?? "unknown";
|
|
1871
|
+
const gamePaths = [...text.matchAll(/\/Game\/[A-Za-z0-9_./-]+/g)].map((match) => match[0]);
|
|
1872
|
+
const objectPath = gamePaths.find((path) => /\/StaticMeshes\//.test(path)) ?? gamePaths[0] ?? "unknown";
|
|
1873
|
+
return { engineVersion: branch, objectPath };
|
|
1874
|
+
}
|
|
1875
|
+
function attribute(description, elementName, attributeName, required) {
|
|
1876
|
+
const entry = description.elements.get(elementName)?.channels[0]?.attributeSet.attributes.get(attributeName);
|
|
1877
|
+
if (entry) return entry;
|
|
1878
|
+
if (required) {
|
|
1879
|
+
throw new UAssetError("MISSING_MESH_ATTRIBUTE", `MeshDescription is missing ${attributeName}`, {
|
|
1880
|
+
element: elementName,
|
|
1881
|
+
attribute: attributeName
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1884
|
+
return void 0;
|
|
1885
|
+
}
|
|
1886
|
+
function channelValues(entry) {
|
|
1887
|
+
const channel = entry.channels[0];
|
|
1888
|
+
assertUAsset(
|
|
1889
|
+
channel !== void 0,
|
|
1890
|
+
"MISSING_MESH_ATTRIBUTE",
|
|
1891
|
+
`${entry.name} has no data channel`,
|
|
1892
|
+
{
|
|
1893
|
+
attribute: entry.name
|
|
1894
|
+
}
|
|
1895
|
+
);
|
|
1896
|
+
return channel.values;
|
|
1897
|
+
}
|
|
1898
|
+
function componentAt(values, elementId, extent, component) {
|
|
1899
|
+
const value = values[elementId * extent + component];
|
|
1900
|
+
if (typeof value !== "number") {
|
|
1901
|
+
throw new UAssetError(
|
|
1902
|
+
"INVALID_MESH_REFERENCE",
|
|
1903
|
+
"Attribute data ended before its declared element count",
|
|
1904
|
+
{
|
|
1905
|
+
elementId,
|
|
1906
|
+
extent,
|
|
1907
|
+
component
|
|
1908
|
+
}
|
|
1909
|
+
);
|
|
1910
|
+
}
|
|
1911
|
+
return value;
|
|
1912
|
+
}
|
|
1913
|
+
function groupTrianglesByInstance(inputs) {
|
|
1914
|
+
const trianglesByGroup = /* @__PURE__ */ new Map();
|
|
1915
|
+
for (const triangleId of inputs.validTriangleIds) {
|
|
1916
|
+
const source = triangleId * inputs.triangleExtent;
|
|
1917
|
+
const corners = [0, 0, 0];
|
|
1918
|
+
for (let corner = 0; corner < 3; corner += 1) {
|
|
1919
|
+
const instanceId = inputs.triangleVertexInstances[source + corner];
|
|
1920
|
+
const outputIndex = inputs.instanceToOutput.get(instanceId ?? -1);
|
|
1921
|
+
if (outputIndex === void 0) {
|
|
1922
|
+
throw new UAssetError(
|
|
1923
|
+
"INVALID_MESH_REFERENCE",
|
|
1924
|
+
"Triangle references an invalid vertex instance",
|
|
1925
|
+
{
|
|
1926
|
+
triangleId,
|
|
1927
|
+
instanceId
|
|
1928
|
+
}
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
corners[corner] = outputIndex;
|
|
1932
|
+
}
|
|
1933
|
+
if (inputs.flipWinding) {
|
|
1934
|
+
const second = corners[1];
|
|
1935
|
+
corners[1] = corners[2];
|
|
1936
|
+
corners[2] = second;
|
|
1937
|
+
}
|
|
1938
|
+
const rawGroupId = inputs.triangleGroups ? componentAt(inputs.triangleGroups, triangleId, inputs.triangleGroupExtent, 0) : 0;
|
|
1939
|
+
const groupId = rawGroupId >= 0 ? rawGroupId : 0;
|
|
1940
|
+
const groupTriangles = trianglesByGroup.get(groupId) ?? [];
|
|
1941
|
+
groupTriangles.push(corners[0], corners[1], corners[2]);
|
|
1942
|
+
trianglesByGroup.set(groupId, groupTriangles);
|
|
1943
|
+
}
|
|
1944
|
+
return trianglesByGroup;
|
|
1945
|
+
}
|
|
1946
|
+
function expandVertexInstances(inputs) {
|
|
1947
|
+
const instanceToOutput = /* @__PURE__ */ new Map();
|
|
1948
|
+
const positions = new Float32Array(inputs.validInstanceIds.length * 3);
|
|
1949
|
+
const uvs = new Float32Array(inputs.validInstanceIds.length * 2);
|
|
1950
|
+
const normals = inputs.normalsByInstance ? new Float32Array(inputs.validInstanceIds.length * 3) : void 0;
|
|
1951
|
+
for (let outputIndex = 0; outputIndex < inputs.validInstanceIds.length; outputIndex += 1) {
|
|
1952
|
+
const instanceId = inputs.validInstanceIds[outputIndex];
|
|
1953
|
+
if (instanceId === void 0) {
|
|
1954
|
+
throw new UAssetError("INVALID_MESH_REFERENCE", "Vertex instance allocation ended early", {
|
|
1955
|
+
outputIndex
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
instanceToOutput.set(instanceId, outputIndex);
|
|
1959
|
+
const vertexId = inputs.vertexIndices[instanceId * inputs.vertexExtent];
|
|
1960
|
+
if (typeof vertexId !== "number" || !inputs.validVertexIds.has(vertexId)) {
|
|
1961
|
+
throw new UAssetError(
|
|
1962
|
+
"INVALID_MESH_REFERENCE",
|
|
1963
|
+
"Vertex instance references an invalid vertex",
|
|
1964
|
+
{
|
|
1965
|
+
instanceId,
|
|
1966
|
+
vertexId
|
|
1967
|
+
}
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
const [x, y, z] = convertVector(
|
|
1971
|
+
componentAt(inputs.positionsByVertex, vertexId, inputs.positionExtent, 0),
|
|
1972
|
+
componentAt(inputs.positionsByVertex, vertexId, inputs.positionExtent, 1),
|
|
1973
|
+
componentAt(inputs.positionsByVertex, vertexId, inputs.positionExtent, 2),
|
|
1974
|
+
inputs.options.convertCoordinates
|
|
1975
|
+
);
|
|
1976
|
+
positions[outputIndex * 3] = x;
|
|
1977
|
+
positions[outputIndex * 3 + 1] = y;
|
|
1978
|
+
positions[outputIndex * 3 + 2] = z;
|
|
1979
|
+
uvs[outputIndex * 2] = componentAt(inputs.uvsByInstance, instanceId, inputs.uvExtent, 0);
|
|
1980
|
+
const v = componentAt(inputs.uvsByInstance, instanceId, inputs.uvExtent, 1);
|
|
1981
|
+
uvs[outputIndex * 2 + 1] = inputs.options.flipV ? 1 - v : v;
|
|
1982
|
+
if (normals && inputs.normalsByInstance) {
|
|
1983
|
+
const [nx, ny, nz] = convertVector(
|
|
1984
|
+
componentAt(inputs.normalsByInstance, instanceId, inputs.normalExtent, 0),
|
|
1985
|
+
componentAt(inputs.normalsByInstance, instanceId, inputs.normalExtent, 1),
|
|
1986
|
+
componentAt(inputs.normalsByInstance, instanceId, inputs.normalExtent, 2),
|
|
1987
|
+
inputs.options.convertCoordinates
|
|
1988
|
+
);
|
|
1989
|
+
normals[outputIndex * 3] = nx;
|
|
1990
|
+
normals[outputIndex * 3 + 1] = ny;
|
|
1991
|
+
normals[outputIndex * 3 + 2] = nz;
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
return { positions, normals, uvs, instanceToOutput };
|
|
1995
|
+
}
|
|
1996
|
+
function buildGeometryFromMeshDescription(description, options) {
|
|
1997
|
+
const vertices = description.elements.get("Vertices");
|
|
1998
|
+
const vertexInstances = description.elements.get("VertexInstances");
|
|
1999
|
+
const triangles = description.elements.get("Triangles");
|
|
2000
|
+
assertUAsset(
|
|
2001
|
+
vertices !== void 0 && vertexInstances !== void 0 && triangles !== void 0,
|
|
2002
|
+
"INVALID_MESH_DESCRIPTION",
|
|
2003
|
+
"MeshDescription lacks required static-mesh element containers",
|
|
2004
|
+
{ available: [...description.elements.keys()] }
|
|
2005
|
+
);
|
|
2006
|
+
const positionAttribute = attribute(description, "Vertices", "Position", true);
|
|
2007
|
+
const vertexIndexAttribute = attribute(description, "VertexInstances", "VertexIndex", true);
|
|
2008
|
+
const uvAttribute = attribute(description, "VertexInstances", "TextureCoordinate", true);
|
|
2009
|
+
const normalAttribute = attribute(description, "VertexInstances", "Normal", false);
|
|
2010
|
+
const triangleIndexAttribute = attribute(description, "Triangles", "VertexInstanceIndex", true);
|
|
2011
|
+
const triangleGroupAttribute = attribute(description, "Triangles", "PolygonGroupIndex", false);
|
|
2012
|
+
const materialSlotAttribute = description.elements.get("PolygonGroups")?.channels[0]?.attributeSet.attributes.get("ImportedMaterialSlotName");
|
|
2013
|
+
const positionsByVertex = channelValues(positionAttribute);
|
|
2014
|
+
const vertexIndices = channelValues(vertexIndexAttribute);
|
|
2015
|
+
const uvsByInstance = channelValues(uvAttribute);
|
|
2016
|
+
const normalsByInstance = normalAttribute ? channelValues(normalAttribute) : void 0;
|
|
2017
|
+
const triangleVertexInstances = channelValues(triangleIndexAttribute);
|
|
2018
|
+
const triangleGroups = triangleGroupAttribute ? channelValues(triangleGroupAttribute) : void 0;
|
|
2019
|
+
const materialSlotNames = materialSlotAttribute ? channelValues(materialSlotAttribute) : void 0;
|
|
2020
|
+
const verticesChannel = vertices.channels[0];
|
|
2021
|
+
const instancesChannel = vertexInstances.channels[0];
|
|
2022
|
+
const trianglesChannel = triangles.channels[0];
|
|
2023
|
+
if (verticesChannel === void 0 || instancesChannel === void 0 || trianglesChannel === void 0) {
|
|
2024
|
+
throw new UAssetError(
|
|
2025
|
+
"INVALID_MESH_DESCRIPTION",
|
|
2026
|
+
"MeshDescription element container has no channels"
|
|
2027
|
+
);
|
|
2028
|
+
}
|
|
2029
|
+
const validVertexIds = new Set(verticesChannel.allocation.validIds);
|
|
2030
|
+
const validInstanceIds = instancesChannel.allocation.validIds;
|
|
2031
|
+
assertUAsset(
|
|
2032
|
+
validVertexIds.size > 0 && validInstanceIds.length > 0,
|
|
2033
|
+
"UNSUPPORTED_STATIC_MESH_LAYOUT",
|
|
2034
|
+
"Static mesh contains no renderable vertices"
|
|
2035
|
+
);
|
|
2036
|
+
const expanded = expandVertexInstances({
|
|
2037
|
+
validInstanceIds,
|
|
2038
|
+
validVertexIds,
|
|
2039
|
+
vertexIndices,
|
|
2040
|
+
vertexExtent: vertexIndexAttribute.extent,
|
|
2041
|
+
positionsByVertex,
|
|
2042
|
+
positionExtent: positionAttribute.extent,
|
|
2043
|
+
uvsByInstance,
|
|
2044
|
+
uvExtent: uvAttribute.extent,
|
|
2045
|
+
normalsByInstance,
|
|
2046
|
+
normalExtent: normalAttribute?.extent ?? 1,
|
|
2047
|
+
options
|
|
2048
|
+
});
|
|
2049
|
+
const instanceToOutput = expanded.instanceToOutput;
|
|
2050
|
+
const validTriangleIds = trianglesChannel.allocation.validIds;
|
|
2051
|
+
assertUAsset(
|
|
2052
|
+
validTriangleIds.length > 0,
|
|
2053
|
+
"UNSUPPORTED_STATIC_MESH_LAYOUT",
|
|
2054
|
+
"Static mesh contains no triangles"
|
|
2055
|
+
);
|
|
2056
|
+
const trianglesByGroup = groupTrianglesByInstance({
|
|
2057
|
+
validTriangleIds,
|
|
2058
|
+
triangleExtent: triangleIndexAttribute.extent,
|
|
2059
|
+
triangleVertexInstances,
|
|
2060
|
+
instanceToOutput,
|
|
2061
|
+
triangleGroups,
|
|
2062
|
+
triangleGroupExtent: triangleGroupAttribute?.extent ?? 1,
|
|
2063
|
+
flipWinding: options.flipWinding
|
|
2064
|
+
});
|
|
2065
|
+
const slotNameStride = materialSlotAttribute?.extent ?? 1;
|
|
2066
|
+
return assembleGeometry(
|
|
2067
|
+
expanded.positions,
|
|
2068
|
+
expanded.normals,
|
|
2069
|
+
expanded.uvs,
|
|
2070
|
+
trianglesByGroup,
|
|
2071
|
+
(groupId) => materialSlotNames?.[groupId * slotNameStride] ?? String(groupId),
|
|
2072
|
+
{
|
|
2073
|
+
vertices: verticesChannel.allocation.validIds.length,
|
|
2074
|
+
vertexInstances: validInstanceIds.length,
|
|
2075
|
+
triangles: validTriangleIds.length
|
|
2076
|
+
}
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2079
|
+
function tryMeshDescriptionPayload(payload, options) {
|
|
2080
|
+
for (const offset of findMeshDescriptionOffsets(payload)) {
|
|
2081
|
+
try {
|
|
2082
|
+
const description = parseMeshDescription(payload, offset);
|
|
2083
|
+
if (description.elements.has("Vertices") && description.elements.has("VertexInstances") && description.elements.has("Triangles")) {
|
|
2084
|
+
return {
|
|
2085
|
+
description,
|
|
2086
|
+
offset,
|
|
2087
|
+
build: buildGeometryFromMeshDescription(description, options)
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
} catch (error) {
|
|
2091
|
+
if (!(error instanceof UAssetError)) throw error;
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
return void 0;
|
|
2095
|
+
}
|
|
2096
|
+
function probeCompressedBuffer(buffer, codecs, options) {
|
|
2097
|
+
try {
|
|
2098
|
+
const payload = decompressCompressedBuffer(buffer, codecs);
|
|
2099
|
+
const found = tryMeshDescriptionPayload(payload, options);
|
|
2100
|
+
if (found) {
|
|
2101
|
+
return {
|
|
2102
|
+
selection: {
|
|
2103
|
+
layout: "mesh-description",
|
|
2104
|
+
payload: {
|
|
2105
|
+
frame: "decompressed",
|
|
2106
|
+
offset: found.offset,
|
|
2107
|
+
byteLength: found.description.byteLength
|
|
2108
|
+
},
|
|
2109
|
+
build: found.build,
|
|
2110
|
+
compressedBuffer: buffer
|
|
2111
|
+
}
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
return {};
|
|
2115
|
+
} catch (error) {
|
|
2116
|
+
if (!(error instanceof UAssetError)) throw error;
|
|
2117
|
+
return { missingCodecError: error.code === "MISSING_CODEC" ? error : void 0 };
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
function selectCompressedMeshDescription(bytes, codecs, options) {
|
|
2121
|
+
let missingCodecError;
|
|
2122
|
+
for (const offset of findCompressedBufferOffsets(bytes)) {
|
|
2123
|
+
let buffer;
|
|
2124
|
+
try {
|
|
2125
|
+
buffer = parseCompressedBuffer(bytes, offset);
|
|
2126
|
+
} catch (error) {
|
|
2127
|
+
if (!(error instanceof UAssetError)) throw error;
|
|
2128
|
+
continue;
|
|
2129
|
+
}
|
|
2130
|
+
if (buffer === void 0) continue;
|
|
2131
|
+
const probed = probeCompressedBuffer(buffer, codecs, options);
|
|
2132
|
+
if (probed.selection) return probed;
|
|
2133
|
+
missingCodecError ??= probed.missingCodecError;
|
|
2134
|
+
}
|
|
2135
|
+
return { missingCodecError };
|
|
2136
|
+
}
|
|
2137
|
+
function selectRawMesh(bytes, options) {
|
|
2138
|
+
const blob = findRawMeshBlobs(bytes)[0];
|
|
2139
|
+
if (blob === void 0) return void 0;
|
|
2140
|
+
return {
|
|
2141
|
+
layout: "raw-mesh",
|
|
2142
|
+
payload: { frame: "package", offset: blob.offset, byteLength: blob.byteLength },
|
|
2143
|
+
build: buildGeometryFromRawMesh(blob.mesh, options)
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
function selectBulkData(bytes, parseOptions, options) {
|
|
2147
|
+
const layout = readPackageLayout(bytes);
|
|
2148
|
+
if (layout === void 0) return {};
|
|
2149
|
+
const found = selectBulkSourceModel(bytes, layout, parseOptions, options);
|
|
2150
|
+
if (found.selection === void 0) {
|
|
2151
|
+
return found.blockingError === void 0 ? {} : { blockingError: found.blockingError };
|
|
2152
|
+
}
|
|
2153
|
+
return { selection: toPayloadSelection(found.selection) };
|
|
2154
|
+
}
|
|
2155
|
+
function toPayloadSelection(model) {
|
|
2156
|
+
return {
|
|
2157
|
+
layout: model.layout,
|
|
2158
|
+
payload: { frame: "bulk-data", offset: 0, byteLength: model.payloadByteLength },
|
|
2159
|
+
build: model.build,
|
|
2160
|
+
bulkData: model.bulkData
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
function buildUnrealInfo(summary, selected) {
|
|
2164
|
+
const compressedInfo = selected.compressedBuffer;
|
|
2165
|
+
return {
|
|
2166
|
+
packageTag: summary.packageTag,
|
|
2167
|
+
legacyFileVersion: summary.legacyFileVersion,
|
|
2168
|
+
...summary.fileVersionUE4 === void 0 ? {} : { fileVersionUE4: summary.fileVersionUE4 },
|
|
2169
|
+
...summary.fileVersionUE5 === void 0 ? {} : { fileVersionUE5: summary.fileVersionUE5 },
|
|
2170
|
+
...summary.licenseeVersion === void 0 ? {} : { licenseeVersion: summary.licenseeVersion },
|
|
2171
|
+
...summary.editorObjectVersion === void 0 ? {} : { editorObjectVersion: summary.editorObjectVersion },
|
|
2172
|
+
layout: selected.layout,
|
|
2173
|
+
payload: selected.payload,
|
|
2174
|
+
...compressedInfo === void 0 ? {} : {
|
|
2175
|
+
compressedBuffer: {
|
|
2176
|
+
offset: compressedInfo.offset,
|
|
2177
|
+
method: compressedInfo.method,
|
|
2178
|
+
compressor: compressedInfo.compressor,
|
|
2179
|
+
compressionLevel: compressedInfo.compressionLevel,
|
|
2180
|
+
rawSize: compressedInfo.totalRawSize,
|
|
2181
|
+
compressedSize: compressedInfo.totalCompressedSize,
|
|
2182
|
+
blockCount: compressedInfo.blockCount
|
|
2183
|
+
}
|
|
2184
|
+
},
|
|
2185
|
+
...selected.bulkData === void 0 ? {} : { bulkData: selected.bulkData }
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
function parseUAssetStaticMesh(input, options = {}) {
|
|
2189
|
+
const bytes = asBytes(input);
|
|
2190
|
+
const resolved = {
|
|
2191
|
+
convertCoordinates: options.convertCoordinates ?? true,
|
|
2192
|
+
flipWinding: options.flipWinding ?? true,
|
|
2193
|
+
flipV: options.flipV ?? false
|
|
2194
|
+
};
|
|
2195
|
+
const summary = readPackageSummary(bytes);
|
|
2196
|
+
const metadataScan = scanMetadata(bytes);
|
|
2197
|
+
const compressed = selectCompressedMeshDescription(
|
|
2198
|
+
bytes,
|
|
2199
|
+
{ oodle: options.oodle, lz4: options.lz4 },
|
|
2200
|
+
resolved
|
|
2201
|
+
);
|
|
2202
|
+
let selected = compressed.selection;
|
|
2203
|
+
if (selected === void 0) {
|
|
2204
|
+
const inlineFound = tryMeshDescriptionPayload(bytes, resolved);
|
|
2205
|
+
if (inlineFound) {
|
|
2206
|
+
selected = {
|
|
2207
|
+
layout: "mesh-description",
|
|
2208
|
+
payload: {
|
|
2209
|
+
frame: "package",
|
|
2210
|
+
offset: inlineFound.offset,
|
|
2211
|
+
byteLength: inlineFound.description.byteLength
|
|
2212
|
+
},
|
|
2213
|
+
build: inlineFound.build
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
let bulk = {};
|
|
2218
|
+
if (selected === void 0) {
|
|
2219
|
+
bulk = selectBulkData(bytes, options, resolved);
|
|
2220
|
+
selected = bulk.selection;
|
|
2221
|
+
}
|
|
2222
|
+
selected ??= selectRawMesh(bytes, resolved);
|
|
2223
|
+
if (selected === void 0) {
|
|
2224
|
+
if (bulk.blockingError) throw bulk.blockingError;
|
|
2225
|
+
if (compressed.missingCodecError) throw compressed.missingCodecError;
|
|
2226
|
+
throw new UAssetError(
|
|
2227
|
+
"UNSUPPORTED_STATIC_MESH_LAYOUT",
|
|
2228
|
+
"No supported static-mesh payload was found in this package",
|
|
2229
|
+
{
|
|
2230
|
+
fileVersionUE4: summary.fileVersionUE4,
|
|
2231
|
+
editorObjectVersion: summary.editorObjectVersion,
|
|
2232
|
+
// What was looked for, rather than a version range. A real UE4.27 pack reached here and
|
|
2233
|
+
// was told the parser covers "UE4.26–5.x editor static meshes with serialized
|
|
2234
|
+
// FMeshDescription data" — a claim that describes the file in front of it, so the only
|
|
2235
|
+
// conclusion left to the caller was that the asset is corrupt. Naming the three probes
|
|
2236
|
+
// that ran and matched nothing says the true thing: this is a coverage gap in the
|
|
2237
|
+
// payload forms, not a broken package.
|
|
2238
|
+
probed: "compressed-buffer, inline mesh-description, bulk-data, raw-mesh \u2014 none matched",
|
|
2239
|
+
supported: "UE4.26\u20135.x editor static meshes whose FMeshDescription is serialized inline or in a compressed buffer (UE5 compressed payloads need an `oodle` codec); UE4.2x editor packages whose FMeshDescription or FRawMesh source model is kept in FByteBulkData, inline, at the end of the package, or in a sibling .ubulk/.uptnl file whose bytes the caller supplies through `bulkDataFiles` (zlib-compressed payloads need a `zlib` codec); and UE4.18-era packages with inline uncompressed FRawMesh source models. IoStore containers, PAK archives, cooked render buffers, Nanite clusters and skeletal meshes are not read."
|
|
2240
|
+
}
|
|
2241
|
+
);
|
|
2242
|
+
}
|
|
2243
|
+
return {
|
|
2244
|
+
positions: selected.build.positions,
|
|
2245
|
+
normals: selected.build.normals,
|
|
2246
|
+
uvs: selected.build.uvs,
|
|
2247
|
+
indices: selected.build.indices,
|
|
2248
|
+
sections: selected.build.sections,
|
|
2249
|
+
bounds: computeBounds(selected.build.positions),
|
|
2250
|
+
metadata: {
|
|
2251
|
+
assetClass: "StaticMesh",
|
|
2252
|
+
engineVersion: metadataScan.engineVersion,
|
|
2253
|
+
objectPath: metadataScan.objectPath,
|
|
2254
|
+
packageByteLength: bytes.byteLength
|
|
2255
|
+
},
|
|
2256
|
+
unreal: buildUnrealInfo(summary, selected),
|
|
2257
|
+
sourceStats: selected.build.sourceStats
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
function fallbackMaterial() {
|
|
2261
|
+
return new MeshStandardMaterial({ color: 12110047, metalness: 0.08, roughness: 0.42 });
|
|
2262
|
+
}
|
|
2263
|
+
function createThreeGeometry(decoded) {
|
|
2264
|
+
const geometry = new BufferGeometry();
|
|
2265
|
+
geometry.setAttribute("position", new BufferAttribute(decoded.positions, 3));
|
|
2266
|
+
geometry.setAttribute("uv", new BufferAttribute(decoded.uvs, 2));
|
|
2267
|
+
if (decoded.normals) {
|
|
2268
|
+
geometry.setAttribute("normal", new BufferAttribute(decoded.normals, 3));
|
|
2269
|
+
} else {
|
|
2270
|
+
geometry.computeVertexNormals();
|
|
2271
|
+
}
|
|
2272
|
+
geometry.setIndex(new BufferAttribute(decoded.indices, 1));
|
|
2273
|
+
for (const section of decoded.sections) {
|
|
2274
|
+
geometry.addGroup(section.start, section.count, section.materialIndex);
|
|
2275
|
+
}
|
|
2276
|
+
geometry.computeBoundingBox();
|
|
2277
|
+
geometry.computeBoundingSphere();
|
|
2278
|
+
return geometry;
|
|
2279
|
+
}
|
|
2280
|
+
function createThreeObject(decoded, options = {}) {
|
|
2281
|
+
const geometry = createThreeGeometry(decoded);
|
|
2282
|
+
const chosen = options.materialFactory ? options.materialFactory(decoded) : decoded.sections.length > 1 ? decoded.sections.map(() => fallbackMaterial()) : fallbackMaterial();
|
|
2283
|
+
const material = Array.isArray(chosen) ? [...chosen] : chosen;
|
|
2284
|
+
const mesh = new Mesh(geometry, material);
|
|
2285
|
+
mesh.name = decoded.metadata.objectPath.split("/").at(-1) || "UnrealStaticMesh";
|
|
2286
|
+
mesh.userData.unreal = {
|
|
2287
|
+
...decoded.metadata,
|
|
2288
|
+
...decoded.unreal,
|
|
2289
|
+
sections: decoded.sections,
|
|
2290
|
+
sourceStats: decoded.sourceStats,
|
|
2291
|
+
directUasset: true
|
|
2292
|
+
};
|
|
2293
|
+
return mesh;
|
|
2294
|
+
}
|
|
2295
|
+
var UAssetLoader = class extends Loader {
|
|
2296
|
+
options;
|
|
2297
|
+
constructor(manager, options = {}) {
|
|
2298
|
+
super(manager);
|
|
2299
|
+
this.options = options;
|
|
2300
|
+
}
|
|
2301
|
+
parse(data) {
|
|
2302
|
+
return createThreeObject(parseUAssetStaticMesh(data, this.options.parse), this.options.three);
|
|
2303
|
+
}
|
|
2304
|
+
load(url, onLoad, onProgress, onError) {
|
|
2305
|
+
const fileLoader = new FileLoader(this.manager);
|
|
2306
|
+
fileLoader.setPath(this.path);
|
|
2307
|
+
fileLoader.setRequestHeader(this.requestHeader);
|
|
2308
|
+
fileLoader.setWithCredentials(this.withCredentials);
|
|
2309
|
+
fileLoader.setResponseType("arraybuffer");
|
|
2310
|
+
fileLoader.load(
|
|
2311
|
+
url,
|
|
2312
|
+
(data) => {
|
|
2313
|
+
try {
|
|
2314
|
+
if (!(data instanceof ArrayBuffer)) {
|
|
2315
|
+
throw new TypeError("Expected FileLoader to return an ArrayBuffer");
|
|
2316
|
+
}
|
|
2317
|
+
onLoad(this.parse(data));
|
|
2318
|
+
} catch (error) {
|
|
2319
|
+
onError?.(error);
|
|
2320
|
+
}
|
|
2321
|
+
},
|
|
2322
|
+
onProgress,
|
|
2323
|
+
onError
|
|
2324
|
+
);
|
|
2325
|
+
return this;
|
|
2326
|
+
}
|
|
2327
|
+
};
|
|
2328
|
+
|
|
2329
|
+
export { BULK_DATA_FLAG, COMPRESSED_BUFFER_MAGIC, COMPRESSION_METHOD, PACKAGE_FILE_TAG, UAssetError, UAssetLoader, createThreeGeometry, createThreeObject, decompressBulkData, decompressCompressedBuffer, findBulkDataHeaders, findCompressedBufferOffsets, findMeshDescriptionOffsets, findRawMeshBlobs, looksLikeMeshDescription, looksLikeMeshDescriptionUe4, parseBulkDataHeader, parseCompressedBuffer, parseMeshDescription, parseMeshDescriptionUe4, parseRawMesh, parseUAssetStaticMesh, readPackageLayout, readPackageSummary, resolveBulkDataPayload };
|