崩坏学园2及大部分采用ETC1压缩格式的Unity3D游戏的拆包图处理

Posted 西城飘雪

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了崩坏学园2及大部分采用ETC1压缩格式的Unity3D游戏的拆包图处理相关的知识,希望对你有一定的参考价值。

0x0 背景

众所周知(?) , 大部分(?)以Unity3D为引擎的手游为了进一步压缩资源大小, 在Android平台经常将贴图资源以ETC1格式压缩以减少体积. 蛋疼的是ETC1不支持Alpha通道.... 

程序猿们选择将原图拆分, 用一张贴图来单独记录Alpha信息. 这就给后来的拆(偷)包(图)带来了不便(不要脸(*≧▽≦)), 怎么办呢? 合并回去就行了呀!(理直气壮)

0x1 实现

众...... 一张RGBA图片包含三个颜色通道以及一个Alpha通道, 经过拆分之后就变成了一张图片记录原图的RGB参数, 一张图片仅记录Alpha参数.

如崩崩的拆包图: 

那么要想合并回去, 只需要在一图中获取RGB对应的值, 在二图中获取Alpha的值, 然后合并在一起生成一一张RGBA信息的图保存下来.

本文所用语言为C#, 首先用易于理解的GetPixel()方法写一次~

 

0x2 核心代码 - 获取像素法

 1 private Bitmap mergeImageOld(Bitmap rgbTexture,Bitmap alphaTexture)
 2 {
 3     textureWithAlpha = new Bitmap(rgbTexture.Width, rgbTexture.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); //新建一个与RGB同分辨率的Bitmap
 4     try
 5     {
 6         for (int i = 0; i < rgbTexture.Width; i++)
 7         {
 8             for (int j = 0; j < rgbTexture.Height; j++)
 9             {
10                 Color withAlpha = Color.FromArgb(alphaTexture.GetPixel(i, j).R, rgbTexture.GetPixel(i, j));
11                 textureWithAlpha.SetPixel(i, j, withAlpha);
12             }   //internal for end
13         }   //for end
14 
15         return textureWithAlpha;
16     }
17     catch(Exception ex)
18     {
19         Console.WriteLine(ex.Message);
20         return textureWithAlpha;
21     }
22 }   //mergeImageOld()

其中6-13行是关键代码 for循环中逐行逐像素处理 

这里第十行用到的重载为:

Color Color.FromArgb(int alpha,Color baseColor);

第一个参数就是Alpha值, 第二个参数为颜色, alphaTexture.GetPixel(i, j).R 的意思的是在alphaTexture中第i行第j个像素获取红色分量, rgbTexture.GetPixel(i, j) 的意思是在rgbTexture中获取颜色

Alpha值拿到了, 颜色也拿到了, 直接SetPixel()就好咯~

 

0x3 进阶代码 - 指针法

上面的代码跑起来有个最大的问题就是..........太鸡儿慢了........本身GetPixel()就慢如蜗牛  我们还要在两张图中GetPixel()..... 

笔者的电脑上处理一张1.21M 1024*1024大小的图片耗时2.4秒左右 这怎么能受得了 

翻阅前辈资料后决定使用指针法来代替获取像素 

使用指针必须在项目设置中勾选允许不安全的代码  并且涉及到指针操作的代码必须放在  unsafe {.....} 区域中  不然不让编译~

代码如下: 

 1 public unsafe Bitmap mergeImage(Bitmap rgbTexture,Bitmap alphaTexture)
 2 {
 3     int width = rgbTexture.Width;
 4     int height = rgbTexture.Height;
 5 
 6     try
 7     {
 8         BitmapData textureWithAlphaData = rgbTexture.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);    //将图像锁定到内存中以便操作
 9         BitmapData alphaTextureData = alphaTexture.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
10 
11         byte* resultP = (byte*)textureWithAlphaData.Scan0;  //获取在内存的中首地址
12         byte* alphaP = (byte*)alphaTextureData.Scan0;
13 
14         for (int j = 0; j < height; j++)
15         {
16             for (int i = 0; i < width; i++)
17             {
18                 resultP[3] = alphaP[2]; //ARBG在内存中存储顺序为GBRA 所以resultP[3]即为Alpha分量  resultP[2]即为红色分量 
19                 resultP += 4;   //下移4个位置 处理下一个像素的信息
20                 alphaP += 4;
21             }
22         }
23 
24         rgbTexture.UnlockBits(textureWithAlphaData);    //解锁
25         alphaTexture.UnlockBits(alphaTextureData);
26 
27         return rgbTexture;
28     }
29     catch
30     {
31         return rgbTexture;
32     }
33 }   //mergeImage()

 

其中第8行要注意第二个参数设置成读写或只写(ImageLockMode.WriteOnly), 第三个参数因为这里需要带透明通道的ARGB格式所以设置成32位AGRB   

第9行就可以设置成只读了(ImageLockMode.ReadOnly) 而且因为本身读的图片就不带Alpha通道 也可以将格式设置成 PixelFormat.Format32bppRgb

第18行注意内存中32位ARGB格式Bitmap的次序为[G,B,R,A] 所以Alpha分量其实在第四个位置

另外一定要注意释放资源  笔者放在了这个方法外面 不然处理数量一多分分钟内存爆炸

1 rgbTexture.Dispose();
2 alphaTexture.Dispose();
3 textureWithAlpha.Dispose();

0x4 指针法补充

本文的例子由于需要Alpha通道, 所以直接采用了32位ARGB格式, 32位的Bitmap中有每像素占用四个字节, 每行数据的长度必定为4的倍数,所以不用考虑对齐

而另外也很常见的24位图每个像素占用的字节数就为24/8 = 3, 这时候每行的数据长度就不一定为4的倍数了 

举个栗子: 一张 10 * 10 的24位图片  每一行的数据长度为 3 * 10 = 30 字节  这时就会自动用"0"补充到32字节, 一共补充了 32 - 30 = 2个字节

那么如果在这种情况下使用指针来读取数据, 就必须跳过这些补位的字节  每行的实际字节数的获取方法为 BitmapData类中的属性 BitmapData.Stride

所以要处理24位的图片时, 需要在第一层for循环结尾处(即每行结尾处跳过占位的字节  如上面那个栗子就要跳2个字节)

代码如下 可以跟0x3中的对比一下

 

 1 byte* resultP = (byte*)textureWithAlphaData.Scan0;  //获取在内存的中首地址
 2 byte* alphaP = (byte*)alphaTextureData.Scan0;
 3 
 4 int resultOffset = textureWithAlphaData.Stride - width * 3; //用实际占位过的长度来减去图片每行有效像素占用的长度  此行代码仅对24位图有效
 5 int alphaOffset = textureWithAlphaData.Stride - width * 3;
 6 
 7 for (int j = 0; j < height; j++)
 8 {
 9     for (int i = 0; i < width; i++)
10     {
11         resultP[2] = alphaP[2]; //RBG在内存中存储顺序为GBR  这里仅仅举栗子方便对比  这行代码跑过之后会将2图的红色分量设置到1图中去
12         resultP += 3;   //下移3个位置 处理下一个像素的信息
13         alphaP += 3;
14     }
15     resultP += resultOffset;    //由于上面用的是图片的宽度  所以现在指针停在了占位字节的前面  所以这里需要跳过多出的字节
16     alphaP += alphaOffset;
17 }

 

 

 

0x5 结尾

代码地址: https://github.com/yyuueexxiinngg/HSoD2TextureMerge

用到的提取工具: https://github.com/Perfare/UnityStudio

两种方式的耗时对比如图:   (右键新标签打开)

 

以上是关于崩坏学园2及大部分采用ETC1压缩格式的Unity3D游戏的拆包图处理的主要内容,如果未能解决你的问题,请参考以下文章

Android NDK。从.PVR文件加载ETC1压缩纹理

Flink在米哈游的落地实践

Flink在米哈游的落地实践

300.Unity内存优化ID-315-001:图片压缩格式详解及优化原理篇之不同平台的压缩格式

Unity 之 纹理类型导入设置和压缩格式介绍

Unite 2018 | 《崩坏3》:在Unity中实现高品质的卡通渲染(下)