Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for games that use DDS textures #1929

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 149 additions & 7 deletions UndertaleModLib/Util/GMImage.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using ICSharpCode.SharpZipLib.BZip2;
using ImageMagick;
using System;
using System;
using System.Buffers.Binary;
using System.IO;
using ICSharpCode.SharpZipLib.BZip2;
using ImageMagick;

namespace UndertaleModLib.Util;

Expand Down Expand Up @@ -34,7 +34,12 @@ public enum ImageFormat
/// <summary>
/// BZip2 compression applied on top of GameMaker's custom variant of the QOI image file format.
/// </summary>
Bz2Qoi
Bz2Qoi,

/// <summary>
/// DDS file format.
/// </summary>
Dds,
}

/// <summary>
Expand Down Expand Up @@ -77,6 +82,11 @@ public enum ImageFormat
/// </summary>
private static ReadOnlySpan<byte> MagicBz2Footer => new byte[] { 0x17, 0x72, 0x45, 0x38, 0x50, 0x90 };

/// <summary>
/// DDS file format magic.
/// </summary>
private static ReadOnlySpan<byte> MagicDds => "DDS "u8;

/// <summary>
/// Backing data for the image, whether compressed or not.
/// </summary>
Expand Down Expand Up @@ -335,6 +345,61 @@ public static GMImage FromBinaryReader(IBinaryReader reader, long maxEndOfStream
return FromQoi(reader.ReadBytes(12 + (int)compressedLength));
}

// DDS
if (header.StartsWith(MagicDds))
{
// Size int skipped because 8 bytes were already read
uint flags = reader.ReadUInt32();
uint height = reader.ReadUInt32();

//uint width = reader.ReadUInt32();
reader.Position += 4;

uint pitchOrLinearSize = reader.ReadUInt32();

//uint depth = reader.ReadUInt32();
//uint mipMapCount = reader.ReadUInt32();
//byte[] reserved1 = reader.ReadBytes(4 * 11);
//uint pixelFormatSize = reader.ReadUInt32();
reader.Position += 56;

uint pixelFormatFlags = reader.ReadUInt32();
uint pixelFormatFourCC = reader.ReadUInt32();

// TODO: Check caps for DDSCAPS_COMPLEX (when there's extra data afterwards)

// Skip to end of header
reader.Position += 40;

// Check if DX10 header is present and skip it
// DDPF_FOURCC == 0x4
// DX10 == 0x30315844
if ((pixelFormatFlags & 0x4) != 0 && pixelFormatFourCC == 0x30315844)
reader.Position += 20;

// Check if that int is the size or pitch
// DDSD_LINEARSIZE == 0x80000
int size = (int)(reader.Position - startAddress);
if ((flags & 0x80000) != 0)
size += (int)pitchOrLinearSize;
else
size += (int)(pitchOrLinearSize * height);

// Read entire data
reader.Position = startAddress;
byte[] bytes = reader.ReadBytes(size);

// Check if rest of bytes are 0x00 padded
byte[] paddingBytes = reader.ReadBytes((int)(maxEndOfStreamPosition - reader.Position));
for (int i=0; i<paddingBytes.Length; i++)
{
if (paddingBytes[i] != 0)
throw new IOException("Non-zero bytes in padding");
}

return FromDds(bytes);
}

throw new IOException("Failed to recognize any known image header");
}

Expand Down Expand Up @@ -452,6 +517,31 @@ public static GMImage FromQoi(byte[] data)
return new GMImage(ImageFormat.Qoi, width, height, data);
}

/// <summary>
/// Creates a <see cref="GMImage"/> of DDS format, wrapping around the provided byte array containing DDS data.
/// </summary>
/// <param name="data">Byte array of DDS data.</param>
public static GMImage FromDds(byte[] data)
{
ArgumentNullException.ThrowIfNull(data);

ReadOnlySpan<byte> span = data.AsSpan();

// Get height and width
int height = (int)BinaryPrimitives.ReadUInt32LittleEndian(span[12..16]);
int width = (int)BinaryPrimitives.ReadUInt32LittleEndian(span[16..20]);

// Create wrapper image
return new GMImage(ImageFormat.Dds, width, height, data);
}

