Skip to main content

Let's Read .uasset Together

strvRiku Ishikawa
strv

Hey everyone, have you been reading UAsset files?

This article is meant to help you become better friends with UAsset. It's a long article, so let's start with a bit of fun. Open the Content folder in your project and drag & drop a uasset file into the area below (everything is processed locally in your browser, so don't worry about anything being uploaded anywhere).

Drop a .uasset file here, or click to select

...Did you enjoy that? If you understand this article (and the engine implementation it covers), you might be able to create things like this yourself.

.uasset Overview

.uasset is Unreal Engine's asset file format. Virtually all assets that you import, create, or edit in the editor—Blueprints, Materials, StaticMeshes, etc.—are saved in this format.

In day-to-day development, the editor hides everything for you, so you never have to think about what's inside. UAsset might seem like some mysterious UE-specific file, but just like common file formats such as PNG or MP4, it contains well-structured data. If you know the specification, you can read it.

Environment

  • Unreal Engine 5.3 - 5.7

This article covers uncooked (editor-editable) uasset files. Cooked assets after packaging have different property serialization methods, editor-only data is stripped, and the structure changes significantly. We won't cover .uexp separation or IoStore format here.

Why Understanding the UAsset Format is Useful

You might think—what's the point of knowing what's inside a uasset? It's true that you rarely need to think about it during normal development, but there are situations where this knowledge comes in handy.

  • Asset Analysis
    • When you want to extract specific information from large numbers of assets or investigate dependencies, understanding the format lets you build your own tools
  • Compatibility Issue Investigation
    • When you encounter assets that won't load between engine versions, examining the version information in the header helps isolate the cause
  • Understanding Packaging
    • Understanding how dependencies between assets are recorded gives you better insight into what gets included during cooking and packaging, and why certain assets end up in your build
  • Simple Reading Outside UE
    • When you want to check basic asset information (class, dependencies, etc.) without launching the editor

In this article, we'll explain the basic structure of uasset files. We'll reference engine source code like PackageFileSummary.h and LinkerLoad.cpp to get a grasp of the overall picture.

UAsset Fundamentals

Before diving into format details, we need to understand "what" uasset stores and "how" it stores it. This section organizes the internal engine concepts.

What is UPackage?

In Unreal Engine, assets are managed in units called Packages. One .uasset file corresponds to one package.

Packages are represented internally as the UPackage class. UPackage itself inherits from UObject, so it's handled using the same mechanisms as other objects.

Package.h
UCLASS(MinimalAPI, Config=Engine)
class UPackage : public UObject
{
// Package flags
std::atomic<uint32> PackageFlagsPrivate;

// Path information for load source
FPackagePath LoadedPath;

// ...
};

Outer Hierarchy

A package contains multiple UObjects. These objects form a hierarchical structure through a parent-child relationship called Outer, where the topmost Outer is always UPackage.

For example, a Blueprint asset has a hierarchy like this:

UPackage "/Game/MyBlueprint"
└─ UBlueprint "MyBlueprint"
└─ UBlueprintGeneratedClass "MyBlueprint_C"
└─ UFunction "ExecuteUbergraph_MyBlueprint"

This hierarchical relationship is expressed in the file format through a field called OuterIndex.

Information Contained in a Package

When you import anything into UE, it becomes a uasset. PNG, FBX, WAV—they all become uasset. You might wonder what's actually inside.

Blueprints contain graph information, Textures contain image data, StaticMeshes contain geometry data—the content differs by asset type, but in fact, they're all expressed using common data structures.

Name Map

The foundation of everything is the Name Map. All FNames used within the package (class names, property names, object names, etc.) are stored here, and referenced by index from various locations.

Export

Objects defined within this package. Each export holds information like "what class is this an instance of," "what is the parent object," and "where is the serialized data." For Blueprints, the BP class itself and function objects are recorded as exports; for Textures, the texture object is recorded.

Import

References to external packages. The parent class being inherited from, referenced materials, other assets being used—things this package depends on are recorded as imports.

Bulk Data

Large binary data like texture pixel data or audio waveform data. This is managed separately from export serialization data and may be stored at the end of the file or in a separate file (.ubulk).

Other Metadata

In addition to these, dependency relationships between exports (Depends Map), asset registry information, and thumbnail images are also included.

Concrete Example: Blueprint Asset

When we actually parse BP_ThirdPersonCharacter.uasset from the UE5 ThirdPerson template, we find the following data:

  • Name Map: 281 entries
  • Import: 87 entries
  • Export: 42 entries

Name Map (excerpt):

[  0] /Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter
[ 1] /Script/CoreUObject
[ 2] /Script/Engine
[ 33] BP_ThirdPersonCharacter
[ 34] BP_ThirdPersonCharacter_C

Package paths, dependency package names, object names, property names—all strings are stored here.

Import (excerpt):

[ 40] /Script/CoreUObject.Class -> TP_ThirdPersonCharacter
[ 71] /Script/CoreUObject.Package -> /Script/Engine
[ 84] /Script/TP_ThirdPerson.TP_ThirdPersonCharacter -> Default__TP_ThirdPersonCharacter

Import 40 is a reference to the parent class TP_ThirdPersonCharacter, and Import 71 is a reference to the engine package.

Export (excerpt):

[  1] BP_ThirdPersonCharacter : Blueprint (outer: root)
[ 2] BP_ThirdPersonCharacter_C : BlueprintGeneratedClass (outer: root)
[ 3] Default__BP_ThirdPersonCharacter_C : BP_ThirdPersonCharacter_C (outer: root)
[ 7] EventGraph : EdGraph (outer: BP_ThirdPersonCharacter)
[ 12] ExecuteUbergraph_BP_ThirdPersonCharacter : Function (outer: BP_ThirdPersonCharacter_C)

Export 1 is the Blueprint asset itself, Export 2 is the generated class, and Export 3 is the CDO (Class Default Object). EventGraph has Blueprint as its outer, forming the Outer hierarchy.

The important point here is that strings like "BP_ThirdPersonCharacter" are not stored directly. In the actual file, they're recorded as indices into the Name Map, and you need to reference the Name Map to get the name.

Version Compatibility

Unreal Engine is frequently updated, and the uasset format changes along with it. Types of changes include new property types, extensions to existing structures, and changes to serialization methods.

Officially, upgrading the engine "one version at a time" is recommended, but when you actually read the engine code, you'll find that uasset has a surprisingly well-thought-out version compatibility system.

The engine source defines VER_UE4_OLDEST_LOADABLE_PACKAGE = 214 as the oldest loadable version. Since the UE4 version enum has over 500 entries, 214 corresponds to early UE4. This means that even UE5.7 maintains compatibility to load assets from early UE4.

UAsset holds detailed version information in its header:

  • Engine Version
    • Version numbers for both UE4 and UE5
    • Identifies which engine version saved the asset
  • Custom Versions
    • Version numbers independently maintained by each engine subsystem (animation, materials, physics, etc.)
    • Managed with GUIDs as keys, allowing each subsystem to evolve independently
    • Over 50 subsystems each have their own version

When loading an asset, the engine checks these version values and performs conversion from old formats to new formats as needed. The engine source is full of version branches like if (Ar.CustomVer(...) >= SomeVersion), maintaining compatibility over many years.

However, being able to read a uasset and having its contents work correctly are different things. If Blueprint nodes have been deprecated, material parameters have changed, or the code using the asset has changed, it may load but not work as expected. The "one version at a time" recommendation is probably meant to verify this semantic compatibility as you go.

UAsset File Format

Now that we understand what information uasset holds, let's see how it's actually stored in the file.

Overall Structure of a UAsset File

A uasset file has the following structure:

The position of each section is not fixed but determined by offset values stored in the header (Package File Summary). This design allows backward compatibility to be maintained even when sections are added in version updates.

This article won't exhaustively cover all sections (that's not realistic given Unreal Engine's scale). Instead, we'll focus on Package File Summary, Name Map, Import Map, and Export Map as the foundation for understanding uasset, while also explaining Thumbnail and Bulk Data.

Package File Summary (Header)

The Package File Summary is like the table of contents of a uasset file. Located at the beginning of the file, it holds metadata about the entire package (version information, package flags, etc.) and offsets to each subsequent section.

When loading a uasset, you first parse this header to understand the overall structure, then jump to the necessary sections to read data. In the engine, it's defined as the FPackageFileSummary struct, with serialization processing in PackageFileSummary.cpp.

Binary Layout

To get a feel for it, let's look at the actual binary layout. Conditional fields may or may not be present depending on version and flags.

#SizeFieldDescription
14TagMagic number (0x9E2A83C1)
24LegacyFileVersionFormat version (-9 is latest)
34LegacyUE3VersionFor UE3 compatibility (condition: ≠-4)
44FileVersionUE4UE4 version number
54FileVersionUE5UE5 version number (condition: ≤-8)
64FileVersionLicenseeUELicensee version
720SavedHashPackage hash (UE5.1+)
84TotalHeaderSizeTotal header size
9VariableCustomVersionsPer-subsystem versions
10VariablePackageNamePackage name (deprecated)
114PackageFlagsPackage flags
124+4NameCount, NameOffsetName Map info
134+4ExportCount, ExportOffsetExport Map info
144+4ImportCount, ImportOffsetImport Map info
............
Full Field List (44 items)
#SizeFieldDescriptionCondition
14TagMagic number (0x9E2A83C1)
24LegacyFileVersionFormat version (-9 is latest)
34LegacyUE3VersionFor UE3 compatibility≠ -4
44FileVersionUE4UE4 version number
54FileVersionUE5UE5 version number≤ -8
64FileVersionLicenseeUELicensee version
720SavedHashPackage hashUE5.1+
84TotalHeaderSizeTotal header sizeUE5.1+
9VariableCustomVersionsPer-subsystem versions≤ -2
104TotalHeaderSizeTotal header sizeBefore UE5.1
11VariablePackageNamePackage name (deprecated)
124PackageFlagsPackage flags
134+4NameCount, NameOffsetName Map count and offset
144+4SoftObjectPathsCount, OffsetSoft object path referencesUE5.1+
15VariableLocalizationIdLocalization IDUE4.14+
164+4GatherableTextDataCount, OffsetGatherable text dataUE4.7+
174+4ExportCount, ExportOffsetExport Map count and offset
184+4ImportCount, ImportOffsetImport Map count and offset
194×4CellExport/ImportCount, OffsetVerse cell infoUE5.5+
204MetaDataOffsetMetadata offsetUE5.4+
214DependsOffsetDepends Map offset
224+4SoftPackageRefsCount, OffsetSoft package referencesUE4.13+
234SearchableNamesOffsetSearchable names offsetUE4.15+
244ThumbnailTableOffsetThumbnail table offset
254+4ImportTypeHierarchiesCount, OffsetImport type hierarchy infoUE5.7+
2616GuidPackage GUIDBefore UE5.1
2716PersistentGuidPersistent GUIDEditor
284GenerationCountGeneration info count
29VariableGenerations[]Past version info
30VariableSavedByEngineVersionEngine version at save timeUE4.5+
31VariableCompatibleWithEngineVersionCompatible engine versionUE4.12+
324CompressionFlagsCompression flags
33VariableCompressedChunks[]Compressed chunks (always empty)
344PackageSourcePackage source identifier
35VariableAdditionalPackagesToCook[]Additional cook targets (always empty)
364NumTextureAllocationsTexture allocation count (always 0)> -7
374AssetRegistryDataOffsetAsset registry data offset
388BulkDataStartOffsetBulk data start offset
394WorldTileInfoDataOffsetWorld tile info offsetUE4.11+
40VariableChunkIDs[]Streaming chunk IDsUE4.10+
414+4PreloadDependencyCount, OffsetPreload dependenciesUE4.18+
424NamesReferencedFromExportDataCountNames referenced from export dataUE5.0+
438PayloadTocOffsetPayload TOC offsetUE5.0+
444DataResourceOffsetData resource offsetUE5.3+

The CustomVersions structure is "Count (i32) + (GUID 16bytes + Version i32) × Count". FString is "Length (i32) + UTF-8/UTF-16 string".

In actual parsing, you read through fields with conditional branches based on version values. Looking at operator<< in PackageFileSummary.cpp really shows how messy this gets. It's surprising how much code for UE3 format still remains.

Magic Number (Tag)

A magic number is a fixed byte sequence placed at the beginning of a file to identify the file format. Many file formats use this—PNG's 89 50 4E 47, PDF's 25 50 44 46, etc.—and programs use it to determine the format when reading a file.

The first 4 bytes of a uasset file are defined as a magic number with these values:

ObjectVersion.h
#define PACKAGE_FILE_TAG         0x9E2A83C1
#define PACKAGE_FILE_TAG_SWAPPED 0xC1832A9E // Byte order reversed

In the engine's FLinkerLoad::LoadPackageInternal, this magic number is read first to determine if it's a valid package file. PACKAGE_FILE_TAG_SWAPPED is provided to detect files saved with different endianness.

Version Information

As mentioned in the overview, uasset is strongly version-compatibility conscious. The header contains version information, and the engine branches processing based on these values when loading.

FieldDescription
LegacyFileVersionNegative integer. Smaller values are newer (-4, -7, -8, etc.). -8 or below for UE5
FileVersionUE4UE4 version number. Always serialized even for UE5 assets
FileVersionUE5UE5 version number (1000 or above). 0 for UE4 assets
CustomVersionsCustom versions per subsystem (array of GUID + version number)

FileVersionUE4 and FileVersionUE5 are combined in the FPackageFileVersion struct, allowing you to determine which engine version saved the asset.

The reason FileVersionUE4 is serialized even for UE5 assets is for compatibility with existing version checks. There are many checks throughout the engine in the format if (Version >= VER_UE4_XXX), and for UE5 assets, this field is set to the final UE4 version value. This way, all UE4 version checks automatically pass, and UE5-specific features can be checked with FileVersionUE5.

ObjectVersion.h
struct FPackageFileVersion
{
// UE4 version
int32 FileVersionUE4 = 0;
// UE5 version (1000 or above is UE5)
int32 FileVersionUE5 = 0;
};

Let's also look at how CustomVersions is specifically used.

DevObjectVersion.cpp
// Custom version for Editor subsystem
const FGuid FEditorObjectVersion::GUID(0xE4B068ED, 0xF49442E9, 0xA231DA0B, 0x2E46BB41);
FDevVersionRegistration GRegisterEditorObjectVersion(
FEditorObjectVersion::GUID,
FEditorObjectVersion::LatestVersion,
TEXT("Dev-Editor")
);

Some notable CustomVersions include:

CustomVersionPurpose
FEditorObjectVersionEditor features in general (FText, SplineComponent, etc.)
FReleaseObjectVersionChanges for release builds
FAnimationCustomVersionAnimation-related
FSkeletalMeshCustomVersionSkeletalMesh structural changes
FLandscapeCustomVersionLandscape-related

Let's look at an actual usage example with FText serialization.

Text.cpp
Ar.UsingCustomVersion(FEditorObjectVersion::GUID);

if (Ar.CustomVer(FEditorObjectVersion::GUID) >= FEditorObjectVersion::TextFormatArgumentDataIsVariant)
{
// Read/write in newer format
Record << SA_VALUE(TEXT("Type"), TypeAsByte);
}

Ar.CustomVer() retrieves the CustomVersion recorded in the package, and processing branches based on whether it's at or above a specific version.

Package Flags

UPackage has various flags that represent the nature of the package.

FlagDescription
PKG_ContainsMapPackage contains ULevel/UWorld
PKG_CookedCooked (packaged)
PKG_EditorOnlyEditor-only package
PKG_CompiledInPackage generated from C++ code (/Script/Engine, etc.)
PKG_NewlyCreatedNewly created package (unsaved)

Key Offset Fields

The header stores offsets to each section. Here are the main ones:

FieldDescription
TotalHeaderSizeTotal header size
NameCount / NameOffsetName Map entry count and offset
ImportCount / ImportOffsetImport Map entry count and offset
ExportCount / ExportOffsetExport Map entry count and offset
DependsOffsetOffset to Depends Map
SoftPackageReferencesCount / OffsetSoft package reference info
ThumbnailTableOffsetOffset to thumbnail table
AssetRegistryDataOffsetOffset to asset registry data
BulkDataStartOffsetBulk data start position

The engine uses these offsets to jump to and read from the necessary sections.

Field Evolution Across Versions

An important note here is that header fields increase and decrease across versions. The engine source defines constants as EUnrealEngineObjectUE5Version.

Excerpt from ObjectVersion.h
enum EUnrealEngineObjectUE5Version
{
INITIAL_VERSION = 1000,
NAMES_REFERENCED_FROM_EXPORT_DATA = 1001,
PAYLOAD_TOC = 1002,
// ...
ADD_SOFTOBJECTPATH_LIST = 1008,
// ...
METADATA_SERIALIZATION_OFFSET = 1014,
VERSE_CELLS = 1015,
PACKAGE_SAVED_HASH = 1016,
};

For example, fields added in UE5 include:

VersionAdded Fields
1008 (ADD_SOFTOBJECTPATH_LIST)SoftObjectPathsCount, SoftObjectPathsOffset
1014 (METADATA_SERIALIZATION_OFFSET)MetaDataOffset
1015 (VERSE_CELLS)CellExportCount/Offset, CellImportCount/Offset
1016 (PACKAGE_SAVED_HASH)SavedHash (changed from GUID to FIoHash)

The engine checks these version values to determine field presence and branches serialization processing accordingly. Looking at these reveals not just the format's history but also gives you a peek at names of features yet to be added—kind of interesting.

Name Map

The Name Map is a table that aggregates all names used within the package (class names, property names, object names, etc.). Throughout the file, instead of embedding strings directly, indices into the Name Map are recorded, eliminating duplicates and reducing file size.

Difference Between In-File FName and Runtime FName

FName in the file is fundamentally different from runtime FName.

Runtime FName:

  • Index into a global NameTable shared across the entire engine
  • Initialized at process startup, NAME_None is always index 0
  • Strings are stored only once and shared across all objects

In-File FName:

  • Index into a NameMap independent to each package
  • The same string can have different indices in different packages
  • The file records local indices unrelated to the global table
In-file FName reference format
// FName reference is 8 bytes
int32 NameIndex; // Index into package-local NameMap
int32 Number; // Instance number (for distinguishing same-named objects)

The Number field is used to distinguish multiple objects with the same name. For example, names like MyActor_0, MyActor_1 reference the same Name Map entry but are distinguished by Number (Number 0 means no suffix, 1 or above means _0, _1, ...).

Note that this is different from FNameEntrySerialized, which is used to serialize Name Map entries themselves. FNameEntrySerialized is a struct containing the actual string and hash values, used for parsing the Name Map.

Index Conversion During Load

When loading a package, the Linker converts file indices to runtime indices with the following process:

NameMap loading in LinkerLoad.cpp
FNameEntrySerialized NameEntry(ENAME_LinkerConstructor);
for (int32 Idx = 0; Idx < NameCount; ++Idx)
{
*this << NameEntry;
// Register the string read from file as a global FName,
// and store its global index in NameMap
NameMap.Emplace(FName(NameEntry).GetDisplayIndex());
}

This NameMap array functions as a mapping table. To resolve the name at file index N, reference NameMap[N] and use the global FNameEntryId stored there to retrieve the string from the global table.

Name Map Serialization

The Name Map is serialized as an array of strings.

Each entry is serialized as a length-prefixed string (FString). From a certain UE4 version onward (VER_UE4_NAME_HASHES_SERIALIZED), hash values are also serialized together.

Reading FNameEntrySerialized
// String body
FString Name;
Ar << Name;

// Hash values (for newer versions)
if (Ar.UEVer() >= VER_UE4_NAME_HASHES_SERIALIZED)
{
uint16 DummyHashes[2];
Ar << DummyHashes[0] << DummyHashes[1];
}

An important point is that the first entry in the Name Map (index 0) is not necessarily "None". In the runtime global table, NAME_None is always index 0, but since the in-file Name Map is independent per package, the position of "None" varies by package.

Import Map

The Import Map is a list of external objects this package depends on. References to parent classes being inherited, materials being referenced, textures being used—objects existing in other packages are recorded here.

For example, if a Blueprint asset inherits from the Actor class, a reference to /Script/Engine.Actor is recorded as an Import.

Import Map Binary Layout

The Import Map consists of Summary.ImportCount entries arranged consecutively starting from the position Summary.ImportOffset. Each entry has the following layout:

OffsetSizeFieldDescription
08ClassPackagePackage name containing the class (FName)
88ClassNameClass name (FName)
164OuterIndexReference to Outer (FPackageIndex)
208ObjectNameObject name (FName)
288PackageNameOwning package name (Editor only, UE4.18+)
364bImportOptionalOptional flag (UE5.1+)

FName is 8 bytes as int32 NameIndex + int32 Number. FPackageIndex is 4 bytes as int32, where negative values point to Import and positive values point to Export. Bools are—perhaps surprisingly—serialized as 4-byte integers. This is a historical specification to maintain compatibility with the UBOOL type from UE3 and earlier.

Entry size varies by version, so it can't be treated as a fixed size. The engine serializes and reads each entry sequentially.

Imports can have hierarchical structure. For example, a reference to /Script/Engine.Actor is expressed as a chain of Import entries like this:

  1. Import 0: Package /Script/Engine (OuterIndex = 0)
  2. Import 1: Class Actor (OuterIndex = references Import 0)

This OuterIndex is a type called FPackageIndex, a mechanism that can reference both Imports and Exports.

FPackageIndex

FPackageIndex is an index pointing to an entry in either the Import Map or Export Map. It's a wrapper around int32 that distinguishes the reference target by the sign of the value.

Interpreting FPackageIndex
// Relationship between FPackageIndex value and reference target
// 0 : null (points nowhere)
// Negative: Index into Import Map (-1 → Import[0], -2 → Import[1], ...)
// Positive: Index into Export Map (1 → Export[0], 2 → Export[1], ...)

bool IsNull() const { return Index == 0; }
bool IsImport() const { return Index < 0; }
bool IsExport() const { return Index > 0; }

int32 ToImport() const { return -Index - 1; } // Import[0] is -1
int32 ToExport() const { return Index - 1; } // Export[0] is 1

This mechanism allows object references to be handled uniformly whether they're from external packages or within the same package. Simple, but a nice design.

Export Map

The Export Map is a list of objects defined within this package. While Import is "reference to external," Export is "what this file provides."

For Blueprint assets, the Blueprint class itself, default objects, and function graphs are recorded as Exports.

Export Map Binary Layout

The Export Map consists of Summary.ExportCount entries arranged consecutively starting from the position Summary.ExportOffset. It has more fields than Import Map and varies more significantly across versions.

OffsetSizeFieldDescription
04ClassIndexReference to class (FPackageIndex)
44SuperIndexReference to super class (UStruct only)
84TemplateIndexReference to template (UE4.14+)
124OuterIndexReference to Outer
168ObjectNameObject name (FName)
244ObjectFlagsObject flags
288SerialSizeSize of serialized data
368SerialOffsetPosition of serialized data
444bForcedExportForced export flag
484bNotForClientExclude from client flag
524bNotForServerExclude from server flag
564PackageFlagsPackage flags
604bNotAlwaysLoadedForEditorGameNot always loaded for editor game flag
644bIsAssetAsset flag
.........(Dependency info, etc., added in UE4.17+)

SerialSize and SerialOffset were 4 bytes (int32) before UE4.14. The Export Map stores only object metadata; the actual property data is stored separately at the position indicated by SerialOffset.

bIsAsset Flag

FObjectExport has a flag called bIsAsset that indicates whether that export is an object displayed in the Content Browser (for Blueprints, the BP itself; for Materials, the material itself).

Checking bIsAsset
for (const FObjectExport& Export : ExportMap)
{
if (Export.bIsAsset)
{
// Object displayed in Content Browser
}
}

An export with null (0) OuterIndex means "object directly under UPackage," but Blueprint packages can have multiple top-level objects (Blueprint itself, BlueprintGeneratedClass, CDO, etc.). bIsAsset is a flag to identify the representative object that appears in the Content Browser among these.

Accessing Serialized Data

Each Export indicates the position and size of actual object data through SerialOffset and SerialSize. By reading the byte sequence in this range, you can access the object's properties and internal data.

However, interpreting this byte sequence requires type-specific processing. Since it gets quite complex from here, this article won't cover it.

Class Path Resolution

Now that we've seen the structure of Import Map, Export Map, and Name Map, we can combine them to resolve object class paths.

For example, if an export's ClassIndex points to Import[3], and the Import structure is:

Import[2]: ClassName="Package", ObjectName="/Script/Engine", OuterIndex=0
Import[3]: ClassName="Class", ObjectName="Actor", OuterIndex=-3 (→Import[2])

In this case, the class path is /Script/Engine.Actor. By recursively following OuterIndex and resolving names from the Name Map to concatenate them, you can build the complete path.

Pseudocode for class path resolution
FString ResolveImportPath(int32 ImportIndex)
{
const FObjectImport& Import = ImportMap[ImportIndex];
FString Name = NameMap[Import.ObjectName.Index];

if (Import.OuterIndex.IsNull())
{
return Name;
}
else if (Import.OuterIndex.IsImport())
{
FString OuterPath = ResolveImportPath(Import.OuterIndex.ToImport());
return OuterPath + TEXT(".") + Name;
}
return Name;
}

Thumbnail

The thumbnail images displayed in the Content Browser are embedded within the uasset file.

Here's something interesting: these thumbnails are actually the result of rendering the asset in the editor, saved directly as PNG or JPEG. Since it's a standard image format rather than GPU-specific encoding, you can extract the relevant portion from the uasset and display it as-is.

Multiple Thumbnails

Thumbnails can be saved for each export object within a package. For typical assets (Blueprint, Material, etc.) there's only one for the main object, but the format allows storing multiple thumbnails.

Thumbnail Storage Location

At the position pointed to by ThumbnailTableOffset is the thumbnail index (table of contents), with the image data for each thumbnail stored before it.

Thumbnail Table Structure

The Thumbnail Table consists of two parts: the index and the image data.

[Thumbnail image data 0]
[Thumbnail image data 1]
...
[Position pointed to by ThumbnailTableOffset]
├─ Count (int32)
├─ Entry 0: ObjectClass, ObjectPath, FileOffset
├─ Entry 1: ObjectClass, ObjectPath, FileOffset
└─ ...

Each index entry has the following structure:

OffsetSizeFieldDescription
0VariableObjectClassObject's class name (FString)
VariableVariableObjectPathObject's path (FString)
Variable4FileOffsetImage data offset within file

Thumbnail Image Data

Each thumbnail's image data is serialized as FObjectThumbnail.

OffsetSizeFieldDescription
04ImageWidthImage width
44ImageHeightImage height (negative means JPEG compression)
84DataLengthLength of compressed data
12VariableCompressedDataPNG or JPEG compressed image data

If ImageHeight is negative, it indicates JPEG compression; positive indicates PNG compression. The actual height is the absolute value.

Only CompressedImageData (already compressed image) is saved to file; the decompressed pixel data ImageData is not written to disk. CompressedImageData is a byte sequence that can be decoded directly as PNG/JPEG.

Bulk Data

Some assets contain large amounts of binary data, such as texture mipmaps or audio waveforms. Such data is managed through a mechanism called Bulk Data.

Why Separate Bulk Data?

With normal property serialization, all data is expanded into memory when loading an object. However, loading all of a huge binary like texture pixel data every time is inefficient.

Bulk Data solves this problem by separating and managing the payload (actual data).

  • Lazy Loading: Load only the Summary and Export Map first; payload isn't loaded until needed
  • Streaming: Load only necessary parts with async IO (texture streaming, etc.)
  • Memory Mapping: Map the file directly to memory, leveraging the OS paging mechanism

Relationship Between Exports and Bulk Data

During export serialization, asset-specific code calls FBulkData::Serialize to write Bulk Data. For example, UTexture2D serializes FBulkData for each mipmap.

At this time, there are two options for where to store the Bulk Data payload:

Storage LocationDescription
InlineWrite payload on the spot during export serialization
End of FileWrite only metadata during serialization; payload is gathered after BulkDataStartOffset

With inline, the payload exists within the Export Data region as part of the export data. With end of file, only metadata (flags, size, offset) is recorded in Export Data, and the actual data is stored in a separate region.

Also, for Uncooked Assets (Editor Assets), the payload is usually stored at the end of the file, but when cooked, it may be separated into a separate file like .ubulk.

Content Virtualization

When Content Virtualization (Virtual Assets) is enabled, Bulk Data payloads are stored in a Package Trailer format at the end of the .uasset file, and those payloads can be moved (virtualized) to external backends (Perforce, DDC, etc.). Virtualized payloads are downloaded on-demand when needed.

Bulk Data Metadata

Each Bulk Data has metadata indicating the payload's position and size.

OffsetSizeFieldDescription
04BulkDataFlagsFlags
44 or 8ElementCountElement count
8 or 124 or 8SizeOnDiskSize on disk
12 or 208OffsetInFileOffset within file

The size of ElementCount and SizeOnDisk changes depending on the presence of the BULKDATA_Size64Bit flag.

Key Flags

FlagValueDescription
BULKDATA_PayloadAtEndOfFile0x0001Stored at end of file
BULKDATA_SerializeCompressedZLIB0x0002ZLIB compressed
BULKDATA_ForceInlinePayload0x0040Force inline storage
BULKDATA_Size64Bit0x2000Size fields are 64-bit

The actual payload byte sequence has different formats depending on the asset type, making generic parsing difficult.

Conclusion

This article explained the basic structure of uasset files.

  • UPackage and Outer Hierarchy: uasset is a serialized UPackage file that preserves the parent-child relationships (Outer) of UObjects
  • Section Structure: Files consist of multiple sections, with offsets to each section recorded in the Package File Summary (header)
  • Version Compatibility: FileVersionUE4/UE5 and CustomVersions handle format changes over many years
  • Name Map: Aggregates all strings within the package, eliminating duplicates by referencing via index from various locations
  • Import Map / Export Map: Import is references to external packages, Export is objects defined within the package. Uniformly referenced via FPackageIndex
  • Thumbnail: Thumbnail images are embedded directly in PNG/JPEG format and can be extracted and displayed
  • Bulk Data: Large binary data is separated for lazy loading, streaming, and memory mapping

Understanding this structure should deepen your understanding of how the engine's serialization processing works. If you've read this article, you should now have a good understanding of what the items displayed in the "playground" at the beginning of the article mean.

This was an attempt to explain UAsset as a file format. While there's information about struct serialization out there, information about uasset itself is surprisingly scarce, so I hope this proves useful.

There's enough content about Export internal serialization (Tagged Property, Native, Binary, etc.) to fill another article of similar length, so I'd like to write about it if the opportunity arises.