

这是一份完美立绘解包的脚本。包含解析 Export 文件结构,Sprite 拆分,解析 Unity 组件树,解析人物所有表情差分(不是全动作,因为组合超过上千种),实现4种图层混合模式以及剪辑蒙版,解析所有魔女化立绘(切割精灵图集立绘 Dicing Sprite 技术,网格体UV坐标渲染)。
更重要的是,以上工作是一键全自动完成的。这真的非常酷。
并且支持自定义动作导出,这可能是你需要下载脚本的理由。脚本运行并不复杂,只需要 Python 环境。预先使用的 AssetRipper 选择默认导出即可。
要想直接获取立绘资源或脚本及使用方法,请移步文末
AssetRipper
Python
首先你要下载游戏《魔法少女的魔女审判》(Steam,whatever)
然后找到游戏资产,在MacOS下为
(用户文件夹)/Library(资源库,需要显示隐藏文件夹)/Application Support/Steam/steamapps/common/manosaba_game/manosaba.app(右键菜单选择显示包内容)/Contents/Resources/Data/StreamingAssets/aa/StandaloneOSX/
在Windows下为
(Steam游戏目录)\manosaba_game\manosaba_Data\StreamingAssets\aa\StandaloneWindows64\
找到游戏资产后,请放过那些让人眼花撩乱的包名因为我只对美少女立绘感兴趣,
我们径直前往 naninovel-characters_assets_naninovel/characters 目录,里面全是魔女的宝藏人物立绘包,至此,基督山伯爵到达了他忠实的藏宝地。

然后打开AssetRipper。我没有使用AssetStudio因为他的Unity版本支持太老,在解析和导出魔裁时会出现内存泄漏的Bug以及无穷无尽的Expection(直接把我16G的内存瞬间干满了,离谱)。

可以不用改动导出设置,直接 File > Open File ,文件管理器中选择你推的角色包我推的艾玛,然后 Export > Export All Files > Select Folder > Export Unity Project。
注意,以下内容均为笔者的技术探索自嗨
世界上只有极少数的人会对接下来的部分感兴趣。到底是什么人才会感兴趣呢真是好难猜啊(棒读)(。・ω・。)。
首先吸引我们的是位于 ExportedProject/Assets/Texture2D/Ema_Angle01.png 的恐怖血腥暴力魔女肢解图艾玛碎片。

不行不行我们必须把艾玛立即拼回去再这样下去我都要对魔法少女产生心理阴影了!!!
肯定在某个地方藏着复原的魔法!(小声:ExportedProject/Assets/Sprite/ ,下图)

定睛一看,这些都是Unity特色的YAML配置文件,但里面参数太多,我们需要找到能将魔女碎片切割下来的参数。
其实配置文件中有意义的数字并不多。显然 m_Rect 参数决定了魔女碎片的切割矩形。于是我用 python 写了用于分割出所有魔女碎片的脚本 breakup.py,运行结果如下图。

现在我需要拼好艾玛酱。
感谢B站用户 @四棱之锋 揭示了仅凭拆包文件就可以还原立绘的深刻原理,但是这位大佬的对 Unity Asset 资产探索之路止步于此。我将秉持前人的火种,对这份15000行的 prefab 资产(下图)发起神圣冲锋。

在确定之前的 Sprite Asset 中没有关于碎片复原的信息后,我转向了位于 ExportedProject/Assets/#WitchTrials/Prefabs/Naninovel/Characters/LayeredCharacters/Ema.prefab 的巨大 Unity 数据结构。
直接说结论吧。对于以 “--- !u!1 &..................................” 行为间隔的数据结构块,我们需要找到记录形变的 Transform 数据结构块。在对其中巨长巨长的 id 编号进行追踪后,我发现这是一颗 GameObject 组件树,对应了Unity中的组件树。每个节点都有 Transform 属性。只需要从树根(father id 为 0,同时也是文件第一个 GameObject)往下走到叶子节点(对应了之前的每一个魔女碎片),将沿途的 TransForm 值叠加,就能得到魔女碎片的最终形变值。
但是问题来了,对于数十种的 Arm、Eyes、Mouth 碎片,该如何组合才能形成游戏中的立绘呢?以及组件前后遮挡关系(Z轴位置)如何确立?
在我对碎片名字进行一番检索后发现了一个叫 CompositionMap 的数据结构(下图),里面用晦涩的咒语描述了艾玛的笑靥,艾玛的怒容,艾玛的脸颊,艾玛的决意,艾玛的讶异,艾玛的眼泪,艾玛的恐惧,艾玛的沉思,艾玛的。。。我承认我写这一段是为了让我的输入法记住艾玛