private static void AddMagickToPngSettings(MagickReadSettings settings)
{
settings.SetDefine(MagickFormat.Png32, "compression-level", 4);
settings.SetDefine(MagickFormat.Png32, "compression-filter", 5);
settings.SetDefine(MagickFormat.Png32, "compression-strategy", 2);
}

// Settings to be used for raw data, and when encoding a PNG
private MagickReadSettings GetMagickRawToPngSettings()
{
Expand All @@ -462,9 +552,18 @@ private MagickReadSettings GetMagickRawToPngSettings()
Format = MagickFormat.Bgra,
Compression = CompressionMethod.NoCompression
};
settings.SetDefine(MagickFormat.Png32, "compression-level", 4);
settings.SetDefine(MagickFormat.Png32, "compression-filter", 5);
settings.SetDefine(MagickFormat.Png32, "compression-strategy", 2);
AddMagickToPngSettings(settings);
return settings;
}

// Settings to be used for decoding DDS, and when encoding a PNG
private MagickReadSettings GetMagickDdsToPngSettings()
{
var settings = new MagickReadSettings()
{
Format = MagickFormat.Dds,
};
AddMagickToPngSettings(settings);
return settings;
}

Expand Down Expand Up @@ -518,6 +617,15 @@ public void SavePng(Stream stream)
rawImage.SavePng(stream);
break;
}
case ImageFormat.Dds:
{
// Create image using ImageMagick, and save it as PNG format
using var image = new MagickImage(_data, GetMagickDdsToPngSettings());
image.Alpha(AlphaOption.Set);
image.Format = MagickFormat.Png32;
image.Write(stream);
break;
}
default:
throw new InvalidOperationException($"Unknown format {Format}");
}
Expand All @@ -536,6 +644,7 @@ public GMImage ConvertToFormat(ImageFormat format, MemoryStream sharedStream = n
ImageFormat.Png => ConvertToPng(),
ImageFormat.Qoi => ConvertToQoi(),
ImageFormat.Bz2Qoi => ConvertToBz2Qoi(sharedStream),
ImageFormat.Dds => ConvertToDds(),
_ => throw new ArgumentOutOfRangeException(nameof(format)),
};
}
Expand All @@ -553,6 +662,7 @@ public GMImage ConvertToRawBgra()
return this;
}
case ImageFormat.Png:
case ImageFormat.Dds:
{
// Convert image to raw byte array
var image = new MagickImage(_data);
Expand Down Expand Up @@ -632,6 +742,14 @@ public GMImage ConvertToPng()
// Convert raw image to PNG
return rawImage.ConvertToPng();
}
case ImageFormat.Dds:
{
// Create image using ImageMagick, and convert it to PNG format
using var image = new MagickImage(_data, GetMagickDdsToPngSettings());
image.Alpha(AlphaOption.Set);
image.Format = MagickFormat.Png32;
return new GMImage(ImageFormat.Png, Width, Height, image.ToByteArray());
}
}

