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 1 commit
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
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)));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If possible, we should be finding the length of the image data using the image contents, rather than using maxEndOfStreamPosition, which can result in unnecessary padding when re-saving with modifications.

Copy link
Contributor Author

@luizzeroxis luizzeroxis Sep 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't think about that! Sadly there's no easy way to find the length in this format. I changed it so it can get the size of file but only without the extra bdata2 field, because getting that is even harder.


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)
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure if this is going to stick around in the long run, but this can probably be marked static.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally

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
Loading