一、资产管线的五个阶段
你在 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×13. 平台编码 - PC: BC7 (Block Compressed, 4×4 宏块) - iOS: ASTC (Adaptive Scalable Texture Compression) - Android: ETC24. 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 编译大概有两种做法:
- 离线全部编译:在打包机上预编译所有 Shader 变体(各种材质质量 + 平台 + Feature Level 的组合),运行时直接加载字节码。优点是不卡帧,缺点是 Shader 变体爆炸(组合数可达数万)
- 运行时编译 + 缓存:游戏里第一次遇到新 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 文件 ↓ 反序列化 → UObjectFStreamableDelegate (回调) ↓ 回到主线程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 文件 → 直接加载到 GPUglTF 使用 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、世界分块 | 不常驻内存,降低显存压力 |
资产管线是引擎唯一一条”从美术的创意到玩家的屏幕”的通道。它的设计决定了内容迭代速度(导入快不快)、包体大小(压缩好不好)、运行时性能(加载会不会卡)。自研引擎里这条管线通常是最先需要搭建的基础设施之一。