CompositionMap 是由 <Key, Composition> 对组成的长长的列表,Key 是组件名称,而 Composition 是描述组件的咒语。
但这等低劣的咒语怎能瞒过魔法学院的优等生?只消稍稍动用名为《编译原理》的魔法书就能将上下文无关文法解析!最终我编写了能将任何咒语崩解为终结符序列的代码(下图为结果示例)。

具体来说,咒语的作用对象是 Unity 中的组件树(下图),在与Unity编辑器多次比对后,我逐渐了解了其中的语法:
“ > ”:排他渲染,比如“Angle01/ArmL>ArmL11”意味着在 ArmL 节点下只渲染 ArmL11 组件。
“ + ”:全部渲染,比如“Angle01/Option_ArmR/Option_ArmR01_01+”意味着渲染 Option_ArmR01_01 所有子组件。
“ - ”:取消渲染,比如“Angle01/Effect_Back_Arms-”意味着不渲染 Effect_Back_Arms 下的任意组件。

为了实现上述规则,我在 objtree.py 中依据 prefab 文件重建了 Unity 组件树。并且对于未在咒语中出现的碎片,其是否渲染取决于其 SpriteRender 中的 m_Enabled 参数。
而渲染的顺序与组件树中出现的先后顺序相同。至此,我们已经我们几乎完成了拼好艾玛酱的工作。至少我一度是这样认为的。
直到我遇见安安。
先放一只安安镇楼
如果安安没有这么可爱,我就不会有动力做接下来的工作。其也是达到完美的必经之路。

在我偶然鉴赏安安时,我发现了有一丝违和感。安安竟然有2只眼睛!
对不起我话说急了,我指的是在游戏中安安永远只会露出一只眼睛。这可是和“炮姐背包上永远挂着呱太”,“美少女换衣服一定会被撞见”同等效力的铁律啊!
于是我顺利成章地找到了一个名为 ClippingMask_Facial_Normal02.png 的贴图。经过我多年平面艺术创作大雾的经验来说,贴图之间存在着名为“剪辑蒙版”的不纯洁上下层关系!
我又来科普了。
剪辑蒙版是以背景图层的Alpha通道为系数对前景图层Alpha通道进行乘积变换的。。。算了直接上图吧

