2397 字
12 分钟
游戏资产管线——从文件到显存

一、资产管线的五个阶段#

你在 UE 编辑器里拖入一个 character.fbx,然后在关卡里 spawn 了这个角色。这背后发生了什么?

FBX/PNG/WAV (源文件)
↓ [1] 导入阶段 (Import)
.uasset (引擎内部格式)
↓ [2] 编译阶段 (Compile)
Cooked Asset (平台优化格式)
↓ [3] 打包阶段 (Pak)
.pak / .ucas (归档文件)
↓ [4] 加载阶段 (Load)
内存中的 UObject
↓ [5] 驻留阶段 (Streaming/Unload)
GPU 显存中的纹理/网格体

核心设计原则:每一步的输出都是下一步的输入,每一步都可以离线(打包机上)完成大量计算,让运行时不背编译的锅。


二、导入(Import):从源格式到引擎内部格式#

1 源格式的多样性#

资产类型常见源格式引擎内部格式
静态网格体FBX, OBJ, glTF.uasset (UStaticMesh)
骨骼网格体FBX, glTF.uasset (USkeletalMesh)
纹理PNG, TGA, PSD, EXR, HDR.uasset (UTexture2D / UTextureCube)
音频WAV, OGG, MP3.uasset (USoundWave)
材质无(引擎内创建).uasset (UMaterial)

2 FBX Importer 做了什么#

FBX 是一个极其复杂的格式——它能表达动画、材质、灯光、相机、NURBS 曲面等大量游戏引擎不需要的东西。FBX Importer 的工作是提取有用信息,丢弃无关信息

FbxNode (树形结构)
├── Mesh → 提取顶点位置、法线、UV、三角面索引
├── Material → 提取贴图路径、基础颜色、粗糙度(如果可以映射)
├── Skeleton → 提取骨骼层次、绑定姿势(Bind Pose)
├── Animation → 提取关键帧、曲线数据
└── 相机/灯光/NURBS → 丢弃

从 FBX 提取的顶点数据被填入 UE 的内部结构:

struct FStaticMeshLOD {
FStaticMeshSectionArray Sections; // 材质分段
FPositionVertexBuffer PositionVertexBuffer; // 位置(压缩存储)
FStaticMeshVertexBuffer StaticMeshVertexBuffer; // UV/法线/切线
FColorVertexBuffer ColorVertexBuffer; // 顶点颜色
FRawStaticIndexBuffer IndexBuffer; // 索引
};

导入阶段的核心决策是”内部格式长什么样”——这决定了下游所有阶段的数据布局。如果自研引擎的顶点格式设计得不好,后面的压缩、流式加载、GPU 绑定全都会受影响。


三、编译(Compile):离线预计算#

1 为什么需要离线编译#

有些计算太重,运行时做会卡帧。离线编译就是提前算好、存下来、运行时直接用

编译阶段操作说明
法线/切线生成美术模型没有切线,Shader 需要——编译器自动算
LOD 生成自动减面,生成多个细节层级
动画压缩移除冗余关键帧,做量化压缩
碰撞体生成根据模型轮廓自动生成简化的碰撞网格
纹理 Mipmap 生成预先生成 1/2、1/4… 分辨率版本
Shader 编译HLSL → DXIL / SPIR-V 字节码
光照贴图烘焙静态光照预计算并存入纹理
导航网格生成自动计算可行走区域

2 纹理编译管线:一个完整案例#

PNG 纹理导入后的编译流程:

1. 读取 PNG → RGBA8 (CPU 内存)
2. 生成 Mipmap Chain
Level 0: 1024×1024
Level 1: 512×512
Level 2: 256×256
...
Level 10: 1×1
3. 平台编码
- PC: BC7 (Block Compressed, 4×4 宏块)
- iOS: ASTC (Adaptive Scalable Texture Compression)
- Android: ETC2
4. Streaming Mip 分离
Level 0-2: 打包为可选流式加载(4K 纹理太大,不在内存中常驻)
Level 3-10: 打包为常驻
5. 写入 .uasset

如果运行时实时做 Mipmap 生成和 BC7 编码,加载一张 4K 纹理需要几百毫秒——而离线编译后,运行时只是把预计算好的字节拷贝到显存。

3 Shader 编译#

这是引擎编译里最复杂的一环。你在材质编辑器里连的那些节点,最终要变成 GPU 能执行的指令:

Material Graph (可视化节点)
↓ 材质编译器
HLSL 源码
↓ FXC / DXC (DirectX Shader Compiler)
DXBC / DXIL 字节码
↓ 平台驱动
GPU ISA (实际硬件指令)

自研引擎里,Shader 编译大概有两种做法:

  1. 离线全部编译:在打包机上预编译所有 Shader 变体(各种材质质量 + 平台 + Feature Level 的组合),运行时直接加载字节码。优点是不卡帧,缺点是 Shader 变体爆炸(组合数可达数万)
  2. 运行时编译 + 缓存:游戏里第一次遇到新 Shader 变体时编译,结果缓存到磁盘。优点是包体小,缺点是首次遇到可能卡帧(“Shader compilation stutter”——UE 游戏也经常被吐槽这个)

四、Cook & Pak:打包与归档#

1 Cook 在做什么#

Cook 是平台特化转换:

  • 去掉不需要的平台数据。比如 Cook for iOS 时,桌面端专用的 DX12 Shader 变体全部丢弃。
  • 纹理压缩为平台原生格式。
  • 网格体的顶点数据序列化为平台最优字节序(Endianness)。
  • 去除编辑器专用数据(Undo History、Thumbnail 缩略图、编辑器属性的序列化数据)。