throw new InvalidOperationException($"Unknown source format {Format}");
Expand All @@ -647,6 +765,7 @@ public GMImage ConvertToQoi()
case ImageFormat.RawBgra:
case ImageFormat.Png:
case ImageFormat.Bz2Qoi:
case ImageFormat.Dds:
{
// Encode image as QOI
return new GMImage(ImageFormat.Qoi, Width, Height, QoiConverter.GetArrayFromImage(this, false));
Expand Down Expand Up @@ -706,6 +825,7 @@ public GMImage ConvertToBz2Qoi(MemoryStream sharedStream = null)
{
case ImageFormat.RawBgra:
case ImageFormat.Png:
case ImageFormat.Dds:
{
// Encode image as QOI, first
byte[] data = QoiConverter.GetArrayFromImage(this, false);
Expand All @@ -726,6 +846,17 @@ public GMImage ConvertToBz2Qoi(MemoryStream sharedStream = null)
throw new InvalidOperationException($"Unknown source format {Format}");
}

/// <summary>
/// Same as <see cref="ConvertToPng"/>.
/// </summary>
/// <remarks>This is supposd to return the image converted to <see cref="ImageFormat.Dds"/> format, but that's not implemented yet.</remarks>
/// <returns></returns>
public GMImage ConvertToDds()
{
// TODO: Actually convert to DDS
return ConvertToPng();
}

/// <summary>
/// Returns the raw BGRA32 pixel data of this image, which can be modified.
/// </summary>
Expand Down Expand Up @@ -756,6 +887,7 @@ public void WriteToBinaryWriter(BinaryWriter writer, bool gm2022_5)
case ImageFormat.RawBgra:
case ImageFormat.Png:
case ImageFormat.Qoi:
case ImageFormat.Dds:
// Data is stored identically to file format, so write it verbatim
writer.Write(_data);
break;
Expand Down Expand Up @@ -843,6 +975,16 @@ public MagickImage GetMagickImage()
case ImageFormat.Bz2Qoi:
// Convert to raw data, then parse that
return ConvertToRawBgra().GetMagickImage();
case ImageFormat.Dds:
{
// Parse the DDS data
MagickReadSettings settings = new()
{
Format = MagickFormat.Dds
};
MagickImage image = new(_data, settings);
return image;
}
}

throw new InvalidOperationException($"Unknown format {Format}");
Expand Down
30 changes: 16 additions & 14 deletions UndertaleModTool/Editors/UndertaleEmbeddedTextureEditor.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,20 @@
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Drawing;
using System.Windows.Threading;
using ImageMagick;
using Microsoft.Win32;
using UndertaleModLib.Models;
using UndertaleModLib.Util;
using System.Globalization;
using UndertaleModLib;
using UndertaleModTool.Windows;
using System.Windows.Threading;
using ImageMagick;
using System.ComponentModel;

namespace UndertaleModTool
{
Expand Down Expand Up @@ -252,9 +244,19 @@ private void Import_Click(object sender, RoutedEventArgs e)
mainWindow.ShowWarning("WARNING: Texture page dimensions are not powers of 2. Sprite blurring is very likely in-game.", "Unexpected texture dimensions");
}

var previousFormat = target.TextureData.Image.Format;

// Import image
target.TextureData.Image = image;

var currentFormat = target.TextureData.Image.Format;

// If texture was DDS, warn user that texture has been converted to PNG
if (previousFormat == GMImage.ImageFormat.Dds && currentFormat == GMImage.ImageFormat.Png)
{
mainWindow.ShowMessage($"{target} was converted into PNG format since we don't support converting images into DDS format. This might have performance issues in the game.");
}

// Update width/height properties in the UI
TexWidth.GetBindingExpression(TextBox.TextProperty)?.UpdateTarget();
TexHeight.GetBindingExpression(TextBox.TextProperty)?.UpdateTarget();
Expand Down
27 changes: 19 additions & 8 deletions UndertaleModTool/Editors/UndertaleTexturePageItemEditor.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
using Microsoft.Win32;
using System;
using System;
using System.ComponentModel;
using System.Windows;
using UndertaleModLib.Models;
using UndertaleModLib.Util;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Data;
using UndertaleModTool.Windows;
using ImageMagick;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.ComponentModel;
using ImageMagick;
using Microsoft.Win32;
using UndertaleModLib.Models;
using UndertaleModLib.Util;
using UndertaleModTool.Windows;

namespace UndertaleModTool
{
Expand Down Expand Up @@ -150,8 +150,19 @@ private void Import_Click(object sender, RoutedEventArgs e)
{
using MagickImage image = TextureWorker.ReadBGRAImageFromFile(dlg.FileName);
UndertaleTexturePageItem item = DataContext as UndertaleTexturePageItem;

var previousFormat = item.TexturePage.TextureData.Image.Format;

item.ReplaceTexture(image);

var currentFormat = item.TexturePage.TextureData.Image.Format;

// If texture was DDS, warn user that texture has been converted to PNG
if (previousFormat == GMImage.ImageFormat.Dds && currentFormat == GMImage.ImageFormat.Png)
{
mainWindow.ShowMessage($"{item.TexturePage} was converted into PNG format since we don't support converting images into DDS format. This might have performance issues in the game.");
}

// Refresh the image of "ItemDisplay"
if (ItemDisplay.FindName("RenderAreaBorder") is not Border border)
return;
Expand Down
Loading