using System.Buffers.Binary; using System.Text; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; namespace TexmLib; /// /// Заголовок TEXM файла (может быть .0 файл) /// /// Строка TEXM /// Ширина или высота (не имеет значения, т.к. текстуры всегда квадратные) /// Ширина или высота (не имеет значения, т.к. текстуры всегда квадратные) /// Кол-во мипмапов (уменьшенные копии текстуры) /// Сколько БИТ занимает 1 пиксель /// Неизвестно /// Дополнительные флаги для текстуры /// Формат пикселя(4444, 8888, 888) public record TexmHeader( string TexmAscii, int Width, int Height, int MipmapCount, int Stride, int Magic1, int FormatOptionFlags, int Format ); /// /// В конце файла есть секция Page, она содержит информацию об атласе самого большого мипмапа /// /// Заголовок секции /// Кол-во элементов /// Элементы public record PageHeader(string Page, int Count, List Items); /// Элемент PAGE-секции TEXM. /// X-координата в атласе. /// Ширина элемента в атласе. /// Y-координата в атласе. /// Высота элемента в атласе. public record PageItem(short X, short Width, short Y, short Height); /// TEXM файл. /// Исходное имя файла текстуры TEXM. /// Заголовок файла (length = 32). /// Байты mipmap уровней. /// PAGE-секция атласа, если присутствует. /// Признак indexed texture с lookup таблицей на 1024 байта. /// Lookup таблица цветов: 256 цветов * 4 байта. public record class TexmFile( string FileName, TexmHeader Header, List MipmapBytes, PageHeader? Pages, bool IsIndexed, byte[] LookupColors) { public Task WriteToFolder(string folder) { if (!Directory.Exists(folder)) { Directory.CreateDirectory(folder); } var outputDir = Path.Combine(folder, Path.GetFileName(FileName)); Directory.CreateDirectory(outputDir); if (IsIndexed) { for (var i = 0; i < Header.MipmapCount; i++) { var mipWidth = Math.Max(1, Header.Width >> i); var mipHeight = Math.Max(1, Header.Height >> i); var reinterpretedPixels = ReinterpretIndexedMipmap(MipmapBytes[i], LookupColors); var image = Image.LoadPixelData(reinterpretedPixels, mipWidth, mipHeight); image.SaveAsPng(Path.Combine(outputDir, Path.GetFileName(FileName)) + $"_{mipWidth}x{mipHeight}_indexed.png"); } return Task.CompletedTask; } for (var i = 0; i < Header.MipmapCount; i++) { var mipWidth = Math.Max(1, Header.Width >> i); var mipHeight = Math.Max(1, Header.Height >> i); var reinterpretedPixels = ReinterpretMipmapBytesAsRgba32( MipmapBytes[i], mipWidth, mipHeight, Header.Format ); var image = Image.LoadPixelData(reinterpretedPixels, mipWidth, mipHeight); image.SaveAsPng(Path.Combine(outputDir, Path.GetFileName(FileName)) + $"_{Header.Format}_{mipWidth}x{mipHeight}.png"); } return Task.CompletedTask; } public byte[] GetRgba32BytesFromMipmap(int index, out int mipWidth, out int mipHeight) { var mipmapBytes = MipmapBytes[index]; mipWidth = Math.Max(1, Header.Width >> index); mipHeight = Math.Max(1, Header.Height >> index); if (IsIndexed) { return ReinterpretIndexedMipmap(mipmapBytes, LookupColors); } return ReinterpretMipmapBytesAsRgba32( mipmapBytes, mipWidth, mipHeight, Header.Format ); } private byte[] ReinterpretIndexedMipmap(byte[] bytes, byte[] lookupColors) { var span = bytes.AsSpan(); var result = new byte[bytes.Length * 4]; for (var i = 0; i < span.Length; i++) { var index = span[i]; var r = lookupColors[index * 4 + 0]; var g = lookupColors[index * 4 + 1]; var b = lookupColors[index * 4 + 2]; var a = lookupColors[index * 4 + 3]; result[i * 4 + 0] = r; result[i * 4 + 1] = g; result[i * 4 + 2] = b; result[i * 4 + 3] = a; } return result; } private byte[] ReinterpretMipmapBytesAsRgba32(byte[] bytes, int mipWidth, int mipHeight, int format) { var result = format switch { 8888 => ReinterpretAs8888(bytes, mipWidth, mipHeight), 888 => ReinterpretAs888(bytes, mipWidth, mipHeight), 4444 => ReinterpretAs4444(bytes, mipWidth, mipHeight), 565 => ReinterpretAs565(bytes, mipWidth, mipHeight), 556 => ReinterpretAs556(bytes, mipWidth, mipHeight), 88 => ReinterpretAs88(bytes, mipWidth, mipHeight), _ => throw new InvalidOperationException($"Invalid format {format}") }; return result; } private byte[] ReinterpretAs565(byte[] bytes, int mipWidth, int mipHeight) { var span = bytes.AsSpan(); var result = new byte[bytes.Length * 2]; for (var i = 0; i < span.Length; i += 2) { var rawPixel = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(i, 2)); var r = (byte)(((rawPixel >> 11) & 0b11111) * 255 / 31); var g = (byte)(((rawPixel >> 5) & 0b111111) * 255 / 63); var b = (byte)((rawPixel & 0b11111) * 255 / 31); result[i / 2 * 4 + 0] = r; result[i / 2 * 4 + 1] = g; result[i / 2 * 4 + 2] = b; result[i / 2 * 4 + 3] = 0xff; } return result; } private byte[] ReinterpretAs556(byte[] bytes, int mipWidth, int mipHeight) { var span = bytes.AsSpan(); var result = new byte[bytes.Length * 2]; for (var i = 0; i < span.Length; i += 2) { var rawPixel = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(i, 2)); var r = (byte)(((rawPixel >> 11) & 0b11111) * 255 / 31); var g = (byte)(((rawPixel >> 6) & 0b11111) * 255 / 31); var b = (byte)((rawPixel & 0b111111) * 255 / 63); result[i / 2 * 4 + 0] = r; result[i / 2 * 4 + 1] = g; result[i / 2 * 4 + 2] = b; result[i / 2 * 4 + 3] = 0xff; } return result; } private byte[] ReinterpretAs4444(byte[] bytes, int mipWidth, int mipHeight) { var span = bytes.AsSpan(); var result = new byte[bytes.Length * 2]; for (var i = 0; i < span.Length; i += 2) { var rawPixel = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(i, 2)); var a = (byte)(((rawPixel >> 12) & 0b1111) * 17); var r = (byte)(((rawPixel >> 8) & 0b1111) * 17); var g = (byte)(((rawPixel >> 4) & 0b1111) * 17); var b = (byte)((rawPixel & 0b1111) * 17); result[i / 2 * 4 + 0] = r; result[i / 2 * 4 + 1] = g; result[i / 2 * 4 + 2] = b; result[i / 2 * 4 + 3] = a; } return result; } private byte[] ReinterpretAs88(byte[] bytes, int mipWidth, int mipHeight) { var span = bytes.AsSpan(); var result = new byte[bytes.Length * 2]; for (var i = 0; i < span.Length; i += 2) { var l = span[i]; var a = span[i + 1]; result[i / 2 * 4 + 0] = l; result[i / 2 * 4 + 1] = l; result[i / 2 * 4 + 2] = l; result[i / 2 * 4 + 3] = a; } return result; } private byte[] ReinterpretAs888(byte[] bytes, int mipWidth, int mipHeight) { var span = bytes.AsSpan(); var result = new byte[bytes.Length * 2]; for (var i = 0; i < span.Length; i += 4) { var rawPixel = span.Slice(i, 4); result[i + 0] = rawPixel[0]; result[i + 1] = rawPixel[1]; result[i + 2] = rawPixel[2]; result[i + 3] = 255; } return result; } private byte[] ReinterpretAs8888(byte[] bytes, int mipWidth, int mipHeight) { var span = bytes.AsSpan(); var result = new byte[bytes.Length]; for (var i = 0; i < span.Length; i += 4) { var rawPixel = span.Slice(i, 4); // swap endianess back // (rawPixel[0], rawPixel[1], rawPixel[2], rawPixel[3]) = (rawPixel[3], rawPixel[2], rawPixel[1], rawPixel[0]); var b = rawPixel[0]; var g = rawPixel[1]; var r = rawPixel[2]; var a = rawPixel[3]; result[i + 0] = r; result[i + 1] = g; result[i + 2] = b; result[i + 3] = 255; // swap endianess back // (rawPixel[0], rawPixel[1], rawPixel[2], rawPixel[3]) = (rawPixel[3], rawPixel[2], rawPixel[1], rawPixel[0]); } return result; } }