Cook 后的资产叫做 Cooked Asset,比 .uasset 更紧凑、更容易被运行时加载。

2 Pak 文件:把几千个文件打成一个大包#

从打包机的输出目录到最终的安装包,UE 会把所有文件打成 .pak 文件。本质上是一个带索引的归档文件,类似 ZIP 但不是压缩——解压不重要,随机访问速度才重要:

[Pak Header]
├── File Table (文件名 → 偏移量 + 大小)
└── Data Blocks
├── Content/Characters/Hero.uexp @ offset 0x0000000
├── Content/Maps/Level1.umap @ offset 0x05A0000
├── Content/Textures/Albedo.ubulk @ offset 0x0B00000
└── ...

运行时加载一个资产时,引擎先在 File Table 里查到这个文件在 Pak 里的偏移量,再 seek + read。因为 Pak 文件在磁盘上是连续的,机械硬盘年代这个设计能大幅减少寻道时间。SSD 普及后这个优势减弱,但仍然方便的是一体化分发(更新时发一个新的 Pak 文件就行)。

UE5 引入了 .ucas/.utoc(Unreal Containers)作为改进的归档格式,支持更高效的流式加载和 Patch。


五、运行时加载(Async Loading)#

1 同步加载的问题#

UStaticMesh* mesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Game/Hero"));
// 如果这个文件 50MB,这行代码会卡住主线程几百毫秒

不能这样做。UE 的解决方案是异步加载(Async Loading)

// 发一个异步请求
FStreamableManager& Streamable = UAssetManager::GetStreamableManager();
Streamable.RequestAsyncLoad(AssetPath,
FStreamableDelegate::CreateLambda([]() {
// 加载完成回调——在主线程执行
UStaticMesh* mesh = Cast<UStaticMesh>(LoadedAsset);
SpawnActor(mesh);
})
);

2 异步加载内部#

RequestAsyncLoad(FString)
↓ 加入加载队列
FAsyncLoadingThread (独立线程)
↓ 读 Pak 文件
↓ 反序列化 → UObject
FStreamableDelegate (回调)
↓ 回到主线程
Spawn 到关卡

后台线程完成磁盘 I/O 和反序列化,主线程只负责最后的 “注册到 World” 步骤。这样即使加载 200MB 的大关卡,主线程也不会卡顿超过一帧。

3 资产引用与生命周期#

UE 的异步加载系统依赖 GC 来管理加载进来的对象。加载进来的 UStaticMesh 会被 FStreamableManager 持有一个引用(FStreamableHandle)。当你释放这个 Handle 时,如果 Mesh 没有被其他地方引用,GC 会回收它。

这就是为什么关卡切换时内存被释放——旧关卡的 UWorld 被 GC 回收,旧关卡引用的所有资产(如果没有被其他关卡共享)也跟着被回收。


六、glTF:现代资产交换格式#

1 glTF 的设计哲学#

glTF(GL Transmission Format)定位是”3D 界的 JPEG”。它的核心理念:运行时可以直接使用,不需要离线编译器

传统管线:FBX → 编译器(数秒/数分钟)→ 引擎格式
glTF管线:GLB 文件 → 直接加载到 GPU

glTF 使用 JSON 描述场景结构,二进制数据(顶点、纹理)放在独立的 .bin 文件或嵌入 Base64:

model.glb (或 .gltf + .bin + 贴图文件)
├── scene (JSON)
│ ├── nodes (层次结构)
│ ├── meshes (网格体引用)
│ ├── materials (PBR 材质参数)
│ ├── skins (骨骼蒙皮数据)
│ └── animations (关键帧动画)
├── buffer.bin (顶点、索引、蒙皮权重——直接可上 GPU)
└── texture.png (BC7/ASTC 预压缩或原始 PNG)

2 为什么未来可能是 glTF 的#

  • PBR 材质标准内置——Albedo / Metallic-Roughness / Normal / Occlusion / Emissive,和现代引擎渲染管线天然对齐
  • 二进制数据 GPU-ready——顶点布局(Position, Normal, UV, Tangent)是定好的
  • 可扩展——extensions 机制允许自定义行为(KHR 官方扩展 + 引擎自定义扩展)
  • 生态成熟——Blender、Maya、Substance 全支持导出 glTF

如果你的自研引擎要从零开始选一个资产格式,glTF 比 FBX 更合适。FBX 是个封闭的黑箱(Autodesk 的私有格式),而 glTF 是开放的、文档完整的、运行时表现明确的。


七、总结#

阶段在哪做做什么为什么不是运行时
导入编辑器 / 命令行FBX → 引擎内部格式需要解析复杂格式,提取有用数据
编译离线Shader 编译、纹理压缩、LOD 生成计算量巨大,运行时做会卡帧
Cook打包机平台特化转换、去除编辑器数据包体更小、加载更快
加载运行时(异步)读文件 → 反序列化 → 注册 UObject异步避免主线程阻塞
Streaming运行时(持续)按需加载/卸载纹理 Mip、世界分块不常驻内存,降低显存压力

资产管线是引擎唯一一条”从美术的创意到玩家的屏幕”的通道。它的设计决定了内容迭代速度(导入快不快)、包体大小(压缩好不好)、运行时性能(加载会不会卡)。自研引擎里这条管线通常是最先需要搭建的基础设施之一。

游戏资产管线——从文件到显存
https://www.m4doka.xyz/posts/engine/engine-5-asset-pipeline/
作者
m4doka
发布于
2026-07-10
许可协议
CC BY-NC-SA 4.0