From 8e82e702a71d5ef4a1bdb7cda1506fdf34d3bf82 Mon Sep 17 00:00:00 2001 From: Jeremy Pritts <49847914+ds5678@users.noreply.github.com> Date: Sun, 24 Mar 2024 17:00:42 -0700 Subject: [PATCH] OpenEXR writing --- .../DirectBitmap.cs | 7 + .../Program.cs | 38 ++- .../Exr/BinaryWriterExtensions.cs | 112 +++++++++ .../Exr/ExrAttributes.cs | 22 ++ AssetRipper.TextureDecoder/Exr/ExrBox2F.cs | 16 ++ AssetRipper.TextureDecoder/Exr/ExrBox2I.cs | 16 ++ AssetRipper.TextureDecoder/Exr/ExrChannel.cs | 32 +++ .../Exr/ExrChannelList.cs | 17 ++ .../Exr/ExrCompression.cs | 13 + AssetRipper.TextureDecoder/Exr/ExrDouble.cs | 17 ++ .../Exr/ExrImageFormat.cs | 14 ++ AssetRipper.TextureDecoder/Exr/ExrInt32.cs | 17 ++ .../Exr/ExrLevelMode.cs | 11 + .../Exr/ExrLineOrder.cs | 8 + .../Exr/ExrPixelType.cs | 8 + AssetRipper.TextureDecoder/Exr/ExrSingle.cs | 17 ++ AssetRipper.TextureDecoder/Exr/ExrString.cs | 16 ++ AssetRipper.TextureDecoder/Exr/ExrVector2.cs | 26 ++ AssetRipper.TextureDecoder/Exr/ExrWriter.cs | 225 ++++++++++++++++++ .../Exr/IExrDataType.cs | 10 + .../Exr/TileDescription.cs | 15 ++ 21 files changed, 645 insertions(+), 12 deletions(-) create mode 100644 AssetRipper.TextureDecoder/Exr/BinaryWriterExtensions.cs create mode 100644 AssetRipper.TextureDecoder/Exr/ExrAttributes.cs create mode 100644 AssetRipper.TextureDecoder/Exr/ExrBox2F.cs create mode 100644 AssetRipper.TextureDecoder/Exr/ExrBox2I.cs create mode 100644 AssetRipper.TextureDecoder/Exr/ExrChannel.cs create mode 100644 AssetRipper.TextureDecoder/Exr/ExrChannelList.cs create mode 100644 AssetRipper.TextureDecoder/Exr/ExrCompression.cs create mode 100644 AssetRipper.TextureDecoder/Exr/ExrDouble.cs create mode 100644 AssetRipper.TextureDecoder/Exr/ExrImageFormat.cs create mode 100644 AssetRipper.TextureDecoder/Exr/ExrInt32.cs create mode 100644 AssetRipper.TextureDecoder/Exr/ExrLevelMode.cs create mode 100644 AssetRipper.TextureDecoder/Exr/ExrLineOrder.cs create mode 100644 AssetRipper.TextureDecoder/Exr/ExrPixelType.cs create mode 100644 AssetRipper.TextureDecoder/Exr/ExrSingle.cs create mode 100644 AssetRipper.TextureDecoder/Exr/ExrString.cs create mode 100644 AssetRipper.TextureDecoder/Exr/ExrVector2.cs create mode 100644 AssetRipper.TextureDecoder/Exr/ExrWriter.cs create mode 100644 AssetRipper.TextureDecoder/Exr/IExrDataType.cs create mode 100644 AssetRipper.TextureDecoder/Exr/TileDescription.cs diff --git a/AssetRipper.TextureDecoder.ConsoleApp/DirectBitmap.cs b/AssetRipper.TextureDecoder.ConsoleApp/DirectBitmap.cs index 6bdf62c..e1491a1 100644 --- a/AssetRipper.TextureDecoder.ConsoleApp/DirectBitmap.cs +++ b/AssetRipper.TextureDecoder.ConsoleApp/DirectBitmap.cs @@ -1,3 +1,4 @@ +using AssetRipper.TextureDecoder.Exr; using AssetRipper.TextureDecoder.Rgb; using AssetRipper.TextureDecoder.Rgb.Channels; using AssetRipper.TextureDecoder.Rgb.Formats; @@ -55,6 +56,12 @@ public readonly struct DirectBitmap } } + public void SaveAsExr(string path) + { + using Stream stream = File.OpenWrite(path); + ExrWriter.Write(stream, Width, Height, Pixels); + } + public void SaveAsHdr(string path) { GetDataAndComponentsForSaving(out byte[] data, out ColorComponents components); diff --git a/AssetRipper.TextureDecoder.ConsoleApp/Program.cs b/AssetRipper.TextureDecoder.ConsoleApp/Program.cs index 74f9bb2..8d813fb 100644 --- a/AssetRipper.TextureDecoder.ConsoleApp/Program.cs +++ b/AssetRipper.TextureDecoder.ConsoleApp/Program.cs @@ -63,18 +63,32 @@ internal static partial class Program switch (outputType) { - case "png": - { - string newPath = Path.Combine(dirPath, name + ".png"); - bitmap.FlipY(); - bitmap.SaveAsPng(newPath); - } - break; case "bgra": - { - string newPath = Path.Combine(dirPath, name + ".bgra"); - WriteAllBytes(newPath, bitmap.Bits); - } + WriteAllBytes(Path.Combine(dirPath, name + ".bgra"), bitmap.Bits); + break; + case "bmp": + bitmap.FlipY(); + bitmap.SaveAsBmp(Path.Combine(dirPath, name + ".bmp")); + break; + case "exr": + bitmap.FlipY(); + bitmap.SaveAsExr(Path.Combine(dirPath, name + ".exr")); + break; + case "hdr": + bitmap.FlipY(); + bitmap.SaveAsHdr(Path.Combine(dirPath, name + ".hdr")); + break; + case "jpg": + bitmap.FlipY(); + bitmap.SaveAsJpg(Path.Combine(dirPath, name + ".jpg")); + break; + case "png": + bitmap.FlipY(); + bitmap.SaveAsPng(Path.Combine(dirPath, name + ".png")); + break; + case "tga": + bitmap.FlipY(); + bitmap.SaveAsTga(Path.Combine(dirPath, name + ".tga")); break; default: throw new NotSupportedException(outputType); @@ -239,4 +253,4 @@ internal static partial class Program using Microsoft.Win32.SafeHandles.SafeFileHandle sfh = File.OpenHandle(path, FileMode.Create, FileAccess.Write, FileShare.Read); RandomAccess.Write(sfh, bytes, 0); } -} \ No newline at end of file +} diff --git a/AssetRipper.TextureDecoder/Exr/BinaryWriterExtensions.cs b/AssetRipper.TextureDecoder/Exr/BinaryWriterExtensions.cs new file mode 100644 index 0000000..8b1d150 --- /dev/null +++ b/AssetRipper.TextureDecoder/Exr/BinaryWriterExtensions.cs @@ -0,0 +1,112 @@ +using System.Buffers; +using System.Text; + +namespace AssetRipper.TextureDecoder.Exr; + +internal static class BinaryWriterExtensions +{ + public static void WriteLengthPrefixedString(this BinaryWriter writer, string value) + { + int length = Encoding.UTF8.GetByteCount(value); + writer.Write(length); + + byte[]? rentedArray; + scoped Span buffer; + if (length <= 1024) + { + rentedArray = null; + buffer = stackalloc byte[length]; + } + else + { + rentedArray = ArrayPool.Shared.Rent(length); + buffer = rentedArray.AsSpan(0, length); + } + + Encoding.UTF8.GetBytes(value, buffer); + writer.Write(buffer); + + if (rentedArray is not null) + { + ArrayPool.Shared.Return(rentedArray); + } + } + + public static void WriteNullTerminatedString(this BinaryWriter writer, string value) + { + int length = Encoding.UTF8.GetByteCount(value); + + byte[]? rentedArray; + scoped Span buffer; + if (length <= 1024) + { + rentedArray = null; + buffer = stackalloc byte[length]; + } + else + { + rentedArray = ArrayPool.Shared.Rent(length); + buffer = rentedArray.AsSpan(0, length); + } + + Encoding.UTF8.GetBytes(value, buffer); + writer.Write(buffer); + + if (rentedArray is not null) + { + ArrayPool.Shared.Return(rentedArray); + } + + writer.WriteNullByte(); + } + + public static void WriteNullTerminatedString(this BinaryWriter writer, ReadOnlySpan utf8String) + { + writer.Write(utf8String); + writer.WriteNullByte(); + } + + public static void WriteNullByte(this BinaryWriter writer) + { + writer.Write(default(byte)); + } + + public static void Write(this BinaryWriter writer, T value) where T : unmanaged, Enum + { + switch (Unsafe.SizeOf()) + { + case sizeof(byte): + writer.Write(Unsafe.As(ref value)); + break; + case sizeof(ushort): + writer.Write(Unsafe.As(ref value)); + break; + case sizeof(uint): + writer.Write(Unsafe.As(ref value)); + break; + case sizeof(ulong): + writer.Write(Unsafe.As(ref value)); + break; + } + } + + public static void WriteAttribute(this BinaryWriter writer, ReadOnlySpan attributeName, T attributeValue) where T : IExrDataType + { + writer.Write(attributeName); + writer.WriteNullByte(); + writer.Write(T.TypeName); + writer.WriteNullByte(); + writer.Write(attributeValue.Size); + attributeValue.Write(writer); + } + + public static void WriteAttribute(this BinaryWriter writer, ReadOnlySpan attributeName, ReadOnlySpan attributeType, T attributeValue) where T : unmanaged, Enum + { + writer.Write(attributeName); + writer.WriteNullByte(); + writer.Write(attributeType); + writer.WriteNullByte(); + writer.Write(Unsafe.SizeOf()); + writer.Write(attributeValue); + } +} diff --git a/AssetRipper.TextureDecoder/Exr/ExrAttributes.cs b/AssetRipper.TextureDecoder/Exr/ExrAttributes.cs new file mode 100644 index 0000000..1e729e4 --- /dev/null +++ b/AssetRipper.TextureDecoder/Exr/ExrAttributes.cs @@ -0,0 +1,22 @@ +namespace AssetRipper.TextureDecoder.Exr; + +public static class ExrAttributes +{ + public static ReadOnlySpan Channels => "channels"u8; + + public static ReadOnlySpan Compression => "compression"u8; + + public static ReadOnlySpan DataWindow => "dataWindow"u8; + + public static ReadOnlySpan DisplayWindow => "displayWindow"u8; + + public static ReadOnlySpan LineOrder => "lineOrder"u8; + + public static ReadOnlySpan PixelAspectRatio => "pixelAspectRatio"u8; + + public static ReadOnlySpan ScreenWindowCenter => "screenWindowCenter"u8; + + public static ReadOnlySpan ScreenWindowWidth => "screenWindowWidth"u8; + + public static ReadOnlySpan Tiles => "tiles"u8; +} diff --git a/AssetRipper.TextureDecoder/Exr/ExrBox2F.cs b/AssetRipper.TextureDecoder/Exr/ExrBox2F.cs new file mode 100644 index 0000000..857b977 --- /dev/null +++ b/AssetRipper.TextureDecoder/Exr/ExrBox2F.cs @@ -0,0 +1,16 @@ +namespace AssetRipper.TextureDecoder.Exr; + +public readonly record struct ExrBox2F(float XMin, float YMin, float XMax, float YMax) : IExrDataType +{ + public static ReadOnlySpan TypeName => "box2f"u8; + + public int Size => sizeof(float) + sizeof(float) + sizeof(float) + sizeof(float); + + public void Write(BinaryWriter writer) + { + writer.Write(XMin); + writer.Write(YMin); + writer.Write(XMax); + writer.Write(YMax); + } +} diff --git a/AssetRipper.TextureDecoder/Exr/ExrBox2I.cs b/AssetRipper.TextureDecoder/Exr/ExrBox2I.cs new file mode 100644 index 0000000..93eb1cc --- /dev/null +++ b/AssetRipper.TextureDecoder/Exr/ExrBox2I.cs @@ -0,0 +1,16 @@ +namespace AssetRipper.TextureDecoder.Exr; + +public readonly record struct ExrBox2I(int XMin, int YMin, int XMax, int YMax) : IExrDataType +{ + public static ReadOnlySpan TypeName => "box2i"u8; + + public int Size => sizeof(int) + sizeof(int) + sizeof(int) + sizeof(int); + + public void Write(BinaryWriter writer) + { + writer.Write(XMin); + writer.Write(YMin); + writer.Write(XMax); + writer.Write(YMax); + } +} diff --git a/AssetRipper.TextureDecoder/Exr/ExrChannel.cs b/AssetRipper.TextureDecoder/Exr/ExrChannel.cs new file mode 100644 index 0000000..e320f03 --- /dev/null +++ b/AssetRipper.TextureDecoder/Exr/ExrChannel.cs @@ -0,0 +1,32 @@ +using System.Text; + +namespace AssetRipper.TextureDecoder.Exr; + +public readonly record struct ExrChannel(string Name, ExrPixelType PixelType, bool PLinear, int XSampling, int YSampling) +{ + public int Size + { + get + { + return Encoding.UTF8.GetByteCount(Name) + + sizeof(byte)//Null terminator + + sizeof(ExrPixelType) + + sizeof(bool) + + 3 * sizeof(byte)//Reserved bytes + + sizeof(int) + + sizeof(int); + } + } + + public void Write(BinaryWriter writer) + { + writer.WriteNullTerminatedString(Name); + writer.Write(PixelType); + writer.Write(PLinear); + writer.WriteNullByte(); + writer.WriteNullByte(); + writer.WriteNullByte(); + writer.Write(XSampling); + writer.Write(YSampling); + } +} diff --git a/AssetRipper.TextureDecoder/Exr/ExrChannelList.cs b/AssetRipper.TextureDecoder/Exr/ExrChannelList.cs new file mode 100644 index 0000000..c23487c --- /dev/null +++ b/AssetRipper.TextureDecoder/Exr/ExrChannelList.cs @@ -0,0 +1,17 @@ +namespace AssetRipper.TextureDecoder.Exr; + +public readonly record struct ExrChannelList(ExrChannel[] Channels) : IExrDataType +{ + public static ReadOnlySpan TypeName => "chlist"u8; + + public int Size => Channels.Sum(c => c.Size); + + public void Write(BinaryWriter writer) + { + foreach (ExrChannel channel in Channels) + { + channel.Write(writer); + } + writer.WriteNullByte(); + } +} diff --git a/AssetRipper.TextureDecoder/Exr/ExrCompression.cs b/AssetRipper.TextureDecoder/Exr/ExrCompression.cs new file mode 100644 index 0000000..e6b94c8 --- /dev/null +++ b/AssetRipper.TextureDecoder/Exr/ExrCompression.cs @@ -0,0 +1,13 @@ +namespace AssetRipper.TextureDecoder.Exr; + +public enum ExrCompression : byte +{ + None = 0, + RLE = 1, + ZIPS = 2, + ZIP = 3, + PIZ = 4, + PXR24 = 5, + B44 = 6, + B44A = 7, +} diff --git a/AssetRipper.TextureDecoder/Exr/ExrDouble.cs b/AssetRipper.TextureDecoder/Exr/ExrDouble.cs new file mode 100644 index 0000000..f040ed3 --- /dev/null +++ b/AssetRipper.TextureDecoder/Exr/ExrDouble.cs @@ -0,0 +1,17 @@ +namespace AssetRipper.TextureDecoder.Exr; + +public readonly record struct ExrDouble(double Value) : IExrDataType +{ + public static ReadOnlySpan TypeName => "double"u8; + + public int Size => sizeof(double); + + public void Write(BinaryWriter writer) + { + writer.Write(Value); + } + + public static implicit operator double(ExrDouble value) => value.Value; + + public static implicit operator ExrDouble(double value) => new(value); +} diff --git a/AssetRipper.TextureDecoder/Exr/ExrImageFormat.cs b/AssetRipper.TextureDecoder/Exr/ExrImageFormat.cs new file mode 100644 index 0000000..ab1f271 --- /dev/null +++ b/AssetRipper.TextureDecoder/Exr/ExrImageFormat.cs @@ -0,0 +1,14 @@ +namespace AssetRipper.TextureDecoder.Exr; +public enum ExrImageFormat +{ + ScanLines, + Tiles, +} +public static class ExrImageFormatExtensions +{ + public static byte ToVersionByte(this ExrImageFormat format) => format switch + { + ExrImageFormat.Tiles => 0x02, + _ => 0x00, + }; +} diff --git a/AssetRipper.TextureDecoder/Exr/ExrInt32.cs b/AssetRipper.TextureDecoder/Exr/ExrInt32.cs new file mode 100644 index 0000000..0f5e687 --- /dev/null +++ b/AssetRipper.TextureDecoder/Exr/ExrInt32.cs @@ -0,0 +1,17 @@ +namespace AssetRipper.TextureDecoder.Exr; + +public readonly record struct ExrInt32(int Value) : IExrDataType +{ + public static ReadOnlySpan TypeName => "int"u8; + + public int Size => sizeof(int); + + public void Write(BinaryWriter writer) + { + writer.Write(Value); + } + + public static implicit operator int(ExrInt32 value) => value.Value; + + public static implicit operator ExrInt32(int value) => new(value); +} diff --git a/AssetRipper.TextureDecoder/Exr/ExrLevelMode.cs b/AssetRipper.TextureDecoder/Exr/ExrLevelMode.cs new file mode 100644 index 0000000..6f09ae6 --- /dev/null +++ b/AssetRipper.TextureDecoder/Exr/ExrLevelMode.cs @@ -0,0 +1,11 @@ +namespace AssetRipper.TextureDecoder.Exr; + +[Flags] +public enum ExrLevelMode : byte +{ + OneLevel = 0, + MipMapLevels = 1, + RipMapLevels = 2, + RoundDown = 0, + RoundUp = 16, +} diff --git a/AssetRipper.TextureDecoder/Exr/ExrLineOrder.cs b/AssetRipper.TextureDecoder/Exr/ExrLineOrder.cs new file mode 100644 index 0000000..77a8a3f --- /dev/null +++ b/AssetRipper.TextureDecoder/Exr/ExrLineOrder.cs @@ -0,0 +1,8 @@ +namespace AssetRipper.TextureDecoder.Exr; + +public enum ExrLineOrder : byte +{ + IncreasingY = 0, + DecreasingY = 1, + RandomY = 2, +} diff --git a/AssetRipper.TextureDecoder/Exr/ExrPixelType.cs b/AssetRipper.TextureDecoder/Exr/ExrPixelType.cs new file mode 100644 index 0000000..873c395 --- /dev/null +++ b/AssetRipper.TextureDecoder/Exr/ExrPixelType.cs @@ -0,0 +1,8 @@ +namespace AssetRipper.TextureDecoder.Exr; + +public enum ExrPixelType : int +{ + UInt = 0, + Half = 1, + Single = 2, +} diff --git a/AssetRipper.TextureDecoder/Exr/ExrSingle.cs b/AssetRipper.TextureDecoder/Exr/ExrSingle.cs new file mode 100644 index 0000000..2de1f42 --- /dev/null +++ b/AssetRipper.TextureDecoder/Exr/ExrSingle.cs @@ -0,0 +1,17 @@ +namespace AssetRipper.TextureDecoder.Exr; + +public readonly record struct ExrSingle(float Value) : IExrDataType +{ + public static ReadOnlySpan TypeName => "float"u8; + + public int Size => sizeof(float); + + public void Write(BinaryWriter writer) + { + writer.Write(Value); + } + + public static implicit operator float(ExrSingle value) => value.Value; + + public static implicit operator ExrSingle(float value) => new(value); +} diff --git a/AssetRipper.TextureDecoder/Exr/ExrString.cs b/AssetRipper.TextureDecoder/Exr/ExrString.cs new file mode 100644 index 0000000..0d4aba7 --- /dev/null +++ b/AssetRipper.TextureDecoder/Exr/ExrString.cs @@ -0,0 +1,16 @@ +using System.Text; + +namespace AssetRipper.TextureDecoder.Exr; + +public readonly record struct ExrString(string Value) : IExrDataType +{ + public static ReadOnlySpan TypeName => "string"u8; + + public int Size => sizeof(int) + Encoding.UTF8.GetByteCount(Value); + + public void Write(BinaryWriter writer) => writer.WriteLengthPrefixedString(Value); + + public static implicit operator string(ExrString value) => value.Value; + + public static implicit operator ExrString(string value) => new(value); +} diff --git a/AssetRipper.TextureDecoder/Exr/ExrVector2.cs b/AssetRipper.TextureDecoder/Exr/ExrVector2.cs new file mode 100644 index 0000000..96db2fd --- /dev/null +++ b/AssetRipper.TextureDecoder/Exr/ExrVector2.cs @@ -0,0 +1,26 @@ +namespace AssetRipper.TextureDecoder.Exr; + +public readonly record struct ExrVector2(Vector2 Value) : IExrDataType +{ + public ExrVector2(float x, float y) : this(new Vector2(x, y)) + { + } + + public int Size => sizeof(float) + sizeof(float); + + public float X => Value.X; + + public float Y => Value.Y; + + public static ReadOnlySpan TypeName => "v2f"u8; + + public void Write(BinaryWriter writer) + { + writer.Write(X); + writer.Write(Y); + } + + public static implicit operator Vector2(ExrVector2 value) => value.Value; + + public static implicit operator ExrVector2(Vector2 value) => new(value); +} diff --git a/AssetRipper.TextureDecoder/Exr/ExrWriter.cs b/AssetRipper.TextureDecoder/Exr/ExrWriter.cs new file mode 100644 index 0000000..5f73e78 --- /dev/null +++ b/AssetRipper.TextureDecoder/Exr/ExrWriter.cs @@ -0,0 +1,225 @@ +using AssetRipper.TextureDecoder.Rgb; + +namespace AssetRipper.TextureDecoder.Exr; + +public static class ExrWriter +{ + public static ReadOnlySpan Magic => [0x76, 0x2f, 0x31, 0x01]; + + public static void Write(Stream stream, int width, int height, ReadOnlySpan pixels) + where TChannelValue : unmanaged + where TColor : unmanaged, IColor + { + int ChannelCount = Color.GetChannelCount(); + ExrPixelType PixelType = Unsafe.SizeOf() <= sizeof(ushort) ? ExrPixelType.Half : ExrPixelType.Single; + int ChannelSize = PixelType is ExrPixelType.Half ? Unsafe.SizeOf() : sizeof(float); + + using BinaryWriter writer = new BinaryWriter(stream); + + //Magic bytes + writer.Write(Magic); + + //Version bytes + writer.Write((byte)2);//OpenEXR version 2 + writer.Write(ExrImageFormat.ScanLines.ToVersionByte()); + writer.WriteNullByte(); + writer.WriteNullByte(); + + ExrChannel[] channels; + { + Span buffer = [default, default, default, default]; + //Stackalloc doesn't work for managed types. + //This can be reworked when ref structs can be used in generics. + + int i = 0; + + //The order doesn't really matter here because the format sorts channels alphabetically + //when reading reading scan lines, but this makes that more clear. + if (TColor.HasAlphaChannel) + { + buffer[i] = new ExrChannel("A", PixelType, false, 1, 1); + i++; + } + if (TColor.HasBlueChannel) + { + buffer[i] = new ExrChannel("B", PixelType, false, 1, 1); + i++; + } + if (TColor.HasGreenChannel) + { + buffer[i] = new ExrChannel("G", PixelType, false, 1, 1); + i++; + } + if (TColor.HasRedChannel) + { + buffer[i] = new ExrChannel("R", PixelType, false, 1, 1); + i++; + } + + channels = buffer[..i].ToArray(); + } + + //Attributes + writer.WriteAttribute(ExrAttributes.Channels, new ExrChannelList(channels)); + writer.WriteAttribute(ExrAttributes.Compression, "compression"u8, ExrCompression.None); + writer.WriteAttribute(ExrAttributes.DataWindow, new ExrBox2I(0, 0, width - 1, height - 1)); + writer.WriteAttribute(ExrAttributes.DisplayWindow, new ExrBox2I(0, 0, width - 1, height - 1)); + writer.WriteAttribute(ExrAttributes.LineOrder, "lineOrder"u8, ExrLineOrder.IncreasingY); + writer.WriteAttribute(ExrAttributes.PixelAspectRatio, new ExrSingle(1f)); + writer.WriteAttribute(ExrAttributes.ScreenWindowCenter, new ExrVector2(0f, 0f)); + writer.WriteAttribute(ExrAttributes.ScreenWindowWidth, new ExrSingle(1f)); + writer.WriteNullByte();//End of header + + //Scan line offset table + long offset = writer.BaseStream.Position + height * sizeof(ulong); + for (int i = 0; i < height; i++) + { + writer.Write(offset); + offset += sizeof(int) + ChannelCount * ChannelSize * width; + } + + //Scan lines + if (PixelType is ExrPixelType.Half) + { + for (int i = 0; i < height; i++) + { + writer.Write(i); + writer.Write(ChannelCount * ChannelSize * width); + if (TColor.HasAlphaChannel) + { + for (int j = 0; j < width; j++) + { + Half alpha = NumericConversion.Convert(pixels[i * width + j].A); + writer.Write(alpha); + } + } + if (TColor.HasBlueChannel) + { + for (int j = 0; j < width; j++) + { + float blue = NumericConversion.Convert(pixels[i * width + j].B); + if (TColor.HasAlphaChannel) + { + float alpha = NumericConversion.Convert(pixels[i * width + j].A); + writer.Write((Half)ConvertSRGB(blue * alpha)); + } + else + { + writer.Write((Half)ConvertSRGB(blue)); + } + } + } + if (TColor.HasGreenChannel) + { + for (int j = 0; j < width; j++) + { + float green = NumericConversion.Convert(pixels[i * width + j].G); + if (TColor.HasAlphaChannel) + { + float alpha = NumericConversion.Convert(pixels[i * width + j].A); + writer.Write((Half)ConvertSRGB(green * alpha)); + } + else + { + writer.Write((Half)ConvertSRGB(green)); + } + } + } + if (TColor.HasRedChannel) + { + for (int j = 0; j < width; j++) + { + float red = NumericConversion.Convert(pixels[i * width + j].R); + if (TColor.HasAlphaChannel) + { + float alpha = NumericConversion.Convert(pixels[i * width + j].A); + writer.Write((Half)ConvertSRGB(red * alpha)); + } + else + { + writer.Write((Half)ConvertSRGB(red)); + } + } + } + } + } + else + { + for (int i = 0; i < height; i++) + { + writer.Write(i); + writer.Write(ChannelCount * ChannelSize * width); + if (TColor.HasAlphaChannel) + { + for (int j = 0; j < width; j++) + { + float alpha = NumericConversion.Convert(pixels[i * width + j].A); + writer.Write(alpha); + } + } + if (TColor.HasBlueChannel) + { + for (int j = 0; j < width; j++) + { + float blue = NumericConversion.Convert(pixels[i * width + j].B); + if (TColor.HasAlphaChannel) + { + float alpha = NumericConversion.Convert(pixels[i * width + j].A); + writer.Write(ConvertSRGB(blue * alpha)); + } + else + { + writer.Write(ConvertSRGB(blue)); + } + } + } + if (TColor.HasGreenChannel) + { + for (int j = 0; j < width; j++) + { + float green = NumericConversion.Convert(pixels[i * width + j].G); + if (TColor.HasAlphaChannel) + { + float alpha = NumericConversion.Convert(pixels[i * width + j].A); + writer.Write(ConvertSRGB(green * alpha)); + } + else + { + writer.Write(ConvertSRGB(green)); + } + } + } + if (TColor.HasRedChannel) + { + for (int j = 0; j < width; j++) + { + float red = NumericConversion.Convert(pixels[i * width + j].R); + if (TColor.HasAlphaChannel) + { + float alpha = NumericConversion.Convert(pixels[i * width + j].A); + writer.Write(ConvertSRGB(red * alpha)); + } + else + { + writer.Write(ConvertSRGB(red)); + } + } + } + } + } + } + + private static float ConvertSRGB(float value) + { + //https://en.wikipedia.org/wiki/SRGB#From_sRGB_to_CIE_XYZ + //https://en.wikipedia.org/wiki/Gamma_correction#Computer_displays + if (value < 0.04045) + { + return value / 12.92f; + } + else + { + return float.Pow((value + 0.055f) / 1.055f, 2.4f); + } + } +} diff --git a/AssetRipper.TextureDecoder/Exr/IExrDataType.cs b/AssetRipper.TextureDecoder/Exr/IExrDataType.cs new file mode 100644 index 0000000..4a3c749 --- /dev/null +++ b/AssetRipper.TextureDecoder/Exr/IExrDataType.cs @@ -0,0 +1,10 @@ +namespace AssetRipper.TextureDecoder.Exr; + +public interface IExrDataType +{ + static abstract ReadOnlySpan TypeName { get; } + + int Size { get; } + + void Write(BinaryWriter writer); +} diff --git a/AssetRipper.TextureDecoder/Exr/TileDescription.cs b/AssetRipper.TextureDecoder/Exr/TileDescription.cs new file mode 100644 index 0000000..261f9e7 --- /dev/null +++ b/AssetRipper.TextureDecoder/Exr/TileDescription.cs @@ -0,0 +1,15 @@ +namespace AssetRipper.TextureDecoder.Exr; + +public readonly record struct TileDescription(int XSize, int YSize, ExrLevelMode Mode) : IExrDataType +{ + public static ReadOnlySpan TypeName => "tileDesc"u8; + + public int Size => sizeof(int) + sizeof(int) + sizeof(ExrLevelMode); + + public void Write(BinaryWriter writer) + { + writer.Write(XSize); + writer.Write(YSize); + writer.Write(Mode); + } +}