From 0b43e363fb01c57ad000a48ced26c7ed58a70e88 Mon Sep 17 00:00:00 2001 From: Benjamin Moir Date: Sun, 26 May 2024 10:15:02 +1000 Subject: [PATCH] Improve tex2dds command --- tools/TLTool/TextureDdsConvertCommand.cs | 285 +++++++++++++++++------ 1 file changed, 211 insertions(+), 74 deletions(-) diff --git a/tools/TLTool/TextureDdsConvertCommand.cs b/tools/TLTool/TextureDdsConvertCommand.cs index 7df1582..34357e2 100644 --- a/tools/TLTool/TextureDdsConvertCommand.cs +++ b/tools/TLTool/TextureDdsConvertCommand.cs @@ -1,5 +1,7 @@ using System.CommandLine; using System.CommandLine.Invocation; +using System.Numerics; +using System.Runtime.InteropServices; namespace TLTool; @@ -49,95 +51,171 @@ public void Execute(InvocationContext context) // On PC, the textures are already in DDS format. // They just have some kind of 4 byte value before it. File.WriteAllBytes(output, data[4..]); + return; } - else if (platform == Platform.PS3) + + var buffer = data; + var header = new DdsHeader { - using var stream = File.OpenRead(meta); - using var reader = new BigEndianBinaryReader(stream); + Flags = 0x1007, // DDS_HEADER_FLAGS_TEXTURE + MipMapCount = 1, + Caps = 0x1000, // DDSCAPS_TEXTURE + }; + + using var stream = File.OpenRead(meta); + using var reader = platform == Platform.PS3 ? new BigEndianBinaryReader(stream) : new BinaryReader(stream); - // Determine format - var isMTex = stream.ReadUInt32LittleEndian() == MTEX; + // Determine file format + var isMTex = stream.ReadUInt32LittleEndian() == MTEX; - // The format, width and height are located at offset 0x0D in TOTEXB. - stream.Position = isMTex ? 0x0D : 0x1D; - var format = (PS3TextureFormat)reader.ReadByte(); - var width = reader.ReadUInt16(); - var height = reader.ReadUInt16(); + // Read texture usage, width and height + stream.Position = isMTex ? 0x0D : 0x1D; + var usage = (TextureUsage)reader.ReadByte(); + header.Width = reader.ReadUInt16(); + header.Height = reader.ReadUInt16(); + header.Depth = reader.ReadUInt16(); + var ddsFormat = TextureFormat.Unknown; + + if (header.Depth > 0) + { + header.Flags |= 0x800000; // DDSD_DEPTH + header.Caps |= 0x8; // DDSCAPS_COMPLEX; + header.Caps2 |= 0x200000; // DDSCAPS2_VOLUME + } - using var writer = new BinaryWriter(File.Create(output)); - var header = new DdsHeader + if (platform == Platform.PS3) + { + var format = new CellTextureFormat(reader.ReadByte()); + ddsFormat = format.ColorType switch { - Width = width, - Height = height, - Flags = 0x1007, // DDS_HEADER_FLAGS_TEXTURE - MipMapCount = 1, + CellColorType.DXT1 => TextureFormat.DXT1, + CellColorType.DXT45 => TextureFormat.DXT5, + CellColorType.A8B8G8R8 => TextureFormat.B8G8R8A8, + CellColorType.A4B4G4R4 => TextureFormat.B4G4R4A4, + _ => throw new NotSupportedException(format.ColorType.ToString()) }; - if (format == PS3TextureFormat.DXT1) + // Compressed textures are seemingly always linear, even if not specified as such. + if (format.IsSwizzled && !format.IsCompressed) { - header.PitchOrLinearSize = (uint)width * height; - header.PixelFormat = new DdsPixelFormat - { - Flags = 0x4, // DDPF_FOURCC - FourCC = GetFourCC(TextureFormat.DXT1) - }; + buffer = new byte[data.Length]; + CellUnSwizzle(data, buffer, (int)header.Width, (int)header.Height, blockSize: GetBlockSize(ddsFormat)); } - else if (format == PS3TextureFormat.ARGB) + } + else if (platform == Platform.PS4) + { + // TODO: Get format properly + ddsFormat = usage switch { - for (int i = 0; i < data.Length; i += 4) - data.AsSpan(i, 4).Reverse(); + TextureUsage.World => TextureFormat.DXT1, + TextureUsage.Interface => TextureFormat.DXT5, + _ => TextureFormat.Unknown + }; - header.PitchOrLinearSize = (uint)width * 4; - header.PixelFormat = new DdsPixelFormat - { - Flags = 0x1 | 0x40, // DDPF_ALPHAPIXELS | DDPF_RGB - RGBBitCount = 32, - ABitMask = 0xFF000000, - RBitMask = 0x00FF0000, - GBitMask = 0x0000FF00, - BBitMask = 0x000000FF, - }; - } + // Un-swizzle texture data. + buffer = new byte[data.Length]; + OrbisUnSwizzle(data, buffer, (int)header.Width, (int)header.Height, blockSize: GetBlockSize(ddsFormat)); + } + else + { + throw new ArgumentException($"Unknown platform '{platform}'."); + } - header.Write(writer); - writer.Write(data); + if (ddsFormat is TextureFormat.DXT1 or TextureFormat.DXT5) + { + header.Flags |= 0x80000; // DDSD_LINEARSIZE + header.PitchOrLinearSize = header.Height * uint.Max(1, (header.Width + 3) / 4) * (uint)GetBlockSize(ddsFormat); + header.PixelFormat = new DdsPixelFormat + { + Flags = 0x4, // DDPF_FOURCC + FourCC = GetFourCC(ddsFormat) + }; } - else if (platform == Platform.PS4) + else { - var buffer = new byte[data.Length]; - using var stream = File.OpenRead(meta); - using var reader = new BinaryReader(stream); + var blockSize = GetBlockSize(ddsFormat); + header.Flags |= 0x8; // DDSD_PITCH + header.PitchOrLinearSize = header.Width * ((uint)blockSize * 8 + 7) / 8; + header.PixelFormat = new DdsPixelFormat + { + Flags = 0x1 | 0x40, // DDPF_ALPHAPIXELS | DDPF_RGB + RGBBitCount = (uint)blockSize * 8, + }; - // The format, width and height are located at offset 0x1D in TOTEXB_D. - stream.Position = 0x1D; - var format = (TextureFormat)reader.ReadByte(); - var width = reader.ReadUInt16(); - var height = reader.ReadUInt16(); + if (ddsFormat == TextureFormat.B8G8R8A8) + { + header.PixelFormat.BBitMask = 0x000000FF; + header.PixelFormat.GBitMask = 0x0000FF00; + header.PixelFormat.RBitMask = 0x00FF0000; + header.PixelFormat.ABitMask = 0xFF000000; - // Un-swizzle texture data. - OrbisUnSwizzle(data, buffer, width, height, blockSize: GetBlockSize(format)); - - using var writer = new BinaryWriter(File.Create(output)); - var header = new DdsHeader + SwizzleFormat(buffer, header.PixelFormat, fromFormat: header.PixelFormat with + { + ABitMask = 0x000000FF, + RBitMask = 0x0000FF00, + GBitMask = 0x00FF0000, + BBitMask = 0xFF000000, + }); + } + else if (ddsFormat == TextureFormat.B4G4R4A4) { - Width = width, - Height = height, - Flags = 0x1007, // DDS_HEADER_FLAGS_TEXTURE - PitchOrLinearSize = (uint)width * height, - MipMapCount = 1, - PixelFormat = + header.PixelFormat.BBitMask = 0x000F; + header.PixelFormat.GBitMask = 0x00F0; + header.PixelFormat.RBitMask = 0x0F00; + header.PixelFormat.ABitMask = 0xF000; + + SwizzleFormat(buffer, header.PixelFormat, fromFormat: header.PixelFormat with { - Flags = 0x4, // DDPF_FOURCC - FourCC = GetFourCC(format) - }, - }; + ABitMask = 0x000F, + BBitMask = 0x00F0, + GBitMask = 0x0F00, + RBitMask = 0xF000, + }); + } + } + + using var writer = new BinaryWriter(File.Create(output)); + header.Write(writer); + writer.Write(buffer); + } + + private static unsafe void SwizzleFormat(Span buffer, in DdsPixelFormat toFormat, in DdsPixelFormat fromFormat) + where T : unmanaged, IBinaryInteger + { + if (toFormat.RGBBitCount != sizeof(T) * 8) + throw new ArgumentException(null, nameof(toFormat)); - header.Write(writer); - writer.Write(buffer); + if (fromFormat.RGBBitCount != sizeof(T) * 8) + throw new ArgumentException(null, nameof(fromFormat)); + + for (int i = 0; i < buffer.Length; i += sizeof(T)) + { + var span = buffer[i..]; + var chan = MemoryMarshal.Read(span); + + // Read channels + T r = GetChannel(chan, fromFormat.RBitMask); + T g = GetChannel(chan, fromFormat.GBitMask); + T b = GetChannel(chan, fromFormat.BBitMask); + T a = GetChannel(chan, fromFormat.ABitMask); + + // Write channels + MemoryMarshal.Write(span, + CreateChannel(r, toFormat.RBitMask) | + CreateChannel(g, toFormat.GBitMask) | + CreateChannel(b, toFormat.BBitMask) | + CreateChannel(a, toFormat.ABitMask) + ); } - else + + static T GetChannel(T channels, uint mask) { - throw new ArgumentException($"Unknown platform '{platform}'."); + return (channels & T.CreateTruncating(mask)) >>> BitOperations.TrailingZeroCount(mask); + } + + static T CreateChannel(T value, uint mask) + { + return (value << BitOperations.TrailingZeroCount(mask)) & T.CreateTruncating(mask); } } @@ -149,24 +227,72 @@ private enum Platform PS4, } + private enum TextureUsage + { + Interface = 1, + World = 3 + } + private enum TextureFormat { - DXT5 = 1, - DXT1 = 3 + Unknown, + DXT1, + DXT5, + B8G8R8A8, + B4G4R4A4, + } + + private readonly struct CellTextureFormat + { + private readonly byte _value; + + public readonly bool IsSwizzled => !Flags.HasFlag(CellTextureFlags.Linear); + + public readonly bool IsCompressed => ColorType switch + { + CellColorType.DXT1 => true, + CellColorType.DXT45 => true, + _ => false + }; + + public readonly CellColorType ColorType => (CellColorType)(_value & ~(0x60)); + + public readonly CellTextureFlags Flags => (CellTextureFlags)(_value & (0x60)); + + public CellTextureFormat(byte format) + { + _value = format; + } + + public CellTextureFormat(CellColorType color, CellTextureFlags flags) + { + _value = (byte)((byte)color | (byte)flags); + } + } + + private enum CellColorType + { + A4B4G4R4 = 0x83, + A8B8G8R8 = 0x85, + DXT1 = 0x86, + DXT45 = 0x88, } - private enum PS3TextureFormat + [Flags] + private enum CellTextureFlags { - ARGB = 1, - DXT1 = 3, + None = 0, + Linear = 0x20, } private static int GetBlockSize(TextureFormat format) { return format switch { - TextureFormat.DXT5 => 16, TextureFormat.DXT1 => 8, + TextureFormat.DXT5 => 16, + TextureFormat.B8G8R8A8 => 4, + TextureFormat.B4G4R4A4 => 2, _ => throw new ArgumentOutOfRangeException(nameof(format)) }; } @@ -175,12 +301,23 @@ private static uint GetFourCC(TextureFormat format) { return format switch { - TextureFormat.DXT5 => 'D' | ('X' << 8) | ('T' << 16) | ('5' << 24), TextureFormat.DXT1 => 'D' | ('X' << 8) | ('T' << 16) | ('1' << 24), + TextureFormat.DXT5 => 'D' | ('X' << 8) | ('T' << 16) | ('5' << 24), _ => throw new ArgumentOutOfRangeException(nameof(format)) }; } + private static void CellUnSwizzle(ReadOnlySpan input, Span output, int width, int height, int blockSize) + { + for (int i = 0; i < width * height; i++) + { + int pixel = MortonReorder(i, width, height); + var source = input.Slice(i * blockSize, blockSize); + var destination = output.Slice(pixel * blockSize, blockSize); + source.CopyTo(destination); + } + } + // Swizzle code from PDTools https://github.com/Nenkai/PDTools/blob/master/PDTools.Files/Textures/PS4/OrbisTexture.cs#L101 private static void OrbisUnSwizzle(ReadOnlySpan input, Span output, int width, int height, int blockSize) {