本文拆包教程不限于明日方舟,在后面也会给出其他手游的拆包教程,例如少女前线,碧蓝航线等
最近一次更新于2019年8月7日
首先拆包最简单的无非就是拆取游戏资源,例如游戏立绘,音频,视频等,再深层次一点有拆取游戏配置文件,apk反编译得到部分源码等
在本文,我们只介绍拆取游戏资源和对游戏资源的后期处理,但是这里的方法不能保证所有的手游立绘都能提取,不同游戏对立绘等游戏文件的处理不同,这里只提供几种思路 XD
可能需要用到的工具有 Unity Studio,装有 opencv 的python或C++,PhotoShop
其中 Unity Studio 是必要工具,可以用来提取市面上大部分手游的游戏资源
装有 opencv 的python或C++,PhotoShop 等可以对提取的游戏资源(立绘)进行处理,如果是要批量处理的话首推 C++ ,python 虽然方便但是太慢,PhotoShop 也可以进行批量处理,但是麻烦且慢
PS:如果有其他有同样功能的工具也可以,不限于上面的几个软件,点击蓝色可以进网盘下载,理论上用 python的 PIL库 更好,不过我直接用 opencv 了,导致最后生成的可执行文件比较大
首先,如果要拆取立绘等文件,就需要对应用程序进行处理,最简单的有两种方式:
当然,到这里还没有结束,我们看到的资源文件并不是原来格式,而是以 ab后缀的文件 展示的,这个时候就需要 Unity Studio 来提取目标文件(当然不是所有手游都会用Unity,所以这个方法并不适用于所有手游,如果以后我遇到其他类型的再更新吧XD)
这里我们以明日方舟为例,如果你正确的找到了路径,那么你可以看到一个名为 charpack 的文件夹,其中储存了明日方舟的干员立绘文件
这个时候打开 Unity Studio ,用如下选项 Extract folder 批量解压提取ab文件,之后使用 load folder 加载处理后的文件
然后在 Asset List 中就可以看到提取的内容
提取出来的图片大部分情况下是有瑕疵的,因为他们需要进一步处理才能得到原图
而对于其中提取的内容,我们通常需要关心很多文件
常规情况下(使用通道分离压缩图片的):我们需要找到一个是Type为Texture2D的原图,一个是Type为Texture2D或Sprite的透明度背景图,而且通常情况下,原图有一个文件名,对应透明度背景图文件名会在原图文件名后面加上alpha字样,我们通过处理这两个图才能得到最终的png图像文件,所以现在就将所有对应的图片文件从 Unity Studio 导出
还有一些情况,有些手游会对图片进行加密或者是做一些额外处理:这种情况我们就需要按照情况来分析哪些文件有用,哪些文件没用,例如碧蓝航线需要使用一张图片 + 一张obj 3D模型文件来还原原图
首先我们在前面提到过,如果我们能找到一个是Type为Texture2D的原图,一个是Type为Texture2D或Sprite的透明度背景图,那么就说明这个立绘需要进行通道分离图像的合成来得到原图
那么这个时候我们应该如何处理呢,首先 png 图像有4个通道,前三个是 RGB 颜色通道,第四个是透明度通道,实际上为了减少图片的大小,大部分游戏厂商都会先对图片进行 ETC 等压缩算法的处理,再将透明通道单独剥离出去,以达到最大程度的压缩,其中你会发现我们经手的立绘和在游戏中展示的一样,但仔细看会发现不够清晰,正是因为在打包成apk文件之前,游戏公司就已经先对图片进行了压缩处理,丧失了一部分信息,所以说并不是画师的原图,如果想提高清晰度,有两种方法,一种是找原画师或官方公布等途径得到原图,一种是利用图片优化软件对其进行清晰度优化
进入正题,如何处理我们得到文件?
这里我们直接引用一下这位贴吧老哥的方法: 透明立绘简单合成方法,小白一键操作
方法很简单,但缺点是对大量处理文件十分无力,而且需要手工对文件顺序进行排序,也没法判断两个图片尺寸比,适合小量立绘的手工处理
这个方法适合大量处理,而且不用对图片进行分类,甚至你在 Untiy Studio上导出图片时,可以毫无顾忌的将图片一股脑导出来,让程序自己去找原图和透明度背景图
在这里我用到的工具是 opencv 库
以明日方舟为例: 可以发现它 Alpha 图像中 R 通道 的值代表原图片透明度,那么我只需要将这个图片矩阵的 R 通道 信息赋值给原图像的 透明度通道 即可,然后注意一下有些Alpha图像是原图像大小的 1/2 倍,需要放大 2 倍
其中放大方法调用的是 双立方插值算法 ,这个算法效果比较好
另外就是 PhotoShop 的默认放大算法也是这个算法
python代码: (本来想用C++的,但无奈python太方便了Orz,导致处理速度会慢很多)
# -*- coding: utf-8 -*-
import shutil
import time
import cv2
import os
def read_png(s):
img = cv2.imread(s, cv2.IMREAD_UNCHANGED)
return img
def save_png(s, img):
cv2.imwrite(s, img, [int(cv2.IMWRITE_PNG_COMPRESSION), 9])
def exchange(img, x): #放大图像
w, h = img.shape[0:2]
tempimg = cv2.resize(img,(w*x,h*x), interpolation=cv2.INTER_CUBIC)
return tempimg
def Do(a, b):
a[:,:,3] = b[:,:,0] #合成的基本思路
return a
print("提取图片/合成立绘?1/2")
a = input()
if a == '1': #提取立绘
file_list = os.listdir("./Input")
print("共%d个图片:" %len(file_list))
print('begin')
for i in file_list:
img = cv2.imread("./Input/" + i, cv2.IMREAD_UNCHANGED)
if img is not None and img.shape[0] > 512:
if '#' in i:
i2 = i.replace(i[i.find('#'):], '#1[alpha].png')
i3 = i.replace(i[i.find('#'):], '#1.png')
else:
i2 = i.replace(".png", '[alpha].png')
i3 = i
if i2 in file_list:
hutil.copy("./Input/" + i, "./Texture2D_A/" + i3)
os.remove("./Input/" + i)
shutil.copy("./Input/" + i2, "./Texture2D_B/" + i2)
os.remove("./Input/" + i2)
print('over')
os.system('pause')
if a == '2': #合成立绘
file_list = os.listdir("./Texture2D_A")
print("待合成%d个图片:" %len(file_list))
for i in file_list:
print('正在处理%s' %i)
time_start = time.time()
i2 = i.replace(".png", '[Alpha].png')
a = read_png('./Texture2D_A/' + i)
b = read_png('./Texture2D_B/' + i2)
if a.shape[0]/b.shape[0] != 1:
b = exchange(b, int(a.shape[0]/b.shape[0]))
save_png('./Picture/' + i, Do(a, b))
shutil.copy("./Texture2D_A/" + i, "./Used/" + i)
os.remove("./Texture2D_A/" + i)
time_end = time.time()
print("耗时 %f s" %(time_end-time_start))
print('over')
os.system('pause')
具体操作就是:
1:你只需要把提取出来的立绘图像(可以包括小人动态的分割图像)放进 Input 文件夹中
2:运行exe文件,先输入 1 提取所有合理的立绘图像
3:再次运行exe文件,输入 2 合成立绘,最后图像在 Picture 文件夹中
其中 Texture2D_A,Texture2D_B 和 Used 都是程序运行的中间文件夹,可以不用去管,另外就是这个程序支持中断操作,也就是说你在合成图片或提取图片时因为某些原因终止了程序,那么程序会保留你之前的记录,你想要把剩余的图片处理完,只需要再次运行即可
明日方舟立绘集合,目前更新至2019年7月20号: 明日方舟立绘集合,提取码:5iyj
明日方舟立绘合成器,目前更新至2019年7月20号: 明日方舟立绘合成器,提取码:dsfd
同理,少女前线也是使用了通道分离来处理立绘图像,所以代码都差不多,就不贴了
少女前线立绘集合,目前更新至2019年7月27号: 少女前线立绘集合,提取码:xmts
少女前线立绘合成器,目前更新至2019年8月3号: 少女前线立绘合成器,提取码:50f9
比较典型的就是就是碧蓝航线,这款游戏没有用通道分离来压缩图像,而是因不明原因,将图像换成了碎块和记录碎块在原图信息的 obj 文件组成
碎块图(尸块图):
obj文件:
打开obj文件分析就会发现其中的 v 记录的是每个碎块在原图像的顶点坐标,4 个为一组:
其中的 vt 记录的是每个碎块在碎块图像的顶点坐标(分别乘上碎块图像长和宽之后就是顶点坐标,这个小数代表在碎块图像中的比例位置),也是 4 个为一组:
那么通过读取 obj 文件的信息来拼接碎尸图,就可以系统的将原图拼接好
python代码:
# -*- coding: utf-8 -*-
import numpy as np
import shutil
import time
import cv2
import os
def read_file(s):
List1 = []
Filer = open(s,'r')
List2 = Filer.readlines()
Filer.close()
for i0 in range(0, len(List2)):
List2[i0] = List2[i0].replace("\n", "")
List1.append(List2[i0].split())
return List1
def read_png(s):
img = cv2.imread(s, cv2.IMREAD_UNCHANGED)
return img
def save_png(s, img):
cv2.imwrite(s, img, [int(cv2.IMWRITE_PNG_COMPRESSION), 9])
def rotate(img):
new_img = cv2.flip(img, -1)
return new_img
def manage(s1, s2):
s1 = "./Mesh/" + s1
max_height = max_weight = vtjShu = firstx = endx = firsty = endy = 0
print('正在处理%s' %s2)
time_start = time.time()
List = read_file(s1)
img = read_png("./Texture2D/" + s2)
height = img.shape[0]
eight = img.shape[1]
img = rotate(img)
img = cv2.flip(img, 1
for i in range(1, len(List)):
if List[i][0] != 'v':
vtjShu = i
break
if abs(int(List[i][1])) > max_weight:
max_weight = abs(int(List[i][1]))
if abs(int(List[i][2])) > max_height:
max_height = abs(int(List[i][2])
Base_img = np.zeros([max_height+1, max_weight+1, 4], np.uint8)
jShu = vtjShu
while vtjShu < len(List):
if List[vtjShu][0] != 'vt':
break
firsty = int(float(List[vtjShu][1]) * float(weight) + 0.5)
firstx = int(float(List[vtjShu][2]) * float(height) + 0.5)
endy = int(float(List[vtjShu+2][1]) * float(weight) + 0.5)
endx = int(float(List[vtjShu+2][2]) * float(height) + 0.5)
y1 = abs(int(List[vtjShu-jShu+1][1]))
x1 = abs(int(List[vtjShu-jShu+1][2]))
for i in range(firstx, endx):
for j in range(firsty, endy):
Base_img[x1+i-firstx][y1+j-firsty] = img[i][j]
vtjShu += 4
Base_img = rotate(Base_img)
save_png("./Picture/"+s2, Base_img)
time_end = time.time()
print("处理完毕,耗时 %f s" %(time_end-time_start))
Mesh_list = os.listdir("./Mesh")
Texture2D_list = os.listdir("./Texture2D")
print("检测到%d组" %len(Texture2D_list))
for i in range(0, len(Texture2D_list)):
s = Texture2D_list[i].replace(".png", "-mesh.obj")
if s not in Mesh_list:
print("Can't find the %s in Mesh" %s)
else:
manage(s, Texture2D_list[i])
shutil.copy("./Texture2D/" + Texture2D_list[i], "./Used/" + Texture2D_list[i])
os.remove("./Texture2D/" + Texture2D_list[i])
print("Over")
os.system("pause")
碧蓝航线立绘集合,目前更新至2019年7月31号: 碧蓝航线立绘集合,提取码:65wf
碧蓝航线立绘合成器,目前更新至2019年8月5号: 碧蓝航线立绘合成器,提取码:slcg
具体操作就是:
1:你只需要把提取出来的立绘图像放在 Texture2D 中,而 obj 文件放进 Mesh 文件夹中,不必考虑顺序问题,程序会自己找
2:运行exe文件即可,最后结果在 Picture 文件夹中
其中 Used 是程序运行的中间文件夹,可以不用去管,另外就是这个程序跟上一个明日方舟立绘合成的程序一样也支持中断操作,所以中途暂停再运行是会按照原来的进度继续,而不是从头开始
其他手游的资源提取方法和后期处理方法可能会有所不同,而有些手游则相同,所以需要变通,如果有时间的话这个博客应该会一直更新
本人拆包立绘仅出于学习目的,不用做商业用途 (舔老婆 prpr)