using AssetRipper.Export.UnityProjects.Textures; using AssetRipper.Import.Logging; using AssetRipper.SourceGenerated.Classes.ClassID_117; using AssetRipper.SourceGenerated.Classes.ClassID_187; using AssetRipper.SourceGenerated.Classes.ClassID_188; using AssetRipper.SourceGenerated.Classes.ClassID_189; using AssetRipper.SourceGenerated.Classes.ClassID_28; using AssetRipper.SourceGenerated.Classes.ClassID_89; using AssetRipper.SourceGenerated.Enums; using AssetRipper.SourceGenerated.Extensions; using AssetRipper.TextureDecoder.Astc; using AssetRipper.TextureDecoder.Atc; using AssetRipper.TextureDecoder.Bc; using AssetRipper.TextureDecoder.Dxt; using AssetRipper.TextureDecoder.Etc; using AssetRipper.TextureDecoder.Pvrtc; using AssetRipper.TextureDecoder.Rgb; using AssetRipper.TextureDecoder.Rgb.Formats; using AssetRipper.TextureDecoder.Yuy2; using System.Runtime.CompilerServices; namespace AssetRipper.Export.Modules.Textures; public static class TextureConverter { public static bool TryConvertToBitmap(IImageTexture texture, out DirectBitmap bitmap) { return texture switch { ICubemapArray cubemapArray => TryConvertToBitmap(cubemapArray, out bitmap), ITexture2DArray texture2DArray => TryConvertToBitmap(texture2DArray, out bitmap), ITexture3D texture3D => TryConvertToBitmap(texture3D, out bitmap), ITexture2D texture2D => TryConvertToBitmap(texture2D, out bitmap), _ => ReturnFalse(out bitmap), }; static bool ReturnFalse(out DirectBitmap bitmap) { bitmap = DirectBitmap.Empty; return false; } } public static bool TryConvertToBitmap(ITexture3D texture, out DirectBitmap bitmap) { byte[] buffer = texture.GetImageData(); if (buffer.Length == 0) { bitmap = DirectBitmap.Empty; return false; } if (!TryGetTextureFormat((GraphicsFormat)texture.Format, out TextureFormat format)) { bitmap = DirectBitmap.Empty; return false; } if (!TryConvertToBitmap( format, texture.Width, texture.Height, texture.Depth, texture.GetCompleteImageSize(), texture.Collection.Version, buffer, out bitmap)) { return false; } bitmap.FlipY(); // despite the name, this packing works for different formats if (texture.LightmapFormatE == TextureUsageMode.NormalmapDXT5nm) { UnpackNormal(bitmap.Bits); } return true; } public static bool TryConvertToBitmap(ITexture2DArray texture, out DirectBitmap bitmap) { byte[] buffer = texture.GetImageData(); if (buffer.Length == 0) { bitmap = DirectBitmap.Empty; return false; } if (!TryGetTextureFormat((GraphicsFormat)texture.Format, out TextureFormat format)) { bitmap = DirectBitmap.Empty; return false; } if (!TryConvertToBitmap( format, texture.Width, texture.Height, texture.Depth, texture.GetCompleteImageSize(), texture.Collection.Version, buffer, out bitmap)) { return false; } bitmap.FlipY(); return true; } public static bool TryConvertToBitmap(ICubemapArray texture, out DirectBitmap bitmap) { byte[] buffer = texture.GetImageData(); if (buffer.Length == 0) { bitmap = DirectBitmap.Empty; return false; } if (!TryGetTextureFormat((GraphicsFormat)texture.Format, out TextureFormat format)) { bitmap = DirectBitmap.Empty; return false; } if (!TryConvertToBitmap( format, texture.Width, texture.GetHeight(), texture.GetDepth(), texture.GetCompleteImageSize(), texture.Collection.Version, buffer, out bitmap)) { return false; } bitmap.FlipY();// Maybe not needed? return true; } public static bool TryConvertToBitmap(ITexture2D texture, out DirectBitmap bitmap) { byte[] buffer = texture.GetImageData(); if (buffer.Length == 0) { bitmap = DirectBitmap.Empty; return false; } if (!TryConvertToBitmap( texture.Format_C28E, texture.Width_C28, texture.Height_C28, texture.ImageCount_C28, texture.ActualImageSize, texture.Collection.Version, buffer, out bitmap)) { return false; } // cubemaps dont need flipping, for some reason if (texture is not ICubemap) { bitmap.FlipY(); } // despite the name, this packing works for different formats if (texture.LightmapFormat_C28E == TextureUsageMode.NormalmapDXT5nm) { UnpackNormal(bitmap.Bits); } return true; } private static bool TryConvertToBitmap( TextureFormat textureFormat, int width, int height, int depth, int imageSize, UnityVersion version, byte[] data, out DirectBitmap bitmap) { return textureFormat switch { TextureFormat.Alpha8 => TryConvertToBitmap, byte>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.ARGB4444 => TryConvertToBitmap(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.RGB24 => TryConvertToBitmap, byte>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.RGBA32 => TryConvertToBitmap, byte>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.ARGB32 => TryConvertToBitmap, byte>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.ARGBFloat => TryConvertToBitmap, float>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.RGB565 => TryConvertToBitmap(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.BGR24 => TryConvertToBitmap, byte>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.R16 => TryConvertToBitmap, ushort>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.RGBA4444 => TryConvertToBitmap(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.BGRA32_14 or TextureFormat.BGRA32_37 => TryConvertToBitmap, byte>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.RHalf => TryConvertToBitmap, Half>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.RGHalf => TryConvertToBitmap, Half>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.RGBAHalf => TryConvertToBitmap, Half>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.RFloat => TryConvertToBitmap, float>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.RGFloat => TryConvertToBitmap, float>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.RGBAFloat => TryConvertToBitmap, float>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.RGB9e5Float => TryConvertToBitmap(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.RG16 => TryConvertToBitmap, byte>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.R8 => TryConvertToBitmap, byte>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.RG32 => TryConvertToBitmap, ushort>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.RGB48 => TryConvertToBitmap, ushort>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.RGBA64 => TryConvertToBitmap, ushort>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.R8_SIGNED => TryConvertToBitmap, sbyte>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.RG16_SIGNED => TryConvertToBitmap, sbyte>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.RGB24_SIGNED => TryConvertToBitmap, sbyte>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.RGBA32_SIGNED => TryConvertToBitmap, sbyte>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.R16_SIGNED => TryConvertToBitmap, short>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.RG32_SIGNED => TryConvertToBitmap, short>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.RGB48_SIGNED => TryConvertToBitmap, short>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), TextureFormat.RGBA64_SIGNED => TryConvertToBitmap, short>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), _ => TryConvertToBitmap, byte>(textureFormat, width, height, depth, imageSize, version, data, out bitmap), }; } private static bool TryConvertToBitmap( TextureFormat textureFormat, int width, int height, int depth, int imageSize, UnityVersion version, byte[] data, out DirectBitmap bitmap) where TColor : unmanaged, IColor where TChannelValue : unmanaged { if (width <= 0 || height <= 0 || depth <= 0) { Logger.Log(LogType.Error, LogCategory.Export, $"Invalid texture dimensions. Width: {width}, Height: {height}, Depth: {depth}."); bitmap = DirectBitmap.Empty; return false; } if (1L * width * height * depth * Unsafe.SizeOf() > int.MaxValue) { Logger.Log(LogType.Error, LogCategory.Export, $"Texture size is too large. Width: {width}, Height: {height}, Depth: {depth}."); bitmap = DirectBitmap.Empty; return false; } ReadOnlySpan uncompressedSpan; int bytesPerLayer; if (textureFormat.IsCrunched()) { if (CrunchHandler.DecompressCrunch(textureFormat, version, data, out byte[]? decompressedData)) { uncompressedSpan = decompressedData; bytesPerLayer = decompressedData.Length / depth; } else { bitmap = DirectBitmap.Empty; return false; } } else { if (data.Length == imageSize) { // This can happen for Texture3D // For mips, all 3 dimensions get halved with each mip level, unlike Texture2DArray. // https://github.com/AssetRipper/AssetRipper/issues/1886 bytesPerLayer = -1; } else if (data.Length < (long)imageSize * depth) { Logger.Log(LogType.Error, LogCategory.Export, $"Image data length {data.Length} is less than expected {(long)imageSize * depth}. Width: {width}, Height: {height}, Depth: {depth}, Image Size: {imageSize}, Format {textureFormat}."); bitmap = DirectBitmap.Empty; return false; } else { bytesPerLayer = imageSize; } uncompressedSpan = data; } bitmap = new DirectBitmap(width, height, depth); int outputSize = width * height * bitmap.PixelSize; int inputOffset = 0; for (int i = 0; i < depth; i++) { ReadOnlySpan inputSpan = uncompressedSpan.Slice(inputOffset, int.Max(uncompressedSpan.Length - inputOffset, bytesPerLayer)); Span outputSpan = bitmap.Bits.Slice(i * outputSize, outputSize); int bytesRead = DecodeTexture(textureFormat, width, height, inputSpan, outputSpan); if (bytesRead < 0) { bitmap = DirectBitmap.Empty; return false; } inputOffset += bytesPerLayer > 0 ? bytesPerLayer : bytesRead; } return true; } private static int DecodeTexture(TextureFormat textureFormat, int width, int height, ReadOnlySpan inputSpan, Span outputSpan) where TColor : unmanaged, IColor where TChannelValue : unmanaged { switch (textureFormat) { //ASTC case TextureFormat.ASTC_RGB_4x4: case TextureFormat.ASTC_RGBA_4x4: return AstcDecoder.DecodeASTC(inputSpan, width, height, 4, 4, outputSpan); case TextureFormat.ASTC_RGB_5x5: case TextureFormat.ASTC_RGBA_5x5: return AstcDecoder.DecodeASTC(inputSpan, width, height, 5, 5, outputSpan); case TextureFormat.ASTC_RGB_6x6: case TextureFormat.ASTC_RGBA_6x6: return AstcDecoder.DecodeASTC(inputSpan, width, height, 6, 6, outputSpan); case TextureFormat.ASTC_RGB_8x8: case TextureFormat.ASTC_RGBA_8x8: return AstcDecoder.DecodeASTC(inputSpan, width, height, 8, 8, outputSpan); case TextureFormat.ASTC_RGB_10x10: case TextureFormat.ASTC_RGBA_10x10: return AstcDecoder.DecodeASTC(inputSpan, width, height, 10, 10, outputSpan); case TextureFormat.ASTC_RGB_12x12: case TextureFormat.ASTC_RGBA_12x12: return AstcDecoder.DecodeASTC(inputSpan, width, height, 12, 12, outputSpan); //ATC case TextureFormat.ATC_RGB4: return AtcDecoder.DecompressAtcRgb4(inputSpan, width, height, outputSpan); case TextureFormat.ATC_RGBA8: return AtcDecoder.DecompressAtcRgba8(inputSpan, width, height, outputSpan); //BC case TextureFormat.BC4: return Bc4.Decompress(inputSpan, width, height, outputSpan); case TextureFormat.BC5: return Bc5.Decompress(inputSpan, width, height, outputSpan); case TextureFormat.BC6H: return Bc6h.Decompress(inputSpan, width, height, false, outputSpan); case TextureFormat.BC7: return Bc7.Decompress(inputSpan, width, height, outputSpan); //DXT case TextureFormat.DXT1: case TextureFormat.DXT1Crunched: return DxtDecoder.DecompressDXT1(inputSpan, width, height, outputSpan); case TextureFormat.DXT3: return DxtDecoder.DecompressDXT3(inputSpan, width, height, outputSpan); case TextureFormat.DXT5: case TextureFormat.DXT5Crunched: return DxtDecoder.DecompressDXT5(inputSpan, width, height, outputSpan); //ETC case TextureFormat.ETC_RGB4: case TextureFormat.ETC_RGB4_3DS: case TextureFormat.ETC_RGB4Crunched: return EtcDecoder.DecompressETC(inputSpan, width, height, outputSpan); case TextureFormat.EAC_R: return EtcDecoder.DecompressEACRUnsigned(inputSpan, width, height, outputSpan); case TextureFormat.EAC_R_SIGNED: return EtcDecoder.DecompressEACRSigned(inputSpan, width, height, outputSpan); case TextureFormat.EAC_RG: return EtcDecoder.DecompressEACRGUnsigned(inputSpan, width, height, outputSpan); case TextureFormat.EAC_RG_SIGNED: return EtcDecoder.DecompressEACRGSigned(inputSpan, width, height, outputSpan); case TextureFormat.ETC2_RGB: return EtcDecoder.DecompressETC2(inputSpan, width, height, outputSpan); case TextureFormat.ETC2_RGBA1: return EtcDecoder.DecompressETC2A1(inputSpan, width, height, outputSpan); case TextureFormat.ETC2_RGBA8: case TextureFormat.ETC_RGBA8_3DS: case TextureFormat.ETC2_RGBA8Crunched: return EtcDecoder.DecompressETC2A8(inputSpan, width, height, outputSpan); //PVRTC case TextureFormat.PVRTC_RGB2: case TextureFormat.PVRTC_RGBA2: return PvrtcDecoder.DecompressPVRTC(inputSpan, width, height, true, outputSpan); case TextureFormat.PVRTC_RGB4: case TextureFormat.PVRTC_RGBA4: return PvrtcDecoder.DecompressPVRTC(inputSpan, width, height, false, outputSpan); //YUY2 case TextureFormat.YUY2: return Yuy2Decoder.DecompressYUY2(inputSpan, width, height, outputSpan); //RGB case TextureFormat.Alpha8: return RgbConverter.Convert, byte, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.ARGB4444: return RgbConverter.Convert(inputSpan, width, height, outputSpan); case TextureFormat.RGBA4444: return RgbConverter.Convert(inputSpan, width, height, outputSpan); case TextureFormat.RGB565: return RgbConverter.Convert(inputSpan, width, height, outputSpan); case TextureFormat.R8: return RgbConverter.Convert, byte, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.RG16: return RgbConverter.Convert, byte, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.RGB24: return RgbConverter.Convert, byte, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.RGBA32: return RgbConverter.Convert, byte, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.ARGB32: return RgbConverter.Convert, byte, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.ARGBFloat: return RgbConverter.Convert, float, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.BGR24: return RgbConverter.Convert, byte, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.BGRA32_14: case TextureFormat.BGRA32_37: return RgbConverter.Convert, byte, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.R16: return RgbConverter.Convert, ushort, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.RG32: return RgbConverter.Convert, ushort, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.RGB48: return RgbConverter.Convert, ushort, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.RGBA64: return RgbConverter.Convert, ushort, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.RHalf: return RgbConverter.Convert, Half, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.RGHalf: return RgbConverter.Convert, Half, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.RGBAHalf: return RgbConverter.Convert, Half, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.RFloat: return RgbConverter.Convert, float, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.RGFloat: return RgbConverter.Convert, float, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.RGBAFloat: return RgbConverter.Convert, float, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.RGB9e5Float: return RgbConverter.Convert(inputSpan, width, height, outputSpan); case TextureFormat.R8_SIGNED: return RgbConverter.Convert, sbyte, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.RG16_SIGNED: return RgbConverter.Convert, sbyte, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.RGB24_SIGNED: return RgbConverter.Convert, sbyte, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.RGBA32_SIGNED: return RgbConverter.Convert, sbyte, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.R16_SIGNED: return RgbConverter.Convert, short, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.RG32_SIGNED: return RgbConverter.Convert, short, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.RGB48_SIGNED: return RgbConverter.Convert, short, TColor, TChannelValue>(inputSpan, width, height, outputSpan); case TextureFormat.RGBA64_SIGNED: return RgbConverter.Convert, short, TColor, TChannelValue>(inputSpan, width, height, outputSpan); default: Logger.Log(LogType.Error, LogCategory.Export, $"Unsupported texture format '{textureFormat}'"); return -1; } } private static bool TryGetTextureFormat(GraphicsFormat graphicsFormat, out TextureFormat format) { try { format = graphicsFormat.ToTextureFormat(); return true; } catch (NotSupportedException) { format = default; return false; } catch (ArgumentOutOfRangeException) { Logger.Log(LogType.Error, LogCategory.Export, $"Unknown GraphicsFormat '{(int)graphicsFormat}'"); format = default; return false; } } private static void UnpackNormal(Span data) { for (int i = 0; i < data.Length; i += 4) { Span pixelSpan = data.Slice(i, 4); byte r = pixelSpan[3]; byte g = pixelSpan[1]; byte a = pixelSpan[2]; pixelSpan[2] = r; pixelSpan[3] = a; const double MagnitudeSqr = 255 * 255; double vr = r * 2.0 - 255.0; double vg = g * 2.0 - 255.0; double hypotenuseSqr = Math.Min(vr * vr + vg * vg, MagnitudeSqr); double b = (Math.Sqrt(MagnitudeSqr - hypotenuseSqr) + 255.0) / 2.0; pixelSpan[0] = (byte)b; } } }