news 2026/8/18 1:27:11

一次读懂 UEViewer:从 .pak 字节流到模型贴图的完整资源提取链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
一次读懂 UEViewer:从 .pak 字节流到模型贴图的完整资源提取链路

一次读懂 UEViewer:从 .pak 字节流到模型贴图的完整资源提取链路

【免费下载链接】UEViewerViewer and exporter for Unreal Engine 1-4 assets (UE Viewer).项目地址: https://gitcode.com/gh_mirrors/ue/UEViewer

UEViewer(社区俗称 umodel)是一款面向虚幻引擎 1–4 全系列游戏资源的查看与提取工具,它能从.pak.upk.uasset这些二进制包里还原出模型、贴图、动画与材质。想象这样一个场景:你双击 UEViewer 主程序,选定游戏目录,展开包文件列表,右键一个资源点下"导出"——几秒钟后,磁盘上多出一个 PSK 模型和一张 TGA 贴图。这篇文章要回答的问题是:这几秒钟里,程序内部到底发生了什么?我们把这次导出当成一场"从字节到资产"的旅行,沿着数据流经的每一站,看看 UEViewer 如何一步步把 0 和 1 翻译成你能用的文件。

先给这场旅行画一张地图

一次导出可以被拆成四个连续的驿站,每一站只负责一件事,把上一站的产物交给下一站:

四个驿站各司其职,各站之间只通过"文件路径"和"内存对象"这两种简单的中间产物通信。这份地图很关键,因为后面每一次"为什么这么设计"的讨论,都可以回到这张图上找到落点。

第一站:先把"压缩包"伪装成"普通文件夹"

它解决什么问题?游戏资源通常被打进.pak.upk包里,一个包可能塞了上千个文件。如果上层解析逻辑必须自己处理"文件在包内第几个字节、是否压缩、是否加密",那么所有模块都会写满压缩相关的脏代码。

它是怎么设计的?Unreal/FileSystem/目录下的GameFileSystemFVirtualFileSystem提供了统一抽象。包内每个文件都会被登记成一条CGameFileInfo记录,字段里既保存文件名、大小,也保存它在包内的索引(IndexInVfs)。为了快速查找,每条记录还带一个基于文件名计算出的哈希指针(HashNext),于是"按名字找文件"变成一次 O(1) 的哈希命中,而不是遍历整个包目录。

底层解包工作在Unreal/FileSystem/UnArchivePak.cpp中完成。它先检查包头的魔数0x5A6F12E1,再按FPakInfo里的版本号决定如何解释后续字段——因为从 UE4.13 到 4.25,pak 格式本身也一直在演化:

pak 版本演进新增内容对应的解析策略
早期版本只有偏移、大小、压缩方式基础字段直接读
FNameBasedCompressionMethod压缩算法从数字改为名字读出 4 组 32 字节名称再映射
IndexEncryption索引区可能被加密读入bEncryptedIndex标志
EncryptionKeyGuid引入密钥 GUID读到 GUID 后去匹配用户提供的 AES 密钥

为什么这样优于其他方案?一个很能说明问题的细节是MAX_OPEN_PAKS = 32:程序刻意限制同时打开的 pak 文件句柄数量。注释里写明原因——C 运行时库对进程可打开的文件数有 2048 的上限,而一个游戏可能有几十上百个 pak。用一个小型句柄缓存,既绕开了操作系统限制,又避免频繁开关文件的开销。这是典型的"用工程经验换健壮性"的取舍。

第二站:验明正身——这是哪一代引擎、哪一款游戏?

它解决什么问题?同一个结构体,在《真人快打 X》里字段是 64 位的,在《子弹风暴》里多了一个来历不明的字段,到了《火箭联盟》又换了字节序。如果不先搞清"我在跟谁说话",后续一切解析都是空谈。

