Skip to content

Commit

Permalink
Add support for games that use DDS textures
Browse files Browse the repository at this point in the history
  • Loading branch information
luizzeroxis committed Sep 27, 2024
1 parent 14a7265 commit de9476c
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 29 deletions.
109 changes: 102 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,14 @@ public static GMImage FromBinaryReader(IBinaryReader reader, long maxEndOfStream
return FromQoi(reader.ReadBytes(12 + (int)compressedLength));
}

// DDS
if (header.StartsWith(MagicDds))
{
// Read entire image
reader.Position = startAddress;
return FromDds(reader.ReadBytes((int)(maxEndOfStreamPosition - startAddress)));
}

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

Expand Down Expand Up @@ -452,6 +470,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 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 +505,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 +570,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 +597,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 +615,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 +695,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 +718,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 +778,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 +799,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 +840,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 +928,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

0 comments on commit de9476c

Please sign in to comment.