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
兩種方式的耗時對比如圖: (右鍵新標簽打開)