@threenative/raw-unreal
v0.1.4
Published
Loads raw Unreal editor .uasset static meshes directly into Three.js geometry — UE5 FMeshDescription and UE4.18 FRawMesh layouts, with no interchange conversion.
Maintainers
Readme
@threenative/raw-unreal
Loads raw Unreal editor .uasset static meshes directly into three.js geometry — no glTF/GLB/FBX/OBJ conversion step at any point.
raw .uasset → package summary → payload scan → decode → typed arrays → THREE.MeshThree serialized source-model layouts are decoded, from four payload stores:
| Layout | Engine generation | Where the data lives |
|---|---|---|
| mesh-description | UE5.x (FMeshDescription, named element containers) | Inline, or inside a UE5 FCompressedBuffer package-trailer payload |
| mesh-description-ue4 | UE4.2x (FMeshDescription, fixed-order containers) | FByteBulkData, usually zlib-compressed at the end of the package |
| raw-mesh | UE4.6–UE4.2x (FRawMesh source models) | Inline uncompressed, or FByteBulkData |
FByteBulkData payloads are read wherever the flags put them: inline after the header, at the end
of the .uasset against the summary's BulkDataStartOffset, or in a sibling .ubulk/.uptnl
file whose bytes the caller supplies through bulkDataFiles.
Every path is self-validating: a candidate offset is trusted only after a full parse consumes it
exactly with every count agreeing, and the summary walk that anchors bulk data is accepted only
when it ends on the package's own name table. Unsupported layouts throw a typed UAssetError
with a code and details; nothing invents fallback geometry.
Use
import { UAssetLoader } from "@threenative/raw-unreal";
const loader = new UAssetLoader(manager, { parse: { oodle } });
const mesh = await new Promise((resolve, reject) =>
loader.load("/assets/SM_pine01.uasset", resolve, undefined, reject),
);
scene.add(mesh); // materials via mesh.userData.unreal.sections / geometry groupsThe format layer is independent of three.js until the final adapter:
import { parseUAssetStaticMesh } from "@threenative/raw-unreal";
const decoded = parseUAssetStaticMesh(bytes, { oodle });
// decoded.positions / normals / uvs / indices / sections / bounds / metadata / unrealCodec injection and licensing
This package is MIT and bundles no compression codecs. UE5 FCompressedBuffer payloads are
commonly Oodle-compressed, and every Oodle decompressor that is known to work in JavaScript is
GPL-licensed (ooz-wasm). To keep the GPL out of this package while keeping the capability
reachable, the loader takes codecs as call arguments:
import { decompress as oodle } from "ooz-wasm"; // GPL-3.0-or-later — the game's choice to make
parseUAssetStaticMesh(bytes, { oodle });Adding ooz-wasm to a game applies that game's GPL obligations to that game's distribution;
it never touches this package. Uncompressed payloads need no codec, and LZ4 payloads take an
lz4(compressed, rawSize) function the same way. A payload whose codec was not supplied throws
MISSING_CODEC rather than guessing.
UE4 editor bulk data is stored zlib-compressed, which takes a zlib(compressed, rawSize) function
on the same terms — node:zlib's inflateSync in Node, any of the small MIT inflate libraries in
the browser:
import { inflateSync } from "node:zlib";
parseUAssetStaticMesh(bytes, { zlib: (data) => new Uint8Array(inflateSync(data)) });The test suite uses ooz-wasm as a devDependency only — it decodes the committed fixture and
ships in no published artifact.
Fixture provenance
fixtures/SM_cube.uasset is a real Unreal Engine 5.7 editor package from the MIT-licensed
uassets sample set, pinned by SHA-256 in the specs —
see fixtures/PROVENANCE.md. The FRawMesh specs run against synthetic packages built in
__tests__/fixture-builder.ts; the real-pack conformance spec runs only when the licensed
Landscape Pro pack is present on disk and commits nothing from it.
Current compatibility
Decoded: legacy-tag uncooked editor packages (.uasset/.umap), the package summary prefix
and the full summary walk (TotalHeaderSize, BulkDataStartOffset, FEditorObjectVersion) for
LegacyFileVersion −3 through −8, UE5 FCompressedBuffer payloads (uncompressed/Oodle/LZ4
methods), UE5 and UE4.2x FMeshDescription serializations, UE4-era FRawMesh source models,
FByteBulkData payloads stored inline, at the end of the package, or in a supplied sibling file,
FArchive::SerializeCompressed zlib containers, per-wedge positions/normals/UVs, polygon-group
sections with their ImportedMaterialSlotNames from a MeshDescription, Unreal→three.js coordinate
conversion ((x, z, −y)) and winding repair.
Not decoded (throws, honestly): IoStore containers (.utoc/.ucas), PAK archives, cooked
render buffers, Nanite clusters, skeletal meshes and skin weights, textures and material graphs,
LegacyFileVersion −9 and below (UE 5.5+ replaced the package GUID with a saved hash; the summary
walk declines rather than guessing, so bulk data in those packages is not reached), material-slot
names from FRawMesh packages (the section indices are real; the names need the package name map),
and full .umap actor reconstruction.
