diff --git a/AssetStudio/Classes/Texture2D.cs b/AssetStudio/Classes/Texture2D.cs index dc8a232..9e65583 100644 --- a/AssetStudio/Classes/Texture2D.cs +++ b/AssetStudio/Classes/Texture2D.cs @@ -14,12 +14,13 @@ namespace AssetStudio public int m_MipCount; public GLTextureSettings m_TextureSettings; public int m_ImageCount; + public byte[] m_PlatformBlob; public ResourceReader image_data; public StreamingInfo m_StreamData; public Texture2D() { } - public Texture2D(Texture2DArray m_Texture2DArray, int layer) + public Texture2D(Texture2DArray m_Texture2DArray, int layer) // Texture2DArrayImage { reader = m_Texture2DArray.reader; assetsFile = m_Texture2DArray.assetsFile; @@ -63,6 +64,7 @@ namespace AssetStudio m_ImageCount = parsedTex2d.m_ImageCount; m_TextureSettings = parsedTex2d.m_TextureSettings; m_StreamData = parsedTex2d.m_StreamData; + m_PlatformBlob = parsedTex2d.m_PlatformBlob ?? Array.Empty(); image_data = !string.IsNullOrEmpty(m_StreamData?.path) ? new ResourceReader(m_StreamData.path, assetsFile, m_StreamData.offset, m_StreamData.size) @@ -141,9 +143,13 @@ namespace AssetStudio } if (version[0] > 2020 || (version[0] == 2020 && version[1] >= 2)) //2020.2 and up { - var m_PlatformBlob = reader.ReadUInt8Array(); + m_PlatformBlob = reader.ReadUInt8Array(); reader.AlignStream(); } + else + { + m_PlatformBlob = Array.Empty(); + } var image_data_size = reader.ReadInt32(); if (image_data_size == 0 && ((version[0] == 5 && version[1] >= 3) || version[0] > 5))//5.3.0 and up { diff --git a/AssetStudioGUI/AssetStudioGUIForm.cs b/AssetStudioGUI/AssetStudioGUIForm.cs index 7951089..e1eed29 100644 --- a/AssetStudioGUI/AssetStudioGUIForm.cs +++ b/AssetStudioGUI/AssetStudioGUIForm.cs @@ -932,6 +932,8 @@ namespace AssetStudioGUI } } } + var switchSwizzled = m_Texture2D.m_PlatformBlob.Length != 0; + assetItem.InfoText += assetItem.Asset.platform == BuildTarget.Switch ? $"\nUses texture swizzling: {switchSwizzled}" : ""; PreviewTexture(bitmap); StatusStripUpdate("'Ctrl'+'R'/'G'/'B'/'A' for Channel Toggle"); diff --git a/AssetStudioUtility/Texture2DConverter.cs b/AssetStudioUtility/Texture2DConverter.cs index 89a6528..20c4bfa 100644 --- a/AssetStudioUtility/Texture2DConverter.cs +++ b/AssetStudioUtility/Texture2DConverter.cs @@ -9,20 +9,74 @@ namespace AssetStudio private ResourceReader reader; private int m_Width; private int m_Height; + private int m_WidthCrop; + private int m_HeightCrop; private TextureFormat m_TextureFormat; + private byte[] m_PlatformBlob; private int[] version; private BuildTarget platform; - public int outPutSize; + private int outPutDataSize; + + private bool switchSwizzled; + private int gobsPerBlock; + private SixLabors.ImageSharp.Size blockSize; + + public int OutputDataSize => outPutDataSize; + public bool UsesSwitchSwizzle => switchSwizzled; public Texture2DConverter(Texture2D m_Texture2D) { reader = m_Texture2D.image_data; - m_Width = m_Texture2D.m_Width; - m_Height = m_Texture2D.m_Height; + m_WidthCrop = m_Texture2D.m_Width; + m_HeightCrop = m_Texture2D.m_Height; m_TextureFormat = m_Texture2D.m_TextureFormat; + m_PlatformBlob = m_Texture2D.m_PlatformBlob; version = m_Texture2D.version; platform = m_Texture2D.platform; - outPutSize = m_Width * m_Height * 4; + // not guaranteed, you can have a swizzled texture without m_PlatformBlob + // but officially, I don't think this can happen. maybe check which engine + // version this started happening in... + switchSwizzled = platform == BuildTarget.Switch && m_PlatformBlob.Length != 0; + if (switchSwizzled) + { + SetupSwitchSwizzle(); + } + else + { + m_Width = m_WidthCrop; + m_Height = m_HeightCrop; + } + outPutDataSize = m_Width * m_Height * 4; + } + + private void SetupSwitchSwizzle() + { + //apparently there is another value to worry about, but seeing as it's + //always 0 and I have nothing else to test against, this will probably + //work fine for now + gobsPerBlock = 1 << BitConverter.ToInt32(m_PlatformBlob, 8); + + //in older versions of unity, rgb24 has a platformBlob which shouldn't + //be possible. it turns out in this case, the image is just rgba32. + //probably shouldn't be modifying the texture2d here, but eh, who cares + if (m_TextureFormat == TextureFormat.RGB24) + { + m_TextureFormat = TextureFormat.RGBA32; + } + else if (m_TextureFormat == TextureFormat.BGR24) + { + m_TextureFormat = TextureFormat.BGRA32; + } + + blockSize = Texture2DSwitchDeswizzler.GetTextureFormatBlockSize(m_TextureFormat); + var realSize = Texture2DSwitchDeswizzler.GetPaddedTextureSize(m_WidthCrop, m_HeightCrop, blockSize.Width, blockSize.Height, gobsPerBlock); + m_Width = realSize.Width; + m_Height = realSize.Height; + } + + public SixLabors.ImageSharp.Size GetUncroppedSize() + { + return new SixLabors.ImageSharp.Size(m_Width, m_Height); } public bool DecodeTexture2D(byte[] bytes) @@ -36,6 +90,11 @@ namespace AssetStudio try { reader.GetData(buff); + if (switchSwizzled) + { + buff = Texture2DSwitchDeswizzler.Unswizzle(buff, GetUncroppedSize(), blockSize, gobsPerBlock); + } + switch (m_TextureFormat) { case TextureFormat.Alpha8: //test pass @@ -275,7 +334,7 @@ namespace AssetStudio private bool DecodeRGBA32(byte[] image_data, byte[] buff) { - for (var i = 0; i < outPutSize; i += 4) + for (var i = 0; i < outPutDataSize; i += 4) { buff[i] = image_data[i + 2]; buff[i + 1] = image_data[i + 1]; @@ -287,7 +346,7 @@ namespace AssetStudio private bool DecodeARGB32(byte[] image_data, byte[] buff) { - for (var i = 0; i < outPutSize; i += 4) + for (var i = 0; i < outPutDataSize; i += 4) { buff[i] = image_data[i + 3]; buff[i + 1] = image_data[i + 2]; @@ -354,7 +413,7 @@ namespace AssetStudio private bool DecodeBGRA32(byte[] image_data, byte[] buff) { - for (var i = 0; i < outPutSize; i += 4) + for (var i = 0; i < outPutDataSize; i += 4) { buff[i] = image_data[i]; buff[i + 1] = image_data[i + 1]; @@ -366,7 +425,7 @@ namespace AssetStudio private bool DecodeRHalf(byte[] image_data, byte[] buff) { - for (var i = 0; i < outPutSize; i += 4) + for (var i = 0; i < outPutDataSize; i += 4) { buff[i] = 0; buff[i + 1] = 0; @@ -378,7 +437,7 @@ namespace AssetStudio private bool DecodeRGHalf(byte[] image_data, byte[] buff) { - for (var i = 0; i < outPutSize; i += 4) + for (var i = 0; i < outPutDataSize; i += 4) { buff[i] = 0; buff[i + 1] = (byte)Math.Round(Half.ToHalf(image_data, i + 2) * 255f); @@ -390,7 +449,7 @@ namespace AssetStudio private bool DecodeRGBAHalf(byte[] image_data, byte[] buff) { - for (var i = 0; i < outPutSize; i += 4) + for (var i = 0; i < outPutDataSize; i += 4) { buff[i] = (byte)Math.Round(Half.ToHalf(image_data, i * 2 + 4) * 255f); buff[i + 1] = (byte)Math.Round(Half.ToHalf(image_data, i * 2 + 2) * 255f); @@ -402,7 +461,7 @@ namespace AssetStudio private bool DecodeRFloat(byte[] image_data, byte[] buff) { - for (var i = 0; i < outPutSize; i += 4) + for (var i = 0; i < outPutDataSize; i += 4) { buff[i] = 0; buff[i + 1] = 0; @@ -414,7 +473,7 @@ namespace AssetStudio private bool DecodeRGFloat(byte[] image_data, byte[] buff) { - for (var i = 0; i < outPutSize; i += 4) + for (var i = 0; i < outPutDataSize; i += 4) { buff[i] = 0; buff[i + 1] = (byte)Math.Round(BitConverter.ToSingle(image_data, i * 2 + 4) * 255f); @@ -426,7 +485,7 @@ namespace AssetStudio private bool DecodeRGBAFloat(byte[] image_data, byte[] buff) { - for (var i = 0; i < outPutSize; i += 4) + for (var i = 0; i < outPutDataSize; i += 4) { buff[i] = (byte)Math.Round(BitConverter.ToSingle(image_data, i * 4 + 8) * 255f); buff[i + 1] = (byte)Math.Round(BitConverter.ToSingle(image_data, i * 4 + 4) * 255f); @@ -474,7 +533,7 @@ namespace AssetStudio private bool DecodeRGB9e5Float(byte[] image_data, byte[] buff) { - for (var i = 0; i < outPutSize; i += 4) + for (var i = 0; i < outPutDataSize; i += 4) { var n = BitConverter.ToInt32(image_data, i); var scale = n >> 27 & 0x1f; @@ -652,7 +711,7 @@ namespace AssetStudio private bool DecodeRG32(byte[] image_data, byte[] buff) { - for (var i = 0; i < outPutSize; i += 4) + for (var i = 0; i < outPutDataSize; i += 4) { buff[i] = 0; //b buff[i + 1] = DownScaleFrom16BitTo8Bit(BitConverter.ToUInt16(image_data, i + 2)); //g @@ -677,7 +736,7 @@ namespace AssetStudio private bool DecodeRGBA64(byte[] image_data, byte[] buff) { - for (var i = 0; i < outPutSize; i += 4) + for (var i = 0; i < outPutDataSize; i += 4) { buff[i] = DownScaleFrom16BitTo8Bit(BitConverter.ToUInt16(image_data, i * 2 + 4)); //b buff[i + 1] = DownScaleFrom16BitTo8Bit(BitConverter.ToUInt16(image_data, i * 2 + 2)); //g diff --git a/AssetStudioUtility/Texture2DExtensions.cs b/AssetStudioUtility/Texture2DExtensions.cs index 25f7c8a..0bf338c 100644 --- a/AssetStudioUtility/Texture2DExtensions.cs +++ b/AssetStudioUtility/Texture2DExtensions.cs @@ -10,19 +10,29 @@ namespace AssetStudio public static Image ConvertToImage(this Texture2D m_Texture2D, bool flip) { var converter = new Texture2DConverter(m_Texture2D); - var buff = BigArrayPool.Shared.Rent(converter.outPutSize); + var uncroppedSize = converter.GetUncroppedSize(); + var buff = BigArrayPool.Shared.Rent(converter.OutputDataSize); try { - if (converter.DecodeTexture2D(buff)) + if (!converter.DecodeTexture2D(buff)) + return null; + + Image image; + if (converter.UsesSwitchSwizzle) { - var image = Image.LoadPixelData(buff, m_Texture2D.m_Width, m_Texture2D.m_Height); - if (flip) - { - image.Mutate(x => x.Flip(FlipMode.Vertical)); - } - return image; + image = Image.LoadPixelData(buff, uncroppedSize.Width, uncroppedSize.Height); + image.Mutate(x => x.Crop(m_Texture2D.m_Width, m_Texture2D.m_Height)); } - return null; + else + { + image = Image.LoadPixelData(buff, m_Texture2D.m_Width, m_Texture2D.m_Height); + } + + if (flip) + { + image.Mutate(x => x.Flip(FlipMode.Vertical)); + } + return image; } finally { diff --git a/AssetStudioUtility/Texture2DSwitchDeswizzler.cs b/AssetStudioUtility/Texture2DSwitchDeswizzler.cs new file mode 100644 index 0000000..07f2767 --- /dev/null +++ b/AssetStudioUtility/Texture2DSwitchDeswizzler.cs @@ -0,0 +1,129 @@ +// https://github.com/nesrak1/AssetStudio/tree/switch-tex-deswizzle + +using SixLabors.ImageSharp; +using System; + +namespace AssetStudio +{ + public class Texture2DSwitchDeswizzler + { + // referring to block here as a compressed texture block, not a gob one + const int GOB_X_TEXEL_COUNT = 4; + const int GOB_Y_TEXEL_COUNT = 8; + const int TEXEL_BYTE_SIZE = 16; + const int BLOCKS_IN_GOB = GOB_X_TEXEL_COUNT * GOB_Y_TEXEL_COUNT; + static readonly int[] GOB_X_POSES = { + 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 2, 2, 3, 3, 2, 2, 3, 3, 2, 2, 3, 3, 2, 2, 3, 3 + }; + static readonly int[] GOB_Y_POSES = { + 0, 1, 0, 1, 2, 3, 2, 3, 4, 5, 4, 5, 6, 7, 6, 7, 0, 1, 0, 1, 2, 3, 2, 3, 4, 5, 4, 5, 6, 7, 6, 7 + }; + + /* + sector: + A + B + + gob (made of sectors): + ABIJ + CDKL + EFMN + GHOP + + gob blocks (example with height 2): + ACEGIK... from left to right of image + BDFHJL... + --------- start new row of blocks + MOQSUW... + NPRTVX... + */ + + private static int CeilDivide(int a, int b) + { + return (a + b - 1) / b; + } + + internal static byte[] Unswizzle(byte[] data, Size imageSize, Size blockSize, int gobsPerBlock) + { + byte[] newData = new byte[data.Length]; + + int width = imageSize.Width; + int height = imageSize.Height; + + int blockCountX = CeilDivide(width, blockSize.Width); + int blockCountY = CeilDivide(height, blockSize.Height); + + int gobCountX = blockCountX / GOB_X_TEXEL_COUNT; + int gobCountY = blockCountY / GOB_Y_TEXEL_COUNT; + + int srcPos = 0; + for (int i = 0; i < gobCountY / gobsPerBlock; i++) + { + for (int j = 0; j < gobCountX; j++) + { + for (int k = 0; k < gobsPerBlock; k++) + { + for (int l = 0; l < BLOCKS_IN_GOB; l++) + { + int gobX = GOB_X_POSES[l]; + int gobY = GOB_Y_POSES[l]; + int gobDstX = j * GOB_X_TEXEL_COUNT + gobX; + int gobDstY = (i * gobsPerBlock + k) * GOB_Y_TEXEL_COUNT + gobY; + int gobDstLinPos = gobDstY * blockCountX * TEXEL_BYTE_SIZE + gobDstX * TEXEL_BYTE_SIZE; + + Array.Copy(data, srcPos, newData, gobDstLinPos, TEXEL_BYTE_SIZE); + + srcPos += TEXEL_BYTE_SIZE; + } + } + } + } + return newData; + } + + //this should be the amount of pixels that can fit 16 bytes + internal static Size GetTextureFormatBlockSize(TextureFormat m_TextureFormat) + { + switch (m_TextureFormat) + { + case TextureFormat.Alpha8: return new Size(16, 1); // 1 byte per pixel + case TextureFormat.ARGB4444: return new Size(8, 1); // 2 bytes per pixel + case TextureFormat.RGBA32: return new Size(4, 1); // 4 bytes per pixel + case TextureFormat.ARGB32: return new Size(4, 1); // 4 bytes per pixel + case TextureFormat.ARGBFloat: return new Size(1, 1); // 16 bytes per pixel (?) + case TextureFormat.RGB565: return new Size(8, 1); // 2 bytes per pixel + case TextureFormat.R16: return new Size(8, 1); // 2 bytes per pixel + case TextureFormat.DXT1: return new Size(8, 4); // 8 bytes per 4x4=16 pixels + case TextureFormat.DXT5: return new Size(4, 4); // 16 bytes per 4x4=16 pixels + case TextureFormat.RGBA4444: return new Size(8, 1); // 2 bytes per pixel + case TextureFormat.BGRA32: return new Size(4, 1); // 4 bytes per pixel + case TextureFormat.BC6H: return new Size(4, 4); // 16 bytes per 4x4=16 pixels + case TextureFormat.BC7: return new Size(4, 4); // 16 bytes per 4x4=16 pixels + case TextureFormat.BC4: return new Size(8, 4); // 8 bytes per 4x4=16 pixels + case TextureFormat.BC5: return new Size(4, 4); // 16 bytes per 4x4=16 pixels + case TextureFormat.ASTC_RGB_4x4: return new Size(4, 4); // 16 bytes per 4x4=16 pixels + case TextureFormat.ASTC_RGB_5x5: return new Size(5, 5); // 16 bytes per 5x5=25 pixels + case TextureFormat.ASTC_RGB_6x6: return new Size(6, 6); // 16 bytes per 6x6=36 pixels + case TextureFormat.ASTC_RGB_8x8: return new Size(8, 8); // 16 bytes per 8x8=64 pixels + case TextureFormat.ASTC_RGB_10x10: return new Size(10, 10); // 16 bytes per 10x10=100 pixels + case TextureFormat.ASTC_RGB_12x12: return new Size(12, 12); // 16 bytes per 12x12=144 pixels + case TextureFormat.ASTC_RGBA_4x4: return new Size(4, 4); // 16 bytes per 4x4=16 pixels + case TextureFormat.ASTC_RGBA_5x5: return new Size(5, 5); // 16 bytes per 5x5=25 pixels + case TextureFormat.ASTC_RGBA_6x6: return new Size(6, 6); // 16 bytes per 6x6=36 pixels + case TextureFormat.ASTC_RGBA_8x8: return new Size(8, 8); // 16 bytes per 8x8=64 pixels + case TextureFormat.ASTC_RGBA_10x10: return new Size(10, 10); // 16 bytes per 10x10=100 pixels + case TextureFormat.ASTC_RGBA_12x12: return new Size(12, 12); // 16 bytes per 12x12=144 pixels + case TextureFormat.RG16: return new Size(8, 1); // 2 bytes per pixel + case TextureFormat.R8: return new Size(16, 1); // 1 byte per pixel + default: throw new NotImplementedException(); + }; + } + + internal static Size GetPaddedTextureSize(int width, int height, int blockWidth, int blockHeight, int gobsPerBlock) + { + width = CeilDivide(width, blockWidth * GOB_X_TEXEL_COUNT) * blockWidth * GOB_X_TEXEL_COUNT; + height = CeilDivide(height, blockHeight * GOB_Y_TEXEL_COUNT * gobsPerBlock) * blockHeight * GOB_Y_TEXEL_COUNT * gobsPerBlock; + return new Size(width, height); + } + } +}