简单来说,就是前景图层被限制在背景图层(眼睛)的“形状”中,这样就能做出眼睛被“头发”遮挡的效果了。
而且不仅如此,贴图中还有一些名称后带有 “Overlay”、“Multiply”、“Softlight” 的异教徒。这唤醒了我对图层混合模式的回忆。你要问它们师出何门?请好好看看 Photoshop 中的 “叠加”、“正片叠底”和“柔光” 混合模式。
混合模式用对了容光焕发,用错了只会被糊一脸。安安:( ;´Д`)
求放过

很好很好,说了这么多,我也只是知晓了这两种图层模式的存在。我现在不仅要逆向 Unity 资产中对混合模式的描述,还要找出剪辑蒙版的 Mask / Masked 映射关系。而且还要在脚本中实现一遍。
我耗费了大量的时间在逆向 Unity 资产对图层模式的描述上。直接搬出结论:
之前的 Prefab 文件中不存在相关信息。Sprite Asset 中也没有。
Unity 中实现图层混合和剪辑蒙版靠的是 Shader,其通过 Material 与之前的Sprite 组件相关联。
Shader 组件是经过编译的,也就是以二进制机器码存储在资产中。AssetRipper 即便经过反编译,也难以解析或使用。
看来我的全自动解包之路就要葬送于此了。但是我既然把这篇专栏发出来了,就说明天无绝人之路。
在这里我要感谢游戏程序员良好的资产命名规范。你看下面这些文件名,你发现了什么?

没错!谜底就在谜面上!Material 资产的命名已经揭示了一切的真相:“Multiply”、“Default”、“Softlight”等字段是混合模式,而 Mask_Ref / Masked_Ref 描述了剪辑蒙版的映射关系。(比如所有关联 Mask_Ref1 的图层就是所有关联 Masked_Ref1 图层的剪辑蒙版)
噫!好!理论存在,实践开始!
(此处省略了 4 个小时的写代码与 debug 时间)⏰
最终,我在 Numpy 的鼎力支持下,实现了4种图层混合模式和图层任意剪辑蒙版机制(下图)。这也就是 Photoshop 图层机制的基础容我自夸一下

我用简洁的 blend( ) 接口支持了上述功能,只需一行代码,无论是 “正片叠底” 还是 “柔光” ,以及任意复杂的剪辑蒙版(比如图层 6,7,9 剪辑图层 1,3,4,5 之类的),全都不在话下!
让大家看看平凡的立绘与完美的立绘的区别。草,安安实在是太好看了


本人秉持着这样的解包理念:只要Unity能还原的资产,我就能够还原。
这成为了我面对这份 200, 000 字节的二进制数据(下图)的底气。

在魔裁中,所有的魔女化立绘都是以一种名为“Sprite Dicing”技术存储的。它将多种魔女立绘差分无慈悲地切割成碎肉,然后就能省下大量相同碎片的存储空间。
故其在料理碎肉时,必须要有一本精细的菜谱,里面包含了如何将每片碎肉拼凑回去的映射。而在计算机图形领域,天然地就有一个概念与之不谋而合:网格(Mesh)。
科普怪又来了:-P
一般情况下,在计算机图形渲染时,一切模型都是由大量三角形构成的网格体(Mesh)。而模型的贴图就是从纹理集上截取UV坐标定义的三角形,变换到显示上XY(Z)坐标定义的三角形上。故每个顶点(Vertex)有双重坐标:XY(Z)显示坐标,与UV纹理坐标(下图)。
如果我早知道这是用UV坐标渲染的,我也不会这么晚才知道这是用UV坐标渲染的了

总之我对这堆二进制数据一通切割,分组。在经历了无数次误解(数据)与争论(电脑风扇)后,我终于和它知根知底了:
前一半是存储的8位 Float 顶点 XYZ 坐标,可以切分为4个一组,描述了 Mesh 中的一个方格。
后一半存储的是8位 Float UV 坐标,与前一半的 XYZ 坐标一一对应,取值在0到1之间(这也是我判断其为 UV 坐标的重要依据)。
(图形化分析数据的尝试,这与魔女立绘的形状相合)

由于立绘 Mesh 和纹理都是方格,故无需实现三角形的渲染,只需要渲染正方形就行了。这极大地简化了代码实现。(贴一下代码)

(哈哈逗你们的)
噫!好!现在我已经拥有了解析任何魔裁立绘包的能力了!(<- 游戏时长不满5小时的萌新此时还没有意识到,他已经花费了50+个小时在解包上面)
在拼好艾玛酱的过程中,我们需要将许多半透明的图像叠在一起。但是2层半透明的图像叠加并不是简单的像素覆盖,其中要涉及到图层混合算法。
由于《魔裁》的立绘资产并非 Alpha 预乘格式,而是普通的 Straight 格式。所以我们理论上只需要实现 Photoshop 中的默认图层混合(即 Alpha Blending)。
科普一下吧,Alpha 预乘格式仅仅是将普通图像(即 Straight 格式)的 rgb 值乘以了其 alpha 值。其在常见混合算法中拥有一定的性能优势。
这是我在踩完坑才知道的
我首先尝试了 Python Pillow 库的 paste 方法,但是结果有问题,不透明像素上叠加半透明像素竟然是半透明的!我们的艾玛酱怎么能是半透明的纸片人呢?艾玛酱明明就是真实存在的(下图)!

之后我又试了 Pillow 的 alpha_composite 方法。但很遗憾,这个方法是为Alpha 预乘格式的图像准备的。而 blend 方法更是与 Photoshop 中的默认图层混合八杆子打不着。
在 Kimi 大人的指示下(联网搜索,这个后面要考),我尝试用 Numpy 手搓 Alpha Blending,但同样的出现了半透明像素(在艾玛的脸上,显然不该出现的位置)。
于是我开始 Bing,找到了几篇 Alpha Blending 的技术博客。我开始认真地学习图层混合算法。但读了几篇博客我惊奇(其实是惊吓)地发现:他们的算法不仅实现有出入,而且理论上同样会出现半透明像素的问题!
没法。我现在只想找世界上最权威的 Alpha Blending 的描述,其权威程度最好能比魔法少女在二次元更权威。没错,我去找了 Wikipedia,然后重写了一遍混合算法(下图)。它终于对了。终于对了。于对了。对了。了。。

在解包的世界中,最理想的流程是在游戏下载数据包之后,我们按下桌子上的那颗醒目的透明保护罩中的红色按钮其实是Enter,然后整个房间嗡嗡作响,天摇地动,邦邦卡邦!各类游戏解析后的分门别类的资产在一阵烟尘后出现,咳咳!烟太大了容我戴个口罩先...
(口罩低通滤波后的浑厚嗓音)同时同步更新到了游戏Wiki上。这一定是每个解包人的梦想吧。一定是这样吧。怎么可能不是呢。
只有对于长期运营的游戏(比如明日方舟Wiki),这种程度的自动化的价值才能被最大程度地体现。而我作为一个理想主义者,义无反顾地加班加点地打造了《魔裁》立绘解析的自动化工具。
具体是在 config.py 中,你只要告诉它角色解包后的目录,它就能够收集上述流程所需的全部信息,甚至于所有合理的立绘差分预设!要知道,在百余个魔女碎片和几十个 compositionMap 规则中,其自由组合的模式多达 16.8 亿种!(其实没有那么多,雷军梗)(其实有这么多,理论上 2^100 多)
我决定从长长的 compositionMap 规则入手。我粗暴地截断了 “Normal1” 的 Key,然后再排除作为子项出现过的 Key。事实证明,这招十分管用。

总而言之,经历了漫长的 debug 后,我最终实现了一键解包脚本。
在此后,我又优化了图像处理的性能(Numpy on CPU,局部性能提升数十倍,全局性能提升3倍),规范了代码结构,提升了自动化程度,重整了文件层次。容我再自夸一段
其实从务实的角度来讲,这个一键自动化解包的意义并不是很大。对于《魔裁》这样的小体量解包,也许我用半自动的代码解包的时间都远远少于我 debug 全自动脚本的时间。而且全自动脚本的灵活性很差,经常遇到意料之外的数据格式使得程序 crash (比如说某人的 Sprite 里竟然有大小为 0 x 0 的 mask 项目,这使得我的图像导出失败)(这个 bug 已经修复了)。要说如果这个脚本能被很多人使用我也不觉得,可能下载这个脚本的人都寥寥无几。
但是就如罗永浩所说,我们都希望生活在一个理想主义者的国度,偶尔做些富有理想主义色彩的事情也无伤大雅。强行升华
用 AssetRipper 将任何立绘包导出到目录 Dir(不用更改导出设置)。
在终端中 cd 到脚本所在的目录,运行 python run.py -d <Dir和这里的相对路径>
请站在 output 文件夹下见证神迹的出现。
这份脚本还有更多的用法,比如自定义动作,只需要在 config json 里更改组件表,再运行 python run.py -c <config json文件> 就能让角色摆出各种姿势了(包括一些十分娇羞的姿势,下图)(艾玛:⁄(⁄ ⁄ ⁄ω⁄ ⁄ ⁄)⁄ iya~),详情见脚本中的README。

@四棱之锋 写的解包文章 【魔法少女的魔女裁判】立绘解包重装,这对我帮助很大。