diff --git a/AssetRipper.TextureDecoder.ColorGenerator/Program.cs b/AssetRipper.TextureDecoder.ColorGenerator/Program.cs index bc469c1..a9d0b6a 100644 --- a/AssetRipper.TextureDecoder.ColorGenerator/Program.cs +++ b/AssetRipper.TextureDecoder.ColorGenerator/Program.cs @@ -90,6 +90,7 @@ internal static class Program ( "ColorRGB16", typeof(byte), true, true, true, false, false ), ( "ColorRGB9e5", typeof(double), true, true, true, false, false ), ( "ColorRGBA16", typeof(byte), true, true, true, true, false ), + ( "ColorRGB32Half", typeof(Half), true, true, true, false, false ), }; static void Main() diff --git a/AssetRipper.TextureDecoder.TestGenerator/Program.cs b/AssetRipper.TextureDecoder.TestGenerator/Program.cs index fefe461..17edfb4 100644 --- a/AssetRipper.TextureDecoder.TestGenerator/Program.cs +++ b/AssetRipper.TextureDecoder.TestGenerator/Program.cs @@ -34,6 +34,7 @@ internal static class Program new GenerationData(typeof(ColorRGBA16), 2), new GenerationData(typeof(ColorRGBA32), 4), new GenerationData(typeof(ColorRGBA32Signed), 4), + new GenerationData(typeof(ColorRGB32Half), 4), new GenerationData(typeof(ColorRGBA64), 8), new GenerationData(typeof(ColorRGBA64Signed), 8), new GenerationData(typeof(ColorRGBAHalf), 8), @@ -60,7 +61,7 @@ internal static class Program static void Main() { Directory.CreateDirectory(OutputFolder); - + foreach (GenerationData data in dataList) { using MemoryStream memoryStream = new(); @@ -300,7 +301,7 @@ private static void WriteLosslessConversionTest(this IndentedTextWriter textWrit textWriter.WriteLine($"{containingName} converted = original.{nameof(ColorExtensions.Convert)}<{currentName}, {currentData.ChannelTypeName}, {containingName}, {containingData.ChannelTypeName}>();"); textWriter.WriteLine($"{currentName} convertedBack = converted.{nameof(ColorExtensions.Convert)}<{containingName}, {containingData.ChannelTypeName}, {currentName}, {currentData.ChannelTypeName}>();"); textWriter.WriteLine("Assert.That(convertedBack, Is.EqualTo(original));"); - + textWriter.Indent -= 1; textWriter.WriteLine("}"); } diff --git a/AssetRipper.TextureDecoder.Tests/Formats/ColorRGB32HalfTests.g.cs b/AssetRipper.TextureDecoder.Tests/Formats/ColorRGB32HalfTests.g.cs new file mode 100644 index 0000000..78bc7b6 --- /dev/null +++ b/AssetRipper.TextureDecoder.Tests/Formats/ColorRGB32HalfTests.g.cs @@ -0,0 +1,192 @@ +//This code is source generated. Do not edit manually. + +using AssetRipper.TextureDecoder.Rgb; +using AssetRipper.TextureDecoder.Rgb.Formats; +using System.Runtime.CompilerServices; + +namespace AssetRipper.TextureDecoder.Tests.Formats; + +public partial class ColorRGB32HalfTests +{ + [Test] + public void CorrectSizeTest() + { + Assert.That(Unsafe.SizeOf(), Is.EqualTo(4)); + } + + [Test] + public void PropertyIsSymmetric_R() + { + var color = MakeRandomColor(); + var r = color.R; + color.R = r; + Assert.That(color.R, Is.EqualTo(r)); + } + + [Test] + public void PropertyIsSymmetric_G() + { + var color = MakeRandomColor(); + var g = color.G; + color.G = g; + Assert.That(color.G, Is.EqualTo(g)); + } + + [Test] + public void PropertyIsSymmetric_B() + { + var color = MakeRandomColor(); + var b = color.B; + color.B = b; + Assert.That(color.B, Is.EqualTo(b)); + } + + [Test] + public void PropertyIsSymmetric_A() + { + var color = MakeRandomColor(); + var a = color.A; + color.A = a; + Assert.That(color.A, Is.EqualTo(a)); + } + + [Test] + public void ChannelsAreIndependent_R() + { + var color = MakeRandomColor(); + var g = color.G; + var b = color.B; + var a = color.A; + color.R = (Half)0.333f; + Assert.Multiple(() => + { + Assert.That(color.G, Is.EqualTo(g)); + Assert.That(color.B, Is.EqualTo(b)); + Assert.That(color.A, Is.EqualTo(a)); + }); + } + + [Test] + public void ChannelsAreIndependent_G() + { + var color = MakeRandomColor(); + var r = color.R; + var b = color.B; + var a = color.A; + color.G = (Half)0.333f; + Assert.Multiple(() => + { + Assert.That(color.R, Is.EqualTo(r)); + Assert.That(color.B, Is.EqualTo(b)); + Assert.That(color.A, Is.EqualTo(a)); + }); + } + + [Test] + public void ChannelsAreIndependent_B() + { + var color = MakeRandomColor(); + var r = color.R; + var g = color.G; + var a = color.A; + color.B = (Half)0.333f; + Assert.Multiple(() => + { + Assert.That(color.R, Is.EqualTo(r)); + Assert.That(color.G, Is.EqualTo(g)); + Assert.That(color.A, Is.EqualTo(a)); + }); + } + + [Test] + public void ChannelsAreIndependent_A() + { + var color = MakeRandomColor(); + var r = color.R; + var g = color.G; + var b = color.B; + color.A = (Half)0.333f; + Assert.Multiple(() => + { + Assert.That(color.R, Is.EqualTo(r)); + Assert.That(color.G, Is.EqualTo(g)); + Assert.That(color.B, Is.EqualTo(b)); + }); + } + + [Test] + public void GetMethodMatchesProperties() + { + var color = MakeRandomColor(); + color.GetChannels(out var r, out var g, out var b, out var a); + Assert.Multiple(() => + { + Assert.That(color.R, Is.EqualTo(r)); + Assert.That(color.G, Is.EqualTo(g)); + Assert.That(color.B, Is.EqualTo(b)); + Assert.That(color.A, Is.EqualTo(a)); + }); + } + + [Test] + public void MethodsAreSymmetric() + { + var color = MakeRandomColor(); + color.GetChannels(out var r, out var g, out var b, out var a); + color.SetChannels(r, g, b, a); + Assert.Multiple(() => + { + Assert.That(color.R, Is.EqualTo(r)); + Assert.That(color.G, Is.EqualTo(g)); + Assert.That(color.B, Is.EqualTo(b)); + Assert.That(color.A, Is.EqualTo(a)); + }); + } + + public static ColorRGB32Half MakeRandomColor() + { + return new() + { + R = (Half)0.447f, + G = (Half)0.224f, + B = (Half)0.95f, + A = (Half)0.897f, + }; + } + + [Test] + public void ConversionToColorRGBAHalfIsLossless() + { + ColorRGB32Half original = MakeRandomColor(); + ColorRGBAHalf converted = original.Convert(); + ColorRGB32Half convertedBack = converted.Convert(); + Assert.That(convertedBack, Is.EqualTo(original)); + } + + [Test] + public void ConversionToColorRGBASingleIsLossless() + { + ColorRGB32Half original = MakeRandomColor(); + ColorRGBASingle converted = original.Convert(); + ColorRGB32Half convertedBack = converted.Convert(); + Assert.That(convertedBack, Is.EqualTo(original)); + } + + [Test] + public void ConversionToColorRGBHalfIsLossless() + { + ColorRGB32Half original = MakeRandomColor(); + ColorRGBHalf converted = original.Convert(); + ColorRGB32Half convertedBack = converted.Convert(); + Assert.That(convertedBack, Is.EqualTo(original)); + } + + [Test] + public void ConversionToColorRGBSingleIsLossless() + { + ColorRGB32Half original = MakeRandomColor(); + ColorRGBSingle converted = original.Convert(); + ColorRGB32Half convertedBack = converted.Convert(); + Assert.That(convertedBack, Is.EqualTo(original)); + } +} diff --git a/AssetRipper.TextureDecoder.Tests/RgbTests.cs b/AssetRipper.TextureDecoder.Tests/RgbTests.cs index 283815d..3096cf9 100644 --- a/AssetRipper.TextureDecoder.Tests/RgbTests.cs +++ b/AssetRipper.TextureDecoder.Tests/RgbTests.cs @@ -1,4 +1,7 @@ -namespace AssetRipper.TextureDecoder.Tests +using AssetRipper.TextureDecoder.Rgb; +using AssetRipper.TextureDecoder.Rgb.Formats; + +namespace AssetRipper.TextureDecoder.Tests { public sealed class RgbTests { @@ -211,5 +214,20 @@ public void ConvertRGBA64Test() int bytesRead = Rgb.RgbConverter.RGBA64ToBGRA32(data, 512, 512, out _); Assert.That(bytesRead, Is.EqualTo(data.Length)); } + + [Test] + public void ConvertRGB32HalfTest() + { + ReadOnlySpan data = File.ReadAllBytes(PathConstants.RgbTestFilesFolder + "test.rgb24"); + RgbConverter.Convert(data, 256, 256, out var halfData); + int legacyBytesRead = RgbConverter.R11G11B10FloatToBGRA32(halfData, 256, 256, out var legacyRgbData); + int bytesRead = RgbConverter.Convert(halfData, 256, 256, out var rgbData); + Assert.Multiple(() => + { + Assert.That(bytesRead, Is.EqualTo(halfData.Length)); + Assert.That(legacyBytesRead, Is.EqualTo(halfData.Length)); + Assert.That(rgbData, Is.EqualTo(legacyRgbData)); + }); + } } } \ No newline at end of file diff --git a/AssetRipper.TextureDecoder/Rgb/Formats/ColorRGB32Half.cs b/AssetRipper.TextureDecoder/Rgb/Formats/ColorRGB32Half.cs new file mode 100644 index 0000000..d9b78bc --- /dev/null +++ b/AssetRipper.TextureDecoder/Rgb/Formats/ColorRGB32Half.cs @@ -0,0 +1,64 @@ +namespace AssetRipper.TextureDecoder.Rgb.Formats +{ + /// + /// Also called R11G11B10_FLOAT + /// + /// + /// + /// + public partial struct ColorRGB32Half : IColor + { + private uint bits; + + /// + /// 11 bits + /// + public Half R { + get { + var value = (ushort) ((bits << 4) & 0x7FF0); + return Unsafe.As(ref value); + } + set => bits = (uint) ((bits & ~0x7FF) | ((uint) Unsafe.As(ref value) & 0x7FF0) >> 4); + } + + /// + /// 11 bits + /// + public Half G { + get { + var value = (ushort) ((bits >> 7) & 0x7FF0); + return Unsafe.As(ref value); + } + set => bits = (uint) ((bits & ~0x3FF800) | (((uint) Unsafe.As(ref value) >> 4) & 0x7FF0) << 11); + } + + /// + /// 10 bits + /// + public Half B { + get { + var value = (ushort) ((bits >> 17) & 0x7FE0); + return Unsafe.As(ref value); + } + set => bits = (bits & ~0xFFC00000) | ((((uint) Unsafe.As(ref value) >> 5) & 0x3FF) << 22); + } + + public Half A + { + get => (Half) 1.0f; + set { } + } + + public void GetChannels(out Half r, out Half g, out Half b, out Half a) + { + DefaultColorMethods.GetChannels(this, out r, out g, out b, out a); + } + + public void SetChannels(Half r, Half g, Half b, Half a) + { + bits = (((uint) Unsafe.As(ref r) >> 4) & 0x7FF) | + ((((uint) Unsafe.As(ref g) >> 4) & 0x7FF) << 11) | + ((((uint) Unsafe.As(ref b) >> 5) & 0x3FF) << 22); + } + } +} diff --git a/AssetRipper.TextureDecoder/Rgb/Formats/ColorRGB32Half.g.cs b/AssetRipper.TextureDecoder/Rgb/Formats/ColorRGB32Half.g.cs new file mode 100644 index 0000000..84ac1b3 --- /dev/null +++ b/AssetRipper.TextureDecoder/Rgb/Formats/ColorRGB32Half.g.cs @@ -0,0 +1,11 @@ +//This code is source generated. Do not edit manually. + +using AssetRipper.TextureDecoder.Attributes; + +namespace AssetRipper.TextureDecoder.Rgb.Formats +{ + [RgbaAttribute(RedChannel = true, GreenChannel = true, BlueChannel = true, AlphaChannel = false, FullyUtilizedChannels = false)] + public partial struct ColorRGB32Half : IColor + { + } +} diff --git a/AssetRipper.TextureDecoder/Rgb/RgbConverter.cs b/AssetRipper.TextureDecoder/Rgb/RgbConverter.cs index 6adaa63..cef93c5 100644 --- a/AssetRipper.TextureDecoder/Rgb/RgbConverter.cs +++ b/AssetRipper.TextureDecoder/Rgb/RgbConverter.cs @@ -49,7 +49,7 @@ public static int ARGB16ToBGRA32(ReadOnlySpan input, int width, int height for (int j = 0; j < height; j++) { output[oo + 0] = unchecked((byte)(input[io + 0] << 4)); // b - output[oo + 1] = (byte)(input[io + 0] & 0xF0); // g + output[oo + 1] = (byte)(input[io + 0] & 0xF0); // g output[oo + 2] = unchecked((byte)(input[io + 1] << 4)); // r output[oo + 3] = (byte)(input[io + 1] & 0xF0); // a io += 2; @@ -214,7 +214,7 @@ public static int RGBA16ToBGRA32(ReadOnlySpan input, int width, int height for (int j = 0; j < height; j++) { output[oo + 0] = (byte)(input[io + 0] & 0xF0); // b - output[oo + 1] = unchecked((byte)(input[io + 1] << 4)); // g + output[oo + 1] = unchecked((byte)(input[io + 1] << 4)); // g output[oo + 2] = (byte)(input[io + 1] & 0xF0); // r output[oo + 3] = unchecked((byte)(input[io + 0] << 4)); // a io += 2; @@ -486,6 +486,38 @@ public static int RGB9e5FloatToBGRA32(ReadOnlySpan input, int width, int h return io; } + [MethodImpl(OptimizationConstants.AggressiveInliningAndOptimization)] + public static int R11G11B10FloatToBGRA32(ReadOnlySpan input, int width, int height, out byte[] output) + { + output = new byte[width * height * 4]; + return R11G11B10FloatToBGRA32(input, width, height, output); + } + + // reference: https://github.com/microsoft/DirectX-Graphics-Samples/blob/e5ea2ac7430ce39e6f6d619fd85ae32581931589/MiniEngine/Core/Shaders/PixelPacking_R11G11B10.hlsli#L31-L37 + [MethodImpl(OptimizationConstants.AggressiveInliningAndOptimization)] + public static int R11G11B10FloatToBGRA32(ReadOnlySpan input, int width, int height, Span output) + { + int io = 0; + int oo = 0; + for (int i = 0; i < width; i++) + { + for (int j = 0; j < height; j++) + { + uint value = BinaryPrimitives.ReadUInt32LittleEndian(input.Slice(io, 4)); + ushort r = (ushort)((value << 4) & 0x7FF0); + ushort g = (ushort)((value >> 7) & 0x7FF0); + ushort b = (ushort)((value >> 17) & 0x7FE0); + output[oo + 0] = ClampByte((float) Unsafe.As(ref b) * 255f); // b + output[oo + 1] = ClampByte((float) Unsafe.As(ref g) * 255f); // g + output[oo + 2] = ClampByte((float) Unsafe.As(ref r) * 255f); // r + output[oo + 3] = 255; // a + io += 4; + oo += 4; + } + } + return io; + } + [MethodImpl(OptimizationConstants.AggressiveInliningAndOptimization)] public static int RG32ToBGRA32(ReadOnlySpan input, int width, int height, out byte[] output) {