最近用Unity做一个2D游戏的时候发现了Unity3.5引入了一个很低级的bug。在Unity3.5之前,使用Texture2D.PackTextures生成Texture Atlas,当原始图片太大无法打在一张大图上时会自动对原始图片进行缩小适应。在升级到Unity3.5后这一行为发生了改变:太大的图片会被直接裁剪适应,而不是缩放适应。这对于使用EZGUI用户来说是个很严重的问题,如果希望使用一张材质把很多图片缩小后打到一张贴图上,原来EZGUI会自动帮我们完成这个工作,但这个bug使原图被裁剪得不完整了,这条路就行不通了。在Unity论坛上发现其他用户也遇到了这个问题,但是没有很好的解决方法,只能重新设置材质,确保大贴图能容纳下所有的原始图片。
后来我想了个解决办法,当PackTextures无法容下原始图片发生裁剪后,我们可以主动按照裁剪后的图片大小缩放原始图片,使原始图片和裁剪后的图片一样大,然后再次调用PackTextues,这样就保证了这次大图可以容纳下所有原始图片了,完美地绕过了Unity的这个bug。下面附上我实现的一个双线性插值实现缩放的代码:
Texture2D ScaleTextureBilinear(Texture2D originalTexture, float scaleFactor) { Texture2D newTexture = new Texture2D(Mathf.CeilToInt (originalTexture.width * scaleFactor), Mathf.CeilToInt (originalTexture.height * scaleFactor)); float scale = 1.0f / scaleFactor; int maxX = originalTexture.width - 1; int maxY = originalTexture.height - 1; for (int y = 0; y < newTexture.height; y++) { for (int x = 0; x < newTexture.width; x++) { // Bilinear Interpolation float targetX = x * scale; float targetY = y * scale; int x1 = Mathf.Min(maxX, Mathf.FloorToInt(targetX)); int y1 = Mathf.Min(maxY, Mathf.FloorToInt(targetY)); int x2 = Mathf.Min(maxX, x1 + 1); int y2 = Mathf.Min(maxY, y1 + 1); float u = targetX - x1; float v = targetY - y1 ; float w1 = (1 - u) * (1 - v); float w2 = u * (1 - v); float w3 = (1 - u) * v; float w4 = u * v; Color color1 = originalTexture.GetPixel(x1, y1); Color color2 = originalTexture.GetPixel(x2, y1); Color color3 = originalTexture.GetPixel(x1, y2); Color color4 = originalTexture.GetPixel(x2, y2); Color color = new Color(Mathf.Clamp01(color1.r * w1 + color2.r * w2 + color3.r * w3+ color4.r * w4), Mathf.Clamp01(color1.g * w1 + color2.g * w2 + color3.g * w3 + color4.g * w4), Mathf.Clamp01(color1.b * w1 + color2.b * w2 + color3.b * w3 + color4.b * w4), Mathf.Clamp01(color1.a * w1 + color2.a * w2 + color3.a * w3 + color4.a * w4) ); newTexture.SetPixel(x, y, color); } } return newTexture; }这个函数会根据scaleFactor对输入纹理图片进行双线性插值缩放后输出新纹理。
然后对EZGUI的BuildAtlas函数稍作修改:如果最终的大图容纳不下原图,则使用我们自己的函数先对原图进行缩放,然后再次调用PackTextures,这样就可以使BuildAtlas和升级到Unity3.5之前有一样的效果了。
// Pack the textures to the atlas: texList.uvs = atlas.PackTextures((Texture2D[])texList.trimmedTextures.ToArray(typeof(Texture2D)), padding, maxAtlasSize); // Check to see if the texture had to be resized to fit: if (texList.uvs[0].width * atlas.width != ((Texture2D)texList.trimmedTextures[0]).width || texList.uvs[0].height * atlas.height != ((Texture2D)texList.trimmedTextures[0]).height) { Debug.LogWarning("WARNING: Not all textures were able to fit on atlas \"" + atlas.name + "\" at their original sizes. These textures were scaled down to fit. To resolve this, assign some of your sprites to use a different material, or if possible, use a larger maximum texture size."); Debug.LogWarning("WARNING: Textures were resized to fit onto atlas \"" + atlas.name + "\"! To resolve this, assign some of your sprites a different material, or if possible, use a larger maximum texture size."); float scaleRateX = texList.uvs[0].width * atlas.width / ((Texture2D)texList.trimmedTextures[0]).width; float scaleRateY = texList.uvs[0].height * atlas.height /((Texture2D)texList.trimmedTextures[0]).height; float scaleRate = scaleRateX <= scaleRateY ? scaleRateX : scaleRateY; while (true) { Debug.Log("Scale Rate = " + scaleRate); Texture2D[] texArray = (Texture2D[])texList.trimmedTextures.ToArray(typeof(Texture2D)); Texture2D[] newArray = new Texture2D[texArray.Length]; for (int i = 0; i < newArray.Length; i++) { newArray[i] = ScaleTextureBilinear(texArray[i], scaleRate); } texList.uvs = atlas.PackTextures(newArray, padding, maxAtlasSize); if (texList.uvs[0].width * atlas.width == newArray[0].width && texList.uvs[0].height * atlas.height == newArray[0].height) { break; } scaleRate *= 0.9f; } }