Files
parkan-playground/TexmLib/TexmFile.cs
T

297 lines
10 KiB
C#
Raw Normal View History

2024-11-14 12:04:05 +03:00
using System.Buffers.Binary;
using System.Text;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
2024-11-18 23:48:42 +03:00
namespace TexmLib;
2024-11-14 12:04:05 +03:00
/// <summary>
/// Заголовок TEXM файла (может быть .0 файл)
/// </summary>
/// <param name="TexmAscii">Строка TEXM</param>
/// <param name="Width">Ширина или высота (не имеет значения, т.к. текстуры всегда квадратные)</param>
/// <param name="Height">Ширина или высота (не имеет значения, т.к. текстуры всегда квадратные)</param>
/// <param name="MipmapCount">Кол-во мипмапов (уменьшенные копии текстуры)</param>
/// <param name="Stride">Сколько БИТ занимает 1 пиксель</param>
/// <param name="Magic1">Неизвестно</param>
/// <param name="FormatOptionFlags">Дополнительные флаги для текстуры</param>
2024-11-14 12:04:05 +03:00
/// <param name="Format">Формат пикселя(4444, 8888, 888)</param>
public record TexmHeader(
string TexmAscii,
int Width,
int Height,
int MipmapCount,
int Stride,
int Magic1,
int FormatOptionFlags,
2024-11-14 12:04:05 +03:00
int Format
);
/// <summary>
/// В конце файла есть секция Page, она содержит информацию об атласе самого большого мипмапа
/// </summary>
/// <param name="Page">Заголовок секции</param>
/// <param name="Count">Кол-во элементов</param>
/// <param name="Items">Элементы</param>
public record PageHeader(string Page, int Count, List<PageItem> Items);
2026-05-09 20:36:43 +03:00
/// <summary>Элемент PAGE-секции TEXM.</summary>
/// <param name="X">X-координата в атласе.</param>
/// <param name="Width">Ширина элемента в атласе.</param>
/// <param name="Y">Y-координата в атласе.</param>
/// <param name="Height">Высота элемента в атласе.</param>
2024-11-14 12:04:05 +03:00
public record PageItem(short X, short Width, short Y, short Height);
2026-05-09 20:36:43 +03:00
/// <summary>TEXM файл.</summary>
/// <param name="FileName">Исходное имя файла текстуры TEXM.</param>
/// <param name="Header">Заголовок файла (length = 32).</param>
/// <param name="MipmapBytes">Байты mipmap уровней.</param>
/// <param name="Pages">PAGE-секция атласа, если присутствует.</param>
/// <param name="IsIndexed">Признак indexed texture с lookup таблицей на 1024 байта.</param>
/// <param name="LookupColors">Lookup таблица цветов: 256 цветов * 4 байта.</param>
public record class TexmFile(
string FileName,
TexmHeader Header,
List<byte[]> MipmapBytes,
PageHeader? Pages,
bool IsIndexed,
byte[] LookupColors)
2024-11-14 12:04:05 +03:00
{
2026-05-09 20:36:43 +03:00
public Task WriteToFolder(string folder)
2024-11-14 12:04:05 +03:00
{
2026-05-09 20:36:43 +03:00
if (!Directory.Exists(folder))
2024-11-14 12:04:05 +03:00
{
Directory.CreateDirectory(folder);
}
var outputDir = Path.Combine(folder, Path.GetFileName(FileName));
Directory.CreateDirectory(outputDir);
if (IsIndexed)
{
for (var i = 0; i < Header.MipmapCount; i++)
{
2026-05-09 20:36:43 +03:00
var mipWidth = Math.Max(1, Header.Width >> i);
var mipHeight = Math.Max(1, Header.Height >> i);
2024-11-14 12:04:05 +03:00
var reinterpretedPixels = ReinterpretIndexedMipmap(MipmapBytes[i], LookupColors);
var image = Image.LoadPixelData<Rgba32>(reinterpretedPixels, mipWidth, mipHeight);
image.SaveAsPng(Path.Combine(outputDir, Path.GetFileName(FileName)) + $"_{mipWidth}x{mipHeight}_indexed.png");
}
2026-05-09 20:36:43 +03:00
return Task.CompletedTask;
2024-11-14 12:04:05 +03:00
}
for (var i = 0; i < Header.MipmapCount; i++)
{
2026-05-09 20:36:43 +03:00
var mipWidth = Math.Max(1, Header.Width >> i);
var mipHeight = Math.Max(1, Header.Height >> i);
2024-11-14 12:04:05 +03:00
var reinterpretedPixels = ReinterpretMipmapBytesAsRgba32(
MipmapBytes[i],
mipWidth,
mipHeight,
Header.Format
);
var image = Image.LoadPixelData<Rgba32>(reinterpretedPixels, mipWidth, mipHeight);
2024-11-14 21:53:59 +03:00
image.SaveAsPng(Path.Combine(outputDir, Path.GetFileName(FileName)) + $"_{Header.Format}_{mipWidth}x{mipHeight}.png");
2024-11-14 12:04:05 +03:00
}
2026-05-09 20:36:43 +03:00
return Task.CompletedTask;
2024-11-14 12:04:05 +03:00
}
2024-11-19 02:13:14 +03:00
public byte[] GetRgba32BytesFromMipmap(int index, out int mipWidth, out int mipHeight)
{
var mipmapBytes = MipmapBytes[index];
2026-05-09 20:36:43 +03:00
mipWidth = Math.Max(1, Header.Width >> index);
mipHeight = Math.Max(1, Header.Height >> index);
2024-11-19 02:13:14 +03:00
if (IsIndexed)
{
return ReinterpretIndexedMipmap(mipmapBytes, LookupColors);
}
return ReinterpretMipmapBytesAsRgba32(
mipmapBytes,
mipWidth,
mipHeight,
Header.Format
);
}
2024-11-14 12:04:05 +03:00
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];
2024-11-19 02:13:14 +03:00
var r = lookupColors[index * 4 + 0];
var g = lookupColors[index * 4 + 1];
var b = lookupColors[index * 4 + 2];
var a = lookupColors[index * 4 + 3];
2024-11-14 12:04:05 +03:00
result[i * 4 + 0] = r;
result[i * 4 + 1] = g;
result[i * 4 + 2] = b;
2026-05-09 20:36:43 +03:00
result[i * 4 + 3] = a;
2024-11-14 12:04:05 +03:00
}
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),
2026-05-09 20:36:43 +03:00
556 => ReinterpretAs556(bytes, mipWidth, mipHeight),
88 => ReinterpretAs88(bytes, mipWidth, mipHeight),
2024-11-14 12:04:05 +03:00
_ => 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)
{
2026-05-09 20:36:43 +03:00
var rawPixel = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(i, 2));
2024-11-14 12:04:05 +03:00
2026-05-09 20:36:43 +03:00
var r = (byte)(((rawPixel >> 11) & 0b11111) * 255 / 31);
var g = (byte)(((rawPixel >> 5) & 0b111111) * 255 / 63);
var b = (byte)((rawPixel & 0b11111) * 255 / 31);
2025-03-11 04:36:05 +03:00
2026-05-09 20:36:43 +03:00
result[i / 2 * 4 + 0] = r;
result[i / 2 * 4 + 1] = g;
result[i / 2 * 4 + 2] = b;
2025-03-11 04:36:05 +03:00
result[i / 2 * 4 + 3] = 0xff;
2026-05-09 20:36:43 +03:00
}
2025-03-11 04:36:05 +03:00
2026-05-09 20:36:43 +03:00
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;
2024-11-14 12:04:05 +03:00
}
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)
{
2026-05-09 20:36:43 +03:00
var rawPixel = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(i, 2));
2025-03-11 04:36:05 +03:00
2026-05-09 20:36:43 +03:00
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);
2024-11-14 12:04:05 +03:00
result[i / 2 * 4 + 0] = r;
result[i / 2 * 4 + 1] = g;
result[i / 2 * 4 + 2] = b;
result[i / 2 * 4 + 3] = a;
2026-05-09 20:36:43 +03:00
}
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;
2024-11-14 12:04:05 +03:00
}
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);
2026-05-09 20:36:43 +03:00
result[i + 0] = rawPixel[0];
result[i + 1] = rawPixel[1];
result[i + 2] = rawPixel[2];
2024-11-14 12:04:05 +03:00
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);
2025-03-11 04:36:05 +03:00
// swap endianess back
// (rawPixel[0], rawPixel[1], rawPixel[2], rawPixel[3]) = (rawPixel[3], rawPixel[2], rawPixel[1], rawPixel[0]);
2024-11-14 12:04:05 +03:00
2025-03-11 04:36:05 +03:00
var r = rawPixel[0];
2024-11-19 02:13:14 +03:00
var g = rawPixel[1];
2025-03-11 04:36:05 +03:00
var b = rawPixel[2];
2024-11-19 02:13:14 +03:00
var a = rawPixel[3];
2024-11-14 12:04:05 +03:00
result[i + 0] = r;
result[i + 1] = g;
result[i + 2] = b;
2026-05-09 20:36:43 +03:00
result[i + 3] = 255;
2025-03-11 04:36:05 +03:00
// swap endianess back
// (rawPixel[0], rawPixel[1], rawPixel[2], rawPixel[3]) = (rawPixel[3], rawPixel[2], rawPixel[1], rawPixel[0]);
2024-11-14 12:04:05 +03:00
}
return result;
}
2026-05-09 20:36:43 +03:00
}