Skip to content

Commit

Permalink
Improve tex2dds command
Browse files Browse the repository at this point in the history
  • Loading branch information
DaZombieKiller committed May 26, 2024
1 parent 12b84f5 commit 0b43e36
Showing 1 changed file with 211 additions and 74 deletions.
285 changes: 211 additions & 74 deletions tools/TLTool/TextureDdsConvertCommand.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Numerics;
using System.Runtime.InteropServices;

namespace TLTool;

Expand Down Expand Up @@ -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<uint>(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<ushort>(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<T>(Span<byte> buffer, in DdsPixelFormat toFormat, in DdsPixelFormat fromFormat)
where T : unmanaged, IBinaryInteger<T>
{
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<T>(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);
}
}

Expand All @@ -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))
};
}
Expand All @@ -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<byte> input, Span<byte> 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<byte> input, Span<byte> output, int width, int height, int blockSize)
{
Expand Down

0 comments on commit 0b43e36

Please sign in to comment.