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
两种方式的耗时对比如图: (右键新标签打开)