Add support for swizzled Switch textures

Co-Authored-By: nesrak1 <12544505+nesrak1@users.noreply.github.com>
This commit is contained in:
VaDiM 2024-03-09 02:07:14 +03:00
parent 4cd246592b
commit ae4548f1c3
5 changed files with 233 additions and 27 deletions

View File

@ -14,12 +14,13 @@ namespace AssetStudio
public int m_MipCount; public int m_MipCount;
public GLTextureSettings m_TextureSettings; public GLTextureSettings m_TextureSettings;
public int m_ImageCount; public int m_ImageCount;
public byte[] m_PlatformBlob;
public ResourceReader image_data; public ResourceReader image_data;
public StreamingInfo m_StreamData; public StreamingInfo m_StreamData;
public Texture2D() { } public Texture2D() { }
public Texture2D(Texture2DArray m_Texture2DArray, int layer) public Texture2D(Texture2DArray m_Texture2DArray, int layer) // Texture2DArrayImage
{ {
reader = m_Texture2DArray.reader; reader = m_Texture2DArray.reader;
assetsFile = m_Texture2DArray.assetsFile; assetsFile = m_Texture2DArray.assetsFile;
@ -63,6 +64,7 @@ namespace AssetStudio
m_ImageCount = parsedTex2d.m_ImageCount; m_ImageCount = parsedTex2d.m_ImageCount;
m_TextureSettings = parsedTex2d.m_TextureSettings; m_TextureSettings = parsedTex2d.m_TextureSettings;
m_StreamData = parsedTex2d.m_StreamData; m_StreamData = parsedTex2d.m_StreamData;
m_PlatformBlob = parsedTex2d.m_PlatformBlob ?? Array.Empty<byte>();
image_data = !string.IsNullOrEmpty(m_StreamData?.path) image_data = !string.IsNullOrEmpty(m_StreamData?.path)
? new ResourceReader(m_StreamData.path, assetsFile, m_StreamData.offset, m_StreamData.size) ? 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 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(); reader.AlignStream();
} }
else
{
m_PlatformBlob = Array.Empty<byte>();
}
var image_data_size = reader.ReadInt32(); 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 if (image_data_size == 0 && ((version[0] == 5 && version[1] >= 3) || version[0] > 5))//5.3.0 and up
{ {

View File

@ -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); PreviewTexture(bitmap);
StatusStripUpdate("'Ctrl'+'R'/'G'/'B'/'A' for Channel Toggle"); StatusStripUpdate("'Ctrl'+'R'/'G'/'B'/'A' for Channel Toggle");

View File

@ -9,20 +9,74 @@ namespace AssetStudio
private ResourceReader reader; private ResourceReader reader;
private int m_Width; private int m_Width;
private int m_Height; private int m_Height;
private int m_WidthCrop;
private int m_HeightCrop;
private TextureFormat m_TextureFormat; private TextureFormat m_TextureFormat;
private byte[] m_PlatformBlob;
private int[] version; private int[] version;
private BuildTarget platform; 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) public Texture2DConverter(Texture2D m_Texture2D)
{ {
reader = m_Texture2D.image_data; reader = m_Texture2D.image_data;
m_Width = m_Texture2D.m_Width; m_WidthCrop = m_Texture2D.m_Width;
m_Height = m_Texture2D.m_Height; m_HeightCrop = m_Texture2D.m_Height;
m_TextureFormat = m_Texture2D.m_TextureFormat; m_TextureFormat = m_Texture2D.m_TextureFormat;
m_PlatformBlob = m_Texture2D.m_PlatformBlob;
version = m_Texture2D.version; version = m_Texture2D.version;
platform = m_Texture2D.platform; 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) public bool DecodeTexture2D(byte[] bytes)
@ -36,6 +90,11 @@ namespace AssetStudio
try try
{ {
reader.GetData(buff); reader.GetData(buff);
if (switchSwizzled)
{
buff = Texture2DSwitchDeswizzler.Unswizzle(buff, GetUncroppedSize(), blockSize, gobsPerBlock);
}
switch (m_TextureFormat) switch (m_TextureFormat)
{ {
case TextureFormat.Alpha8: //test pass case TextureFormat.Alpha8: //test pass
@ -275,7 +334,7 @@ namespace AssetStudio
private bool DecodeRGBA32(byte[] image_data, byte[] buff) 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] = image_data[i + 2];
buff[i + 1] = image_data[i + 1]; buff[i + 1] = image_data[i + 1];
@ -287,7 +346,7 @@ namespace AssetStudio
private bool DecodeARGB32(byte[] image_data, byte[] buff) 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] = image_data[i + 3];
buff[i + 1] = image_data[i + 2]; buff[i + 1] = image_data[i + 2];
@ -354,7 +413,7 @@ namespace AssetStudio
private bool DecodeBGRA32(byte[] image_data, byte[] buff) 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] = image_data[i];
buff[i + 1] = image_data[i + 1]; buff[i + 1] = image_data[i + 1];
@ -366,7 +425,7 @@ namespace AssetStudio
private bool DecodeRHalf(byte[] image_data, byte[] buff) 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] = 0;
buff[i + 1] = 0; buff[i + 1] = 0;
@ -378,7 +437,7 @@ namespace AssetStudio
private bool DecodeRGHalf(byte[] image_data, byte[] buff) 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] = 0;
buff[i + 1] = (byte)Math.Round(Half.ToHalf(image_data, i + 2) * 255f); 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) 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] = (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); 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) 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] = 0;
buff[i + 1] = 0; buff[i + 1] = 0;
@ -414,7 +473,7 @@ namespace AssetStudio
private bool DecodeRGFloat(byte[] image_data, byte[] buff) 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] = 0;
buff[i + 1] = (byte)Math.Round(BitConverter.ToSingle(image_data, i * 2 + 4) * 255f); 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) 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] = (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); 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) 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 n = BitConverter.ToInt32(image_data, i);
var scale = n >> 27 & 0x1f; var scale = n >> 27 & 0x1f;
@ -652,7 +711,7 @@ namespace AssetStudio
private bool DecodeRG32(byte[] image_data, byte[] buff) 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] = 0; //b
buff[i + 1] = DownScaleFrom16BitTo8Bit(BitConverter.ToUInt16(image_data, i + 2)); //g 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) 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] = DownScaleFrom16BitTo8Bit(BitConverter.ToUInt16(image_data, i * 2 + 4)); //b
buff[i + 1] = DownScaleFrom16BitTo8Bit(BitConverter.ToUInt16(image_data, i * 2 + 2)); //g buff[i + 1] = DownScaleFrom16BitTo8Bit(BitConverter.ToUInt16(image_data, i * 2 + 2)); //g