它是怎么设计的?解析入口是Unreal/UnrealPackage/UnPackage.h里的FPackageFileSummary。第一步是核对标签,标准包文件以PACKAGE_FILE_TAG = 0x9E2A83C1开头。但不少厂商魔改了标签,于是代码里维护了一张"特殊标签表":

#define PACKAGE_FILE_TAG 0x9E2A83C1 // 标准 Unreal 包 // 部分游戏自定义的标签 // 0x9E2A83C2 -> Killing Floor // 0x7E4A8BCA -> iStorm // 0xEA31928C -> Hawken // 0x12345678 -> TaoYuan // 0xEC201133 -> StormWar(后面还跟了一个长度字段,需要跳过)

如果读到0xC1832A9E(标签的字节反转),则说明文件是小端/大端互反的,程序会翻转自己的"读字节序开关"(Ar.ReverseBytes = true)再继续。这比逐字段判断要省事得多。

确定标签后,用版本号把引擎划分成几代:

#define PACKAGE_V2 100 // UE2 起的分界 #define PACKAGE_V3 180 // UE3 起的分界

UE4 的情况更特殊:它的包版本是负数,且从某个版本起可能出现"无版本号包"(unversioned package)。此时版本信息退化成一组自定义版本容器FCustomVersionContainer,程序无法自行推断,只能回调UE4UnversionedPackage()向用户弹窗询问——这正是很多 UE4 游戏首次打开时会跳出一个版本对话框的原因。

为什么这样优于其他方案?看一个代表性特例——压缩块结构FCompressedChunk的序列化代码:

friend FArchive& operator<<(FArchive &Ar, FCompressedChunk &C) { #if MKVSDC || ROCKET_LEAGUE if ((Ar.Game == GAME_MK && Ar.ArVer >= 677) || (Ar.Game == GAME_RocketLeague && Ar.ArLicenseeVer >= 22)) { // MK X 与 Rocket League 使用 64 位文件偏移 int64 UncompressedOffset64, CompressedOffset64; Ar << UncompressedOffset64 << C.UncompressedSize << CompressedOffset64 << C.CompressedSize; ... } #endif #if BULLETSTORM if (Ar.Game == GAME_Bulletstorm && Ar.ArLicenseeVer >= 21) { int32 unk10; // 未知字段,可能是 0 或 1 Ar << unk10; } #endif }

注意这些特例全部被#if宏包裹(MKVSDCROCKET_LEAGUEBULLETSTORM),宏来自Unreal/GameDefines.h。这套"一个游戏一个宏"的设计,让每种特例都能独立开关:编译时不需要的游戏支持会被整个剔除,体积和解析负担都随之下降,而新增一款游戏的补丁也只需改动个别文件,不会污染主流程。

第三站:会报出版本号的"尺子"——FArchive

它解决什么问题?引擎里几乎每一个对象(贴图、骨骼、动画曲线、材质节点)都是"按固定顺序写入的字段序列"。需要一个统一的读写游标,让几千处对象解析代码共享同一套行为。

它是怎么设计的?答案在Unreal/UnCore.h的抽象基类FArchive里:

class FArchive { public: int ArVer; // 引擎版本号 int ArLicenseeVer; // 厂商私有版本号 bool IsLoading; // 当前是读还是写 bool ReverseBytes; // 是否需要字节序反转 virtual void Seek(int Pos) = 0; virtual void Serialize(void *data, int size) = 0; void DetectGame(); // 根据版本号与内容推断游戏 int Engine() const { return (Game & GAME_ENGINE); } };

可以把它想象成一把会主动报出自己"出厂版本"的尺子:它不只会移动游标和读写字节,还随身携带ArVerArLicenseeVerGame三个"身份牌"。任何一处结构体解析在拿不准时,都可以低头问一句"现在是在哪一代引擎、哪款游戏的环境下",然后决定按哪种布局读取。包内的FObjectExport表则记录每个导出对象的类索引、名字、以及SerialOffset/SerialSize——即它在文件里的精确位置和长度,对象模型层据此"按图索骥"地跳转读取。

为什么这样优于其他方案?一种朴素的做法是"每代引擎写一套完整解析器",但那样代码量会爆炸且难以维护。UEViewer 的选择是一份代码、多处版本分支:把版本判断下沉到序列化函数内部,用Ar.GameArLicenseeVer做细粒度分流。另外还有一处值得注意的取舍:UnCore.h里定义了USE_COMPACT_PACKAGE_STRUCTS宏,默认开启后,FObjectExport中那些"框架用不到"的字段(如SuperIndexObjectFlags)会被直接跳过不读。代价是丢掉一部分调试信息,收益是解析更快、内存更省——这是典型的"为实用放弃完整"的工程决策。

第四站:导出器的"点名册"与"查重表"

它解决什么问题?还原出来的对象种类繁多:骨骼网格、静态网格、材质、纹理、声音、动画集……导出模块需要一种低成本的方式,把对象类型分发到对应的导出函数,同时保证同一资源不被重复写出。

它是怎么设计的?Exporters/Exporters.h暴露了一个极简的注册接口:

typedef void (*ExporterFunc_t)(const UObject*); void RegisterExporter(const char* ClassName, ExporterFunc_t Func); // 模板包装,免去手写类型转换 template<class T> FORCEINLINE void RegisterExporter(void (*Func)(const T*)) { RegisterExporter(T::StaticGetTypeinfo()->Name + 1, (ExporterFunc_t)Func); }

Exporters/Exporters.cpp里维护了一个容量为MAX_EXPORTERS(20)的静态数组。每个导出实现文件在初始化时调用一次注册即可,例如Exporters/ExportPsk.cpp注册 PSK 导出、Exporters/ExportTexture.cpp注册纹理导出。新增一种格式,就是"新建一个文件 + 追加一行注册",分发逻辑一行都不用改。

更妙的是去重机制。ExportContext用"包指针 + 导出索引"作为键,构建了一张 4096 桶的哈希表(ItemExists/AddItem)。为什么必须去重?代码注释里有个震撼的数字:在某个测试案例中,580 张纹理被不同材质引用了 18000 次——UE3/UE4 里一张贴图被成百上千个材质共享是常态。如果不查重,一次批量导出会重复解压同一张贴图几千次。更绝的是,OnObjectLoad回调会在纹理导出完成后把它替换成占位对象,后续加载直接跳过真实数据,省下的不仅是磁盘,还有海量解压时间。

从"会用"到"能改":三条上手路径

理解了四站链路后,你会发现 UEViewer 的扩展点被刻意做得很浅。按照你的角色,有三条对应的上手路径:

你的角色目标该看哪里最小改动
快速使用者编译并跑起来根目录common.project+package_lnx.sh克隆仓库后按脚本编译出可执行文件
二次开发者加一种导出格式Exporters/下的现有实现新写一个导出函数 + 一行RegisterExporter
格式研究者适配一款新游戏Unreal/GameDefines.hUnreal/GameSpecific/GameDatabase.cpp加一个游戏宏,在特例处用Ar.Game分流

值得说明的是,这个项目没有用 CMake 这类主流构建系统,而是维护了一套基于 Perl 的自定义构建器Tools/genmake。它读取人类友好的.project描述文件(变量、条件、平台声明),再生成对应平台的 Makefile。你可以在common.project里看到LIBC = sharedOPTIMIZE = size这类声明式配置——整个工程几十个源文件、上百个游戏宏,全靠这一套脚本组织。想亲手验证本文的每一站,可以这样开始:

git clone https://gitcode.com/gh_mirrors/ue/UEViewer cd UEViewer # 查看根目录的 package_lnx.sh 与 Tools/genmake,了解构建入口

地图的边界:哪些地方走不通?

诚实地说,这张地图有几处"禁区"。其一,它只覆盖视觉资源——着色器还原、蓝图逻辑这类非可视化数据不在范围内。其二,UE4 的 AES 加密包需要你手动提供密钥(界面里那个密钥输入框就是干这个的),且新引擎版本的支持往往要等社区跟进。其三,部分厂商私有格式是通过逆向推断出来的,个别字段含义不明时,USE_COMPACT_PACKAGE_STRUCTS这类开关会选择"跳过不读",极端情况下模型可能出现细微瑕疵——这是所有逆向工程的宿命。


读完这条链路,你对 UEViewer 的理解就不再是"一个能提取资源的黑盒",而是一张有明确路标的地图:哈希索引的文件登记、验明正身的包解析、随身携带版本号的FArchive、以及一行注册一个导出器的分发机制。下次当你导出一个模型时,不妨想想它刚刚走过的四个驿站。

那么轮到你了:在实际逆向解析游戏资源的过程中,最常让你卡住的是哪一环——是版本兼容判断、pak 的压缩与加密、纹理的 GPU 压缩格式,还是骨骼动画的绑定关系?欢迎分享你的经历,也期待看到你基于这张地图扩展出的新玩法。

【免费下载链接】UEViewerViewer and exporter for Unreal Engine 1-4 assets (UE Viewer).项目地址: https://gitcode.com/gh_mirrors/ue/UEViewer

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/18 1:25:30

沃尔沃PHEV产能提升三倍:传统车企电动化转型的供应链与制造体系挑战

1. 从一则行业新闻说起&#xff1a;沃尔沃的产能“豪赌” 前几天&#xff0c;行业里不少朋友都在讨论沃尔沃的一则消息&#xff1a;为了满足市场需求&#xff0c;他们计划将插电式混合动力车型的产量提升三倍。这可不是个小动作&#xff0c;三倍意味着整个供应链、生产线、乃至…

作者头像 李华
网站建设 2026/8/18 1:23:56

二分查找算法详解:从基础到边界查找的三种实现

1. 从“找得到”到“找得准”&#xff1a;二分查找的三种境界如果你写过代码&#xff0c;或者刷过算法题&#xff0c;那么“二分查找”这四个字对你来说一定不陌生。它几乎是算法入门的第一道坎&#xff0c;也是面试官最爱考察的基础能力之一。很多人觉得&#xff0c;不就是在一…

作者头像 李华
网站建设 2026/8/18 1:23:04

AI工程插件开发实战:从环境配置到CAD/SolidWorks智能集成

1. 背景与核心概念&#xff1a;AI工程插件的价值与挑战在当前的工业设计与软件开发领域&#xff0c;AI技术的融合正从概念走向落地。一个典型的场景是&#xff1a;工程师希望利用AI来辅助完成CAD&#xff08;计算机辅助设计&#xff09;或SolidWorks&#xff08;SW&#xff09;…

作者头像 李华
网站建设 2026/8/18 1:22:57

DeepSeek Harness 部署指南:从环境配置到生产级 AI 服务搭建

1. 先搞清楚 DeepSeek Harness 到底是什么&#xff0c;以及它到底能帮你做什么 如果你最近在关注 AI 开发工具&#xff0c;尤其是想本地运行或部署大语言模型&#xff0c;那“DeepSeek Harness”这个名字你大概率见过。但别急着去搜安装命令&#xff0c;先花一分钟弄明白它是什…

作者头像 李华
网站建设 2026/8/18 1:22:12

柴油皮卡核心优势与使用维护全解析:从低扭特性到DPF再生

1. 从“工具”到“伙伴”&#xff1a;柴油皮卡的魅力与误解提到柴油皮卡&#xff0c;很多人的第一印象可能还停留在“冒黑烟”、“噪音大”、“冬天难启动”的刻板印象里。作为一个和柴油皮卡打了十几年交道&#xff0c;从工地到高原、从泥地到沙漠都跑过的人&#xff0c;我想说…

作者头像 李华