《DNFMobile》图片资源提取笔记

  欢迎参与讨论,转载请注明出处。

前言

  《DNFMobile》的新一波内测到来了,恰好得到了安装包。便欲对比其资源较之端游有何不同,遂试图提取之。在尝试的途中遇到了不少问题,特此记录之。源码地址

读取资源包

  与众多手游的习惯一样,初始的安装包所携带的资源甚少,皆需经过更新方才完整。更新后经过观察得知游戏采用Unity制作,那么事情便简单了,直接上UnityStudio读取。虽然资源文件的后缀名为.npk,但实际上则是Unity的AssetBundle,且并未作加密。然而诸多现成的Unity提取工具皆有多少缺陷(无法识别pvr格式、无法批量化操作、导出资源过于原始等),且图片资源是大图形式存在的,需要进行切图,而使用切图工具一则怕不够精确,二则怕无法批量化。于是我选择直接使用Unity制作工具以面对此需求。
  由于资源文件本身即是Unity的格式,那么直接调用API加载即可,类似如此:

1
2
3
4
5
6
var assetBundle = AssetBundle.LoadFromFile(path);
var assetBundle.LoadAllAssets<Texture2D>();
foreach (var tex in texs) {
//...
}

切图

  这样可谓相当方便,接下来的问题便是切图了。我本以为大图是由Unity自动生成,所以理应资源内会有对应的Sprite资源,这样通过Sprite资源的信息即可进行切割。但实际上并非如此:大图是事先生成好,然后使用脚本填写每帧配置在运行时自动生成Sprite。这种做法也是理所当然的,毕竟Unity的Sprite的pivot与DNF的IMG包提供的偏移点可谓天南地北。(一者为当前图片下的浮点百分比,另一者为实际坐标)通过直接在配置直接对接IMG包的数据然后进行转化这是很正常的做法。可这下子就麻烦了,我们并无法直接知道这脚本的具体信息。幸好UnityStudio的解析中包括了关于MonoBehavior资源配置的信息。
bwanga
  可即使知道也无法直接Unity进行获取,毕竟我们本身是没有该脚本的。鉴于UnityStudio开源的特性,我起初打算阅读源码掌握其解析之法。最后也成功了,可我突然脑内灵光一闪,想到了直接建立一个同名脚本,并根据配置的信息进行模拟。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DNFAtlasAsset : MonoBehaviour {
[Serializable]
public struct Rectf {
public float x;
public float y;
public float width;
public float height;
}
[Serializable]
public struct DNFAtlasElement {
public string name;
public int originIndex;
public int referenceIndex;
public int originWidth;
public int originHeight;
public int offsetX;
public int offsetY;
public Rectf rect;
}
[Serializable]
public struct DNFAtlasSlot {
public int matType;
public DNFAtlasElement[] elementList;
}
public string atalsName;
public DNFAtlasSlot[] atlasSlotList;
}

  天可怜见,居然成功了!那么接下来采用类似如下方法即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int width = (int)element.rect.width;
int height = (int)element.rect.height;
if (width == 0 || height == 0) {
continue;
}
var colors = tex.GetPixels((int)element.rect.x, (int)element.rect.y, width, height);
var newTex = new Texture2D(width, height, tex.format, tex.mipmapCount > 1);
newTex.SetPixels(colors);
newTex.Apply();
var path = this.GetPath() + tex.name + "/";
var bytes = newTex.EncodeToPNG();
var json = JsonUtility.ToJson(element);
var name = this.ToNumber(element.name);
this.CreateDirectory(path);
File.WriteAllText(path + name + ".json", json);
File.WriteAllBytes(path + name + ".png", bytes);
Texture2D.DestroyImmediate(newTex, true);

  主要思路便是通过GetPixels方法读取区域像素并覆盖至新图。最后将图片转换为PNG、配置转换为JSON并输出即可。
  当然这里还有关于Texture2D的readable问题,隶属于资源包的Texture2D并无法直接使用GetPixels方法,需要对其进行复制,然后利用新图施为,为此我写了个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private Texture2D GetTexture(Texture2D tex){
if (!this.texureMap.ContainsKey(tex)) {
if (tex.width == 0 || tex.height == 0) {
return null;
}
var copyTex = new Texture2D(tex.width, tex.height, tex.format, tex.mipmapCount > 1);
copyTex.LoadRawTextureData(tex.GetRawTextureData());
copyTex.Apply();
var writeTex = new Texture2D(copyTex.width, copyTex.height);
writeTex.SetPixels32(copyTex.GetPixels32());
writeTex.Apply();
writeTex.name = this.ToName(tex.name);
this.texureMap[tex] = writeTex;
Texture2D.DestroyImmediate(copyTex, true);
}
return this.texureMap[tex];
}

  以上便是关于切图方面的问题,具体可参阅源码。

内存问题

  游戏目前的资源包数量高达2900以上,在尝试一口气全部提取时内存竟然高达5G!最终电脑不堪重负倒下收场。这很显然是资源并未回收所致,是以作此函数:

1
2
3
4
5
6
7
8
9
10
public void Destroy() {
foreach (var texture in this.texureMap) {
Texture2D.DestroyImmediate(texture.Key, true);
Texture2D.DestroyImmediate(texture.Value, true);
}
this.texureMap.Clear();
this.assetBundle.Unload(true);
AssetBundle.DestroyImmediate(this.assetBundle, true);
}

  这里采用的是DestroyImmediate方法,好处是立即进行回收,但却会因此阻塞,影响提取效率。若使用Destroy方法则不会如此,不过峰值内存会上升。但大规模读取时还是以稳定为主,而小份读取则两者并无所谓。是以选择DestroyImmediate方法。
  另外在其他地方涉及到资源生成且是继承自UnityEngine.Object的,皆需注意此问题。在经过优化后,占用由5G跌倒了500M-1.5G之间,成功提取了全部图片资源。大功告成!

后记

  这次《DNFMobile》的声音资源经过了高压,原本几M的音乐变成了上百K,可谓惨不忍听,遂无提取的价值。而纸娃娃方面则是采用了类似NPK_Ver4的色板做法,也并无法直接提取到成品。由此可见制作组为了节省空间可是下了不少功夫呀。