View File

@ -10,19 +10,29 @@ namespace AssetStudio
public static Image<Bgra32> ConvertToImage(this Texture2D m_Texture2D, bool flip) public static Image<Bgra32> ConvertToImage(this Texture2D m_Texture2D, bool flip)
{ {
var converter = new Texture2DConverter(m_Texture2D); var converter = new Texture2DConverter(m_Texture2D);
var buff = BigArrayPool<byte>.Shared.Rent(converter.outPutSize); var uncroppedSize = converter.GetUncroppedSize();
var buff = BigArrayPool<byte>.Shared.Rent(converter.OutputDataSize);
try try
{ {
if (converter.DecodeTexture2D(buff)) if (!converter.DecodeTexture2D(buff))
return null;
Image<Bgra32> image;
if (converter.UsesSwitchSwizzle)
{ {
var image = Image.LoadPixelData<Bgra32>(buff, m_Texture2D.m_Width, m_Texture2D.m_Height); image = Image.LoadPixelData<Bgra32>(buff, uncroppedSize.Width, uncroppedSize.Height);
if (flip) image.Mutate(x => x.Crop(m_Texture2D.m_Width, m_Texture2D.m_Height));
{
image.Mutate(x => x.Flip(FlipMode.Vertical));
}
return image;
} }
return null; else
{
image = Image.LoadPixelData<Bgra32>(buff, m_Texture2D.m_Width, m_Texture2D.m_Height);
}
if (flip)
{
image.Mutate(x => x.Flip(FlipMode.Vertical));
}
return image;
} }
finally finally
{ {

View File

@ -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);
}
}
}