みんなで読もう uasset
みなさん、.uasset 読んでますか?
これは、皆さんにもっと UAsset と仲良くなっていただくための記事です。長い記事なので、まずはちょっと遊びから。お手元のプロジェクトの Content フォルダを開き、 .uasset ファイルを以下のエリアに Drag & Drop してみてください(完全にローカルのブラウザ上で処理されるので、どこかにアップロードされたりする心配はありません)。
.uasset ファイルをドロップ、またはクリックして選択
...楽しんでいただけたでしょうか。この記事 (とこの記事で紹介しているエンジンの実装)を理解すると、こういった遊びができるようになるかもしれません。
.uasset おさらい
.uasset は Unreal Engine のアセットファイル形式です。Blueprint、Material、StaticMesh など、エディタ上でインポート・作成・編集するほぼすべてのアセットはこの形式で保存されています。
普段の開発ではエディタがすべてを隠蔽してくれるため、中身を意識することはありません。uasset というと UE 独自の謎めいたファイルに思えるかもしれませんが、PNG や MP4 のような一般的なファイル形式と同様に、きちんと構造化されたデータを持っています。仕様を知れば読み解くことができるというわけです。
検証環境
- Unreal Engine 5.3 ~ 5.7
本記事では uncooked(エディタで編集可能な状態)の uasset を扱います。パッケージング後の cooked アセットはプロパティのシリアライズ方式が異なったり、エディタ専用データが除去されていたりと、構造が大きく変わります。.uexp との分離や IoStore 形式についても触れません。
なぜ uasset フォーマットを知ると便利なのか
uasset の中身なんて知っていて意味があるのか?と思うかもしれません。確かに普段の開発で意識することはほとんどありませんが、知っていると役立つ場面があります。
- アセットの解析
- 大量のアセットから特定の情報を抽出したい、依存関係を調べたいといった場面で、フォーマットを理解していれば自作ツールで対応できる
- 互換性問題の調査
- エンジンバージョン間でアセットが読み込めない問題に遭遇したとき、ヘッダのバージョン情報を見れば原因の切り分けができる
- パッケージングの理解
- クック時やパッケージング時に何が含まれるか、なぜ特定のアセットが含まれてしまうのかは、アセット間の依存関係がどう記録されているかを理解すると見通しがよくなる
- UE 外での簡易読み取り
- エディタを起動せずにアセットの基本情報(クラス、依存先など)を確認したいとき
本記事では、uasset ファイルの基本的な構造について解説します。エンジンソースコードの PackageFileSummary.h や LinkerLoad.cpp を参照しながら、全体像を掴んでいきましょう。
UAsset の基礎
フォーマットの詳細に入る前に、uasset が「何を」「どのような形で」保存しているのかを理解しておく必要があります。このセクションでは、エンジン内部の概念を整理します。
UPackage とは
Unreal Engine では、アセットは パッケージ(Package)という単位で管理されます。1つの .uasset ファイルは、1つのパッケージに対応しています。
パッケージはエンジン内部では UPackage クラスとして表現されます。UPackage 自体も UObject を継承しており、他のオブジェクトと同じ仕組みで扱われます。
UCLASS(MinimalAPI, Config=Engine)
class UPackage : public UObject
{
// パッケージフラグ
std::atomic<uint32> PackageFlagsPrivate;
// ロード元のパス情報
FPackagePath LoadedPath;
// ...
};
Outer 階層
パッケージの中には複数の UObject が含まれます。これらのオブジェクトは Outer という親子関係で階層構造を形成しており、最上位の Outer は必ず UPackage になります。
たとえば Blueprint アセットの場合、以下のような階層になっています。
UPackage "/Game/MyBlueprint"
└─ UBlueprint "MyBlueprint"
└─ UBlueprintGeneratedClass "MyBlueprint_C"
└─ UFunction "ExecuteUbergraph_MyBlueprint"
この階層関係が、ファイルフォーマットでは OuterIndex というフィールドで表現されます。
パッケージに含まれる情報
UE にインポートするとあらゆるファイルが uasset になります。PNG も FBX も WAV も、すべて uasset。一体中身はどうなっているのか気になるところです。
Blueprint ならグラフ情報、Texture なら画像データ、StaticMesh ならジオメトリデータと、アセットタイプによって内容は異なりますが、実のところこれらはすべて共通のデータ構造で表現されます。
Name Map
すべての基盤となるのが Name Map です。パッケージ内で使用されるすべての FName(クラス名、プロパティ名、オブジェクト名など)が格納され、各所からはインデックスで参照されます。
Export
このパッケージ内で定義されているオブジェクトです。各エクスポートは「どのクラスのインスタンスか」「親オブジェクトは何か」「シリアライズデータはどこにあるか」といった情報を持ちます。Blueprint なら BP クラス本体や関数オブジェクト、Texture ならテクスチャオブジェクトがエクスポートとして記録されます。
Import
外部パッケージへの参照です。継承元のクラス、参照しているマテリアル、使用している他のアセットなど、このパッケージが依存しているものがインポートとして記録されます。
Bulk Data
テクスチャのピクセルデータ、オーディオの波形データなど、大きなバイナリデータです。エクスポートのシリアライズデータとは別に管理され、ファイル末尾や別ファイル(.ubulk)に格納されることがあります。
その他のメタデータ
これらに加えて、エクスポート間の依存関係(Depends Map)、アセットレジストリ用の情報、サムネイル画像なども含まれます。
具体例:Blueprint アセットの場合
UE5 の ThirdPerson テンプレートに含まれる BP_ThirdPersonCharacter.uasset を実際に解析すると、以下のようなデータが含まれています。
- Name Map: 281 エントリ
- Import: 87 エントリ
- Export: 42 エントリ
Name Map(抜粋):
[ 0] /Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter
[ 1] /Script/CoreUObject
[ 2] /Script/Engine
[ 33] BP_ThirdPersonCharacter
[ 34] BP_ThirdPersonCharacter_C
パッケージパス、依存パッケージ名、オブジェクト名、プロパティ名など、あらゆる文字列がここに格納されます。
Import(抜粋):
[ 40] /Script/CoreUObject.Class -> TP_ThirdPersonCharacter
[ 71] /Script/CoreUObject.Package -> /Script/Engine
[ 84] /Script/TP_ThirdPerson.TP_ThirdPersonCharacter -> Default__TP_ThirdPersonCharacter
Import 40 は継承元クラス TP_ThirdPersonCharacter への参照、Import 71 はエンジンパッケージへの参照です。
Export(抜粋):
[ 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 が Blueprint アセット本体、Export 2 が生成されたクラス、Export 3 が CDO(Class Default Object)です。EventGraph は Blueprint を outer として持ち、Outer 階層を形成しています。
ここで重要なのは、「BP_ThirdPersonCharacter」などの文字列がそのまま格納されているわけではないということです。実際のファイルでは Name Map へのインデックスとして記録されており、名前を得るには Name Map を参照する必要があります。
バージョン互換性
Unreal Engine は頻繁にアップデートされ、それに伴って uasset のフォーマットも変化します。新しいプロパティ型の追加、既存構造の拡張、シリアライズ方式の変更など、変更の種類はさまざまです。
公式には「エンジンのアップグレードは1バージョンずつ」と推奨されていますが、実際にエンジンコードを読むと、uasset は意外なほどバージョン互換性に配慮した仕組みになっています。
エンジンソースには VER_UE4_OLDEST_LOADABLE_PACKAGE = 214 という定義があり、これが読み込み可能な最古のバージョンです。UE4 のバージョン enum には 500 以上のエントリがあるため、214 は UE4 初期に相当します。つまり UE5.7 でも、実装上は UE4 初期のアセットをロードできるよう互換性が維持されています。
uasset はヘッダに詳細なバージョン情報を持っています。
- エンジンバージョン
- UE4 / UE5 それぞれのバージョン番号
- どのエンジンで保存されたかを特定できる
- カスタムバージョン
- エンジンの各サブシステム(アニメーション、マテリアル、物理など)が独自に持つバージョン番号
- GUID をキーとして管理され、サブシステムごとに独立して進化できる
- 50 以上のサブシステムがそれぞれ独自のバージョンを持つ
エンジンはアセットを読み込む際、これらのバージョン情報を確認し、必要に応じて古い形式から新しい形式への変換処理を行います。エンジンソースには if (Ar.CustomVer(...) >= SomeVersion) のようなバージョン分岐が大量に存在し、長年の互換性を維持しています。
ただし、uasset が読めることと、その中身が正しく動作することは別の話です。Blueprint のノードが廃止されていたり、マテリアルのパラメータが変更されていたり、アセットを使う側のコードが変わっていれば、ロードできても期待通りには動きません。「1バージョンずつ」の推奨は、こうした意味的な互換性を確認しながら進めるためのものでしょう。
UAsset のファイルフォーマット
ここまでで uasset が保持する情報を理解しました。では、それらは実際のファイルにどう格納されているのでしょうか。
uasset ファイルの全体構造
uasset ファイルは、以下のような構造を持っています。
各セクションの位置は固定ではなく、ヘッダ(Package File Summary)に格納されたオフセット値によって決定されます。これにより、バージョンアップでセクションが追加されても後方互換性を維持できる設計になっています。
本記事ではすべてのセクションを網羅的に解説することはしません(Unreal Engine の規模を考えると現実的ではありません)。その代わり、uasset を理解するうえで基盤となる Package File Summary、Name Map、Import Map、Export Map を中心に、Thumbnail と Bulk Data についても説明します。
Package File Summary(ヘッダ)
Package File Summary は、uasset ファイルの目次にあたる部分です。ファイルの先頭に位置し、パッケージ全体のメタデータ(バージョン情報、パッケージフラグなど)と、後続する各セクションへのオフセットを保持しています。
uasset を読み込む際は、まずこのヘッダを解析して全体の構造を把握し、必要なセクションにジャンプしてデータを読み取ります。エンジンでは FPackageFileSummary 構造体として定義されており、シリアライズ処理は PackageFileSummary.cpp にあります。
バイナリレイアウト
雰囲気を掴むために、実際のバイナリレイアウトを見てみましょう。条件付きのフィールドはバージョンやフラグによって有無が変わります。
| # | サイズ | フィールド | 説明 |
|---|---|---|---|
| 1 | 4 | Tag | マジックナンバー (0x9E2A83C1) |
| 2 | 4 | LegacyFileVersion | フォーマットバージョン (-9 が最新) |
| 3 | 4 | LegacyUE3Version | UE3 互換用 (条件: ≠-4) |
| 4 | 4 | FileVersionUE4 | UE4 バージョン番号 |
| 5 | 4 | FileVersionUE5 | UE5 バージョン番号 (条件: ≤-8) |
| 6 | 4 | FileVersionLicenseeUE | ライセンシーバージョン |
| 7 | 20 | SavedHash | パッケージのハッシュ (UE5.1+) |
| 8 | 4 | TotalHeaderSize | ヘッダ全体のサイズ |
| 9 | 可変 | CustomVersions | サブシステム別バージョン |
| 10 | 可変 | PackageName | パッケージ名 (非推奨) |
| 11 | 4 | PackageFlags | パッケージフラグ |
| 12 | 4+4 | NameCount, NameOffset | Name Map の情報 |
| 13 | 4+4 | ExportCount, ExportOffset | Export Map の情報 |
| 14 | 4+4 | ImportCount, ImportOffset | Import Map の情報 |
| ... | ... | ... | ... |
全フィールド一覧(44項目)
| # | サイズ | フィールド | 説明 | 条件 |
|---|---|---|---|---|
| 1 | 4 | Tag | マジックナンバー (0x9E2A83C1) | |
| 2 | 4 | LegacyFileVersion | フォーマットバージョン (-9 が最新) | |
| 3 | 4 | LegacyUE3Version | UE3 互換用 | ≠ -4 |
| 4 | 4 | FileVersionUE4 | UE4 バージョン番号 | |
| 5 | 4 | FileVersionUE5 | UE5 バージョン番号 | ≤ -8 |
| 6 | 4 | FileVersionLicenseeUE | ライセンシーバージョン | |
| 7 | 20 | SavedHash | パッケージのハッシュ | UE5.1+ |
| 8 | 4 | TotalHeaderSize | ヘッダ全体のサイズ | UE5.1+ |
| 9 | 可変 | CustomVersions | サブシステム別バージョン | ≤ -2 |
| 10 | 4 | TotalHeaderSize | ヘッダ全体のサイズ | UE5.1 未満 |
| 11 | 可変 | PackageName | パッケージ名 (非推奨) | |
| 12 | 4 | PackageFlags | パッケージフラグ | |
| 13 | 4+4 | NameCount, NameOffset | Name Map の件数とオフセット | |
| 14 | 4+4 | SoftObjectPathsCount, Offset | ソフトオブジェクトパス参照 | UE5.1+ |
| 15 | 可変 | LocalizationId | ローカライズ ID | UE4.14+ |
| 16 | 4+4 | GatherableTextDataCount, Offset | 収集可能テキストデータ | UE4.7+ |
| 17 | 4+4 | ExportCount, ExportOffset | Export Map の件数とオフセット | |
| 18 | 4+4 | ImportCount, ImportOffset | Import Map の件数とオフセット | |
| 19 | 4×4 | CellExport/ImportCount, Offset | Verse セル情報 | UE5.5+ |
| 20 | 4 | MetaDataOffset | メタデータのオフセット | UE5.4+ |
| 21 | 4 | DependsOffset | Depends Map のオフセット | |
| 22 | 4+4 | SoftPackageRefsCount, Offset | ソフトパッケージ参照 | UE4.13+ |
| 23 | 4 | SearchableNamesOffset | 検索可能名のオフセット | UE4.15+ |
| 24 | 4 | ThumbnailTableOffset | サムネイルテーブルのオフセット | |
| 25 | 4+4 | ImportTypeHierarchiesCount, Offset | Import 型階層情報 | UE5.7+ |
| 26 | 16 | Guid | パッケージ GUID | UE5.1 未満 |
| 27 | 16 | PersistentGuid | 永続 GUID | Editor |
| 28 | 4 | GenerationCount | 世代情報の件数 | |
| 29 | 可変 | Generations[] | 過去バージョンの情報 | |
| 30 | 可変 | SavedByEngineVersion | 保存時のエンジンバージョン | UE4.5+ |
| 31 | 可変 | CompatibleWithEngineVersion | 互換エンジンバージョン | UE4.12+ |
| 32 | 4 | CompressionFlags | 圧縮フラグ | |
| 33 | 可変 | CompressedChunks[] | 圧縮チャンク (常に空) | |
| 34 | 4 | PackageSource | パッケージソース識別子 | |
| 35 | 可変 | AdditionalPackagesToCook[] | 追加クック対象 (常に空) | |
| 36 | 4 | NumTextureAllocations | テクスチャ割当数 (常に0) | > -7 |
| 37 | 4 | AssetRegistryDataOffset | アセットレジストリデータのオフセット | |
| 38 | 8 | BulkDataStartOffset | バルクデータの開始オフセット | |
| 39 | 4 | WorldTileInfoDataOffset | ワールドタイル情報のオフセット | UE4.11+ |
| 40 | 可変 | ChunkIDs[] | ストリーミングチャンク ID | UE4.10+ |
| 41 | 4+4 | PreloadDependencyCount, Offset | プリロード依存関係 | UE4.18+ |
| 42 | 4 | NamesReferencedFromExportDataCount | Export データから参照される名前数 | UE5.0+ |
| 43 | 8 | PayloadTocOffset | ペイロード TOC のオフセット | UE5.0+ |
| 44 | 4 | DataResourceOffset | データリソースのオフセット | UE5.3+ |
CustomVersions の構造は「Count (i32) + (GUID 16bytes + Version i32) × Count」です。FString は「Length (i32) + UTF-8/UTF-16 文字列」です。
実際のパース処理では、バージョン値を見ながら条件分岐でフィールドを読み進めます。PackageFileSummary.cpp の operator<< を見ると、この泥臭さがよくわかります。UE3 形式のためのコードまで多数残っているのには驚かされます。
マジックナンバー (Tag)
マジックナンバーとは、ファイル形式を識別するためにファイル先頭に配置される固定のバイト列です。PNG の 89 50 4E 47、PDF の 25 50 44 46 など、多くのファイル形式で採用されており、プログラムがファイルを読み込む際に形式を判別するために使われます。
uasset ファイルの先頭 4 バイトはマジックナンバーとして以下の値が定義されています。
#define PACKAGE_FILE_TAG 0x9E2A83C1
#define PACKAGE_FILE_TAG_SWAPPED 0xC1832A9E // バイトオーダー逆転版
エンジンの FLinkerLoad::LoadPackageInternal では、まずこのマジックナンバーを読み取り、正規のパッケージファイルかどうかを判定します。PACKAGE_FILE_TAG_SWAPPED は異なるエンディアンで保存されたファイルを検出するために用意されています。
バージョン情報
概要で述べた通り、uasset はバージョン互換性を強く意識しています。ヘッダにはバージョン情報が含まれており、エンジンは読み込み時にこの値に基づいて処理を分岐させます。
| フィールド | 説明 |
|---|---|
| LegacyFileVersion | 負の整数。新しいほど小さい値(-4, -7, -8 等)。UE5 では -8 以下 |
| FileVersionUE4 | UE4 バージョン番号。UE5 アセットでも常にシリアライズされる |
| FileVersionUE5 | UE5 バージョン番号(1000 以上)。UE4 アセットでは 0 |
| CustomVersions | サブシステムごとのカスタムバージョン(GUID + バージョン番号の配列) |
FileVersionUE4 と FileVersionUE5 は FPackageFileVersion 構造体にまとめられており、どのエンジンバージョンで保存されたアセットかを判定できます。
UE5 アセットでも FileVersionUE4 がシリアライズされているのは、既存のバージョンチェックとの互換性のためです。エンジン全体に if (Version >= VER_UE4_XXX) という形式のチェックが多数存在しており、UE5 アセットではこのフィールドに UE4 の最終バージョン値が設定されます。こうすることで、すべての UE4 版チェックが自動的にパスし、UE5 固有の機能は FileVersionUE5 で判定できます。
struct FPackageFileVersion
{
// UE4 バージョン
int32 FileVersionUE4 = 0;
// UE5 バージョン (1000 以上が UE5)
int32 FileVersionUE5 = 0;
};
CustomVersions の具体的な使い方も見てみましょう。
// Editor サブシステムのカスタムバージョン
const FGuid FEditorObjectVersion::GUID(0xE4B068ED, 0xF49442E9, 0xA231DA0B, 0x2E46BB41);
FDevVersionRegistration GRegisterEditorObjectVersion(
FEditorObjectVersion::GUID,
FEditorObjectVersion::LatestVersion,
TEXT("Dev-Editor")
);
代表的な CustomVersion には以下のようなものがあります。
| CustomVersion | 用途 |
|---|---|
| FEditorObjectVersion | エディタ機能全般(FText、SplineComponent 等) |
| FReleaseObjectVersion | リリースビルド向けの変更 |
| FAnimationCustomVersion | アニメーション関連 |
| FSkeletalMeshCustomVersion | SkeletalMesh の構造変更 |
| FLandscapeCustomVersion | Landscape 関連 |
実際の使用例として、FText のシリアライズを見てみましょう。
Ar.UsingCustomVersion(FEditorObjectVersion::GUID);
if (Ar.CustomVer(FEditorObjectVersion::GUID) >= FEditorObjectVersion::TextFormatArgumentDataIsVariant)
{
// 新しい形式で読み書き
Record << SA_VALUE(TEXT("Type"), TypeAsByte);
}
Ar.CustomVer() でパッケージに記録された CustomVersion を取得し、特定のバージョン以上かどうかで処理を分岐させています。
パッケージフラグ
UPackage は様々なフラグを持っており、パッケージの性質を表しています。
| フラグ | 説明 |
|---|---|
| PKG_ContainsMap | ULevel/UWorld を含むパッケージ |
| PKG_Cooked | クック済み(パッケージング済み) |
| PKG_EditorOnly | エディタ専用パッケージ |
| PKG_CompiledIn | C++ コードから生成されたパッケージ(/Script/Engine 等) |
| PKG_NewlyCreated | 新規作成されたパッケージ(未保存) |
主要なオフセットフィールド
ヘッダには各セクションへのオフセットが格納されています。主要なものを以下に示します。
| フィールド | 説明 |
|---|---|
| TotalHeaderSize | ヘッダ全体のサイズ |
| NameCount / NameOffset | Name Map のエントリ数とオフセット |
| ImportCount / ImportOffset | Import Map のエントリ数とオフセット |
| ExportCount / ExportOffset | Export Map のエントリ数とオフセット |
| DependsOffset | Depends Map へのオフセット |
| SoftPackageReferencesCount / Offset | ソフトパッケージ参照の情報 |
| ThumbnailTableOffset | サムネイルテーブルへのオフセット |
| AssetRegistryDataOffset | アセットレジストリデータへのオフセット |
| BulkDataStartOffset | バルクデータ開始位置 |
エンジンはこれらのオフセットを使って、必要なセクションにジャンプして読み取りを行います。
バージョンによるフィールドの変遷
ここで注意が必要なのは、ヘッダのフィールドはバージョンによって増減するということです。エンジンソースでは EUnrealEngineObjectUE5Version として定数が定義されています。
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,
};
たとえば UE5 で追加されたフィールドには以下のようなものがあります。
| バージョン | 追加フィールド |
|---|---|
| 1008 (ADD_SOFTOBJECTPATH_LIST) | SoftObjectPathsCount, SoftObjectPathsOffset |
| 1014 (METADATA_SERIALIZATION_OFFSET) | MetaDataOffset |
| 1015 (VERSE_CELLS) | CellExportCount/Offset, CellImportCount/Offset |
| 1016 (PACKAGE_SAVED_HASH) | SavedHash(GUID から FIoHash への変更) |
エンジンはこれらのバージョン値を見ながら、フィールドの有無を判定してシリアライズ処理を分岐させています。こういったところを見ると、フォーマットの歴史がわかるだけでなく、これから追加されるであろう機能の名前なんかも覗き見ることができて少し面白いです。
Name Map
Name Map は、パッケージ内で使用されるすべての名前(クラス名、プロパティ名、オブジェクト名など)を集約したテーブルです。ファイル内の各所では、文字列を直接埋め込む代わりに Name Map へのインデックスを記録することで、重複を排除しファイルサイズを削減しています。
ファイル内 FName とランタイム FName の違い
ファイル内での FName は、ランタイムの FName とは本質的に異なります。
ランタイムの FName:
- エンジン全体で共有されるグローバル NameTable へのインデックス
- プロセス起動時に初期化され、
NAME_Noneは常にインデックス 0 - 文字列は一度だけ格納され、全オブジェクトで共有される
ファイル内の FName:
- パッケージごとに独立した NameMap へのインデックス
- 同じ文字列でもパッケージごとに異なるインデックスになる
- ファイルにはグローバルテーブルとは無関係なローカルインデックスが記録される
// FName の参照は 8 バイト
int32 NameIndex; // パッケージローカル NameMap へのインデックス
int32 Number; // インスタンス番号(同名オブジェクトの区別用)
Number フィールドは、同じ名前を持つ複数のオブジェクトを区別するために使われます。たとえば MyActor_0, MyActor_1 のような名前は、同じ Name Map エントリを参照しつつ、Number で区別されます(Number が 0 なら接尾辞なし、1 以上なら _0, _1, ... が付く)。
なお、Name Map のエントリ自体をシリアライズする FNameEntrySerialized とは別物です。FNameEntrySerialized は文字列本体とハッシュを含む構造体で、Name Map のパースに使います。
ロード時のインデックス変換
パッケージをロードする際、Linker は以下の処理でファイルインデックスをランタイムインデックスに変換します。
FNameEntrySerialized NameEntry(ENAME_LinkerConstructor);
for (int32 Idx = 0; Idx < NameCount; ++Idx)
{
*this << NameEntry;
// ファイルから読んだ文字列をグローバル FName として登録し、
// そのグローバルインデックスを NameMap に格納
NameMap.Emplace(FName(NameEntry).GetDisplayIndex());
}
この NameMap 配列がマッピングテーブルとして機能します。ファイルインデックス N の名前を解決するには NameMap[N] を参照し、そこに格納されたグローバル FNameEntryId を使ってグローバルテーブルから文字列を取得します。
Name Map のシリアライズ
Name Map は文字列配列としてシリアライズされています。
各エントリは長さプレフィックス付きの文字列(FString)としてシリアライズされます。UE4 の特定バージョン以降(VER_UE4_NAME_HASHES_SERIALIZED)では、ハッシュ値も一緒にシリアライズされます。
// 文字列本体
FString Name;
Ar << Name;
// ハッシュ値(新しいバージョンの場合)
if (Ar.UEVer() >= VER_UE4_NAME_HASHES_SERIALIZED)
{
uint16 DummyHashes[2];
Ar << DummyHashes[0] << DummyHashes[1];
}
重要な点として、Name Map の最初のエントリ(インデックス 0)が "None" であるとは限りません。ランタイムのグローバルテーブルでは NAME_None は常にインデックス 0 ですが、ファイル内の Name Map はパッケージごとに独立しているため、"None" の位置はパッケージによって異なります。
Import Map
Import Map は、このパッケージが依存している外部オブジェクトのリストです。継承元のクラス、参照しているマテリアル、使用しているテクスチャなど、他のパッケージに存在するオブジェクトへの参照がここに記録されます。
たとえば Blueprint アセットが Actor クラスを継承している場合、/Script/Engine.Actor への参照が Import として記録されます。
Import Map のバイナリレイアウト
Import Map は Summary.ImportOffset の位置から、Summary.ImportCount 個のエントリが連続して並びます。各エントリのレイアウトは以下の通りです。
| オフセット | サイズ | フィールド | 説明 |
|---|---|---|---|
| 0 | 8 | ClassPackage | クラスを含むパッケージ名(FName) |
| 8 | 8 | ClassName | クラス名(FName) |
| 16 | 4 | OuterIndex | Outer への参照(FPackageIndex) |
| 20 | 8 | ObjectName | オブジェクト名(FName) |
| 28 | 8 | PackageName | 所属パッケージ名(Editor のみ、UE4.18+) |
| 36 | 4 | bImportOptional | オプショナルフラグ(UE5.1+) |
FName は int32 NameIndex + int32 Number の 8 バイトです。FPackageIndex は int32 の 4 バイトで、負値は Import、正値は Export を指します。bool は意外かもしれませんが 4 バイト整数としてシリアライズされます。これは UE3 以前の UBOOL 型との互換性を維持するための歴史的な仕様です。
エントリサイズはバージョンによって変わるため、固定サイズとして扱えません。エンジンは各エントリを順番にシリアライズして読み込みます。
Import は階層構造を持つことがあります。たとえば /Script/Engine.Actor という参照は、以下のような Import エントリの連鎖で表現されます。
- Import 0: パッケージ
/Script/Engine(OuterIndex = 0) - Import 1: クラス
Actor(OuterIndex = Import 0 を参照)
この OuterIndex が FPackageIndex という型になっており、Import と Export の両方を参照できる仕組みになっています。
FPackageIndex
FPackageIndex は、Import Map または Export Map のエントリを指すインデックスです。int32 のラッパーで、値の正負によって参照先を区別します。
// FPackageIndex の値と参照先の関係
// 0 : null(どこも指さない)
// 負の値 : Import Map へのインデックス(-1 → Import[0], -2 → Import[1], ...)
// 正の値 : 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] は -1
int32 ToExport() const { return Index - 1; } // Export[0] は 1
この仕組みにより、オブジェクト参照が外部パッケージのものか、同一パッケージ内のものかを統一的に扱えます。シンプルですが、よくできた設計ですね。
Export Map
Export Map は、このパッケージ内で定義されているオブジェクトのリストです。Import が「外部への参照」であるのに対し、Export は「このファイルが提供するもの」です。
Blueprint アセットであれば、Blueprint クラス本体、デフォルトオブジェクト、関数グラフなどが Export として記録されます。
Export Map のバイナリレイアウト
Export Map は Summary.ExportOffset の位置から、Summary.ExportCount 個のエントリが連続して並びます。Import Map よりフィールドが多く、バージョンによる差異も大きいです。
| オフセット | サイズ | フィールド | 説明 |
|---|---|---|---|
| 0 | 4 | ClassIndex | クラスへの参照(FPackageIndex) |
| 4 | 4 | SuperIndex | スーパークラスへの参照(UStruct のみ) |
| 8 | 4 | TemplateIndex | テンプレートへの参照(UE4.14+) |
| 12 | 4 | OuterIndex | Outer への参照 |
| 16 | 8 | ObjectName | オブジェクト名(FName) |
| 24 | 4 | ObjectFlags | オブジェクトフラグ |
| 28 | 8 | SerialSize | シリアライズデータのサイズ |
| 36 | 8 | SerialOffset | シリアライズデータの位置 |
| 44 | 4 | bForcedExport | 強制エクスポートフラグ |
| 48 | 4 | bNotForClient | クライアント除外フラグ |
| 52 | 4 | bNotForServer | サーバー除外フラグ |
| 56 | 4 | PackageFlags | パッケージフラグ |
| 60 | 4 | bNotAlwaysLoadedForEditorGame | Editor ゲーム非ロードフラグ |
| 64 | 4 | bIsAsset | アセットフラグ |
| ... | ... | ... | (依存関係情報等、UE4.17+ で追加) |
SerialSize と SerialOffset は UE4.14 以前では 4 バイト(int32)でした。Export Map はオブジェクトのメタデータのみを格納し、実際のプロパティデータは SerialOffset が指す位置に別途格納されています。
bIsAsset フラグ
FObjectExport には bIsAsset というフラグがあり、そのエクスポートが Content Browser に表示されるオブジェクト(Blueprint なら BP 本体、Material ならマテリアル本体)かどうかを示します。
for (const FObjectExport& Export : ExportMap)
{
if (Export.bIsAsset)
{
// Content Browser に表示されるオブジェクト
}
}
OuterIndex が null(0)のエクスポートは「UPackage 直下のオブジェクト」を意味しますが、Blueprint パッケージのように複数のトップレベルオブジェクト(Blueprint 本体、BlueprintGeneratedClass、CDO など)が存在することがあります。bIsAsset はその中から Content Browser に表示される代表的なオブジェクトを識別するためのフラグです。
シリアライズデータへのアクセス
各 Export は SerialOffset と SerialSize によって、実際のオブジェクトデータの位置とサイズを示しています。この範囲のバイト列を読み取ることで、オブジェクトのプロパティや内部データにアクセスできます。
ただし、このバイト列の解釈にはオブジェクトの型に応じた処理が必要です。ここから先はかなり複雑になるので、本記事では扱いません。
クラスパスの解決
さて、ここまでで Import Map、Export Map、Name Map の構造を見てきました。これらを組み合わせることで、オブジェクトのクラスパスを解決できます。
たとえば、あるエクスポートの ClassIndex が Import[3] を指していて、Import の構造が以下のような場合:
Import[2]: ClassName="Package", ObjectName="/Script/Engine", OuterIndex=0
Import[3]: ClassName="Class", ObjectName="Actor", OuterIndex=-3 (→Import[2])
この場合、クラスパスは /Script/Engine.Actor となります。OuterIndex を再帰的に辿り、Name Map から名前を解決して連結することで、完全なパスを構築できます。
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
Content Browser に表示されるサムネイル画像は、uasset ファイル内に埋め込まれています。
実はこのサムネイル、エディタ上でアセットを描画した結果をそのまま PNG や JPEG として保存しています。GPU 向けの特殊なエンコーディングではなく一般的な画像フォーマットなので、uasset から該当部分を切り出せばそのまま表示できます。
複数のサムネイル
サムネイルはパッケージ内の各エクスポートオブジェクトに対して保存できる仕組みになっています。通常のアセット(Blueprint、Material 等)ではメインオブジェクトの 1 枚だけですが、フォーマット上は複数のサムネイルを格納可能です。
Thumbnail の格納位置
ThumbnailTableOffset が指す位置にサムネイルの目次(インデックス)があり、その前に各サムネイルの画像データが格納されています。
Thumbnail Table の構造
Thumbnail Table は目次と画像データの 2 つの部分からなります。
[サムネイル画像データ 0]
[サムネイル画像データ 1]
...
[ThumbnailTableOffset が指す位置]
├─ Count (int32)
├─ Entry 0: ObjectClass, ObjectPath, FileOffset
├─ Entry 1: ObjectClass, ObjectPath, FileOffset
└─ ...
目次の各エントリは以下の構造を持ちます。
| オフセット | サイズ | フィールド | 説明 |
|---|---|---|---|
| 0 | 可変 | ObjectClass | オブジェクトのクラス名(FString) |
| 可変 | 可変 | ObjectPath | オブジェクトのパス(FString) |
| 可変 | 4 | FileOffset | 画像データのファイル内オフセット |
サムネイル画像データ
各サムネイルの画像データは FObjectThumbnail としてシリアライズされます。
| オフセット | サイズ | フィールド | 説明 |
|---|---|---|---|
| 0 | 4 | ImageWidth | 画像の幅 |
| 4 | 4 | ImageHeight | 画像の高さ(負値なら JPEG 圧縮) |
| 8 | 4 | DataLength | 圧縮データの長さ |
| 12 | 可変 | CompressedData | PNG または JPEG 圧縮された画像データ |
ImageHeight が負の値の場合は JPEG 圧縮、正の値の場合は PNG 圧縮を示します。実際の高さは絶対値を取ります。
ファイルに保存されるのは CompressedImageData(圧縮済み画像)のみで、展開後のピクセルデータ ImageData はディスクには書き出されません。CompressedImageData は PNG/JPEG としてそのままデコード可能なバイト列です。
Bulk Data
テクスチャのミップマップやオーディオの波形など、アセットによっては大量のバイナリデータを持つことがあります。こうしたデータは Bulk Data という仕組みで管理されます。
なぜ Bulk Data を分けるのか
通常のプロパティシリアライズでは、オブジェクトを読み込む際にすべてのデータをメモリに展開します。しかし、テクスチャのピクセルデータのような巨大なバイナリを毎回すべて読み込むのは非効率です。
Bulk Data はこの問題を解決するため、ペイロード(実データ)を分離して管理します。
- 遅延読み込み: Summary や Export Map だけ先に読み込み、ペイロードは必要になるまで読み込まない
- ストリーミング: 非同期 IO で必要な部分だけを読み込む(テクスチャストリーミング等)
- メモリマッピング: ファイルを直接メモリにマップし、OS のページング機構を活用
エクスポートと Bulk Data の関係
エクスポートのシリアライズ処理中、アセット固有のコードが FBulkData::Serialize を呼び出すことで Bulk Data が書き込まれます。例えば UTexture2D では、各ミップマップの FBulkData がシリアライズされます。
このとき、Bulk Data のペイロードをどこに格納するかは 2 種類あります。
| 格納位置 | 説明 |
|---|---|
| インライン | エクスポートのシリアライズ中、その場にペイロードを書き込む |
| ファイル末尾 | シリアライズ中はメタデータのみ書き込み、ペイロードは BulkDataStartOffset 以降にまとめて格納 |
インラインの場合、ペイロードはエクスポートデータの一部として Export Data 領域内に存在します。ファイル末尾の場合、Export Data 内にはメタデータ(フラグ、サイズ、オフセット)だけが記録され、実データは別の領域に格納されます。
また、Uncooked Asset(Editor Asset)では通常ファイル末尾に格納されますが、クックされると .ubulk 等の別ファイルに分離されることがあります。
Content Virtualization
Content Virtualization(Virtual Assets)を有効にしている場合、Bulk Data のペイロードは Package Trailer という形式で .uasset ファイル末尾に格納され、さらにそのペイロードを外部バックエンド(Perforce、DDC 等)に移動(仮想化)できます。仮想化されたペイロードは必要時にオンデマンドでダウンロードされる仕組みです。
Bulk Data のメタデータ
各 Bulk Data はメタデータを持ち、ペイロードの位置とサイズを示します。
| オフセット | サイズ | フィールド | 説明 |
|---|---|---|---|
| 0 | 4 | BulkDataFlags | フラグ |
| 4 | 4 or 8 | ElementCount | 要素数 |
| 8 or 12 | 4 or 8 | SizeOnDisk | ディスク上のサイズ |
| 12 or 20 | 8 | OffsetInFile | ファイル内オフセット |
BULKDATA_Size64Bit フラグの有無で ElementCount と SizeOnDisk のサイズが変わります。
主要なフラグ
| フラグ | 値 | 説明 |
|---|---|---|
BULKDATA_PayloadAtEndOfFile | 0x0001 | ファイル末尾に格納 |
BULKDATA_SerializeCompressedZLIB | 0x0002 | ZLIB 圧縮 |
BULKDATA_ForceInlinePayload | 0x0040 | インライン格納を強制 |
BULKDATA_Size64Bit | 0x2000 | サイズフィールドが 64bit |
ペイロードの実際のバイト列はアセット種別ごとに異なるフォーマットを持つため、汎用的なパースは困難です。
まとめ
本記事では、uasset ファイルの基本構造について解説しました。
- UPackage と Outer 階層: uasset は UPackage をシリアライズしたファイルで、UObject の親子関係(Outer)を保持している
- セクション構造: ファイルは複数のセクションから構成され、Package File Summary(ヘッダ)に各セクションへのオフセットが記録されている
- バージョン互換性: FileVersionUE4/UE5 や CustomVersions により、長年のフォーマット変更に対応している
- Name Map: パッケージ内の全文字列を集約し、各所からインデックスで参照することで重複を排除
- Import Map / Export Map: Import は外部パッケージへの参照、Export はパッケージ内で定義されるオブジェクト。FPackageIndex で統一的に参照
- Thumbnail: サムネイル画像は PNG/JPEG 形式でそのまま埋め込まれており、切り出して表示可能
- Bulk Data: 大きなバイナリデータは遅延読み込み・ストリーミング・メモリマッピングのために分離管理される
この構造を理解していれば、エンジンのシリアライズ処理がどのように動作しているかの理解が深まるはずです。記事冒頭の「遊び」で表示されていた項目についても、この記事をお読みになった方ならきっと意味がよくわかるようになっていることでしょう。
今回は UAsset を一つのファイルフォーマットとして解説する試みをしてみました。構造体のシリアライズなどについては情報がありますが、意外と uasset そのものの情報は少ないので、何かのお役に立てれば幸いです。 また、Export 内部のシリアライゼーション(Tagged Property、Native、Binary など)については、この記事と同じくらいの分量で語れる内容がありますので、機会があれば記事を書きたいと思います。