Initial Texture rework.

This commit is contained in:
Ottermandias 2023-08-08 01:10:57 +02:00
parent 2f836426d6
commit e24a535a93
16 changed files with 831 additions and 489 deletions

@ -1 +1 @@
Subproject commit e3d26f16234a4295bf3c7802d87ce43293c6ffe0 Subproject commit 8d61845cd900fc0a3b58d475c43303b13c1165f4

View file

@ -0,0 +1,111 @@
using System;
using System.Numerics;
using Lumina.Data.Files;
using OtterTex;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace Penumbra.Import.Textures;
public readonly struct BaseImage : IDisposable
{
public readonly object? Image;
public BaseImage(ScratchImage scratch)
=> Image = scratch;
public BaseImage(Image<Rgba32> image)
=> Image = image;
public static implicit operator BaseImage(ScratchImage scratch)
=> new(scratch);
public static implicit operator BaseImage(Image<Rgba32> img)
=> new(img);
public ScratchImage? AsDds
=> Image as ScratchImage;
public Image<Rgba32>? AsPng
=> Image as Image<Rgba32>;
public TexFile? AsTex
=> Image as TexFile;
public TextureType Type
=> Image switch
{
null => TextureType.Unknown,
ScratchImage => TextureType.Dds,
Image<Rgba32> => TextureType.Png,
_ => TextureType.Unknown,
};
public void Dispose()
=> (Image as IDisposable)?.Dispose();
/// <summary> Obtain RGBA pixel data for the given image (not including any mip maps.) </summary>
public (byte[] Rgba, int Width, int Height) GetPixelData()
{
switch (Image)
{
case null: return (Array.Empty<byte>(), 0, 0);
case ScratchImage scratch:
{
var rgba = scratch.GetRGBA(out var f).ThrowIfError(f);
return (rgba.Pixels[..(f.Meta.Width * f.Meta.Height * (f.Meta.Format.BitsPerPixel() / 8))].ToArray(), f.Meta.Width,
f.Meta.Height);
}
case Image<Rgba32> img:
{
var ret = new byte[img.Height * img.Width * 4];
img.CopyPixelDataTo(ret);
return (ret, img.Width, img.Height);
}
default: return (Array.Empty<byte>(), 0, 0);
}
}
public (int Width, int Height) Dimensions
=> Image switch
{
null => (0, 0),
ScratchImage scratch => (scratch.Meta.Width, scratch.Meta.Height),
Image<Rgba32> img => (img.Width, img.Height),
_ => (0, 0),
};
public int Width
=> Dimensions.Width;
public int Height
=> Dimensions.Height;
public Vector2 ImageSize
{
get
{
var (width, height) = Dimensions;
return new Vector2(width, height);
}
}
public DXGIFormat Format
=> Image switch
{
null => DXGIFormat.Unknown,
ScratchImage s => s.Meta.Format,
TexFile t => t.Header.Format.ToDXGI(),
Image<Rgba32> => DXGIFormat.B8G8R8X8UNorm,
_ => DXGIFormat.Unknown,
};
public int MipMaps
=> Image switch
{
null => 0,
ScratchImage s => s.Meta.MipLevels,
TexFile t => t.Header.MipLevels,
_ => 1,
};
}

View file

@ -16,14 +16,13 @@ public partial class CombinedTexture
private bool _invertLeft = false; private bool _invertLeft = false;
private bool _invertRight = false; private bool _invertRight = false;
private int _offsetX = 0; private int _offsetX = 0;
private int _offsetY = 0; private int _offsetY = 0;
private Vector4 DataLeft( int offset ) private Vector4 DataLeft( int offset )
=> CappedVector( _left.RGBAPixels, offset, _multiplierLeft, _invertLeft ); => CappedVector( _left.RgbaPixels, offset, _multiplierLeft, _invertLeft );
private Vector4 DataRight( int offset ) private Vector4 DataRight( int offset )
=> CappedVector( _right.RGBAPixels, offset, _multiplierRight, _invertRight ); => CappedVector( _right.RgbaPixels, offset, _multiplierRight, _invertRight );
private Vector4 DataRight( int x, int y ) private Vector4 DataRight( int x, int y )
{ {
@ -35,7 +34,7 @@ public partial class CombinedTexture
} }
var offset = ( y * _right.TextureWrap!.Width + x ) * 4; var offset = ( y * _right.TextureWrap!.Width + x ) * 4;
return CappedVector( _right.RGBAPixels, offset, _multiplierRight, _invertRight ); return CappedVector( _right.RgbaPixels, offset, _multiplierRight, _invertRight );
} }
private void AddPixelsMultiplied( int y, ParallelLoopState _ ) private void AddPixelsMultiplied( int y, ParallelLoopState _ )
@ -49,10 +48,10 @@ public partial class CombinedTexture
var rgba = alpha == 0 var rgba = alpha == 0
? new Rgba32() ? new Rgba32()
: new Rgba32( ( ( right * right.W + left * left.W * ( 1 - right.W ) ) / alpha ) with { W = alpha } ); : new Rgba32( ( ( right * right.W + left * left.W * ( 1 - right.W ) ) / alpha ) with { W = alpha } );
_centerStorage.RGBAPixels[ offset ] = rgba.R; _centerStorage.RgbaPixels[ offset ] = rgba.R;
_centerStorage.RGBAPixels[ offset + 1 ] = rgba.G; _centerStorage.RgbaPixels[ offset + 1 ] = rgba.G;
_centerStorage.RGBAPixels[ offset + 2 ] = rgba.B; _centerStorage.RgbaPixels[ offset + 2 ] = rgba.B;
_centerStorage.RGBAPixels[ offset + 3 ] = rgba.A; _centerStorage.RgbaPixels[ offset + 3 ] = rgba.A;
} }
} }
@ -63,10 +62,10 @@ public partial class CombinedTexture
var offset = ( _left.TextureWrap!.Width * y + x ) * 4; var offset = ( _left.TextureWrap!.Width * y + x ) * 4;
var left = DataLeft( offset ); var left = DataLeft( offset );
var rgba = new Rgba32( left ); var rgba = new Rgba32( left );
_centerStorage.RGBAPixels[ offset ] = rgba.R; _centerStorage.RgbaPixels[ offset ] = rgba.R;
_centerStorage.RGBAPixels[ offset + 1 ] = rgba.G; _centerStorage.RgbaPixels[ offset + 1 ] = rgba.G;
_centerStorage.RGBAPixels[ offset + 2 ] = rgba.B; _centerStorage.RgbaPixels[ offset + 2 ] = rgba.B;
_centerStorage.RGBAPixels[ offset + 3 ] = rgba.A; _centerStorage.RgbaPixels[ offset + 3 ] = rgba.A;
} }
} }
@ -77,10 +76,10 @@ public partial class CombinedTexture
var offset = ( _right.TextureWrap!.Width * y + x ) * 4; var offset = ( _right.TextureWrap!.Width * y + x ) * 4;
var left = DataRight( offset ); var left = DataRight( offset );
var rgba = new Rgba32( left ); var rgba = new Rgba32( left );
_centerStorage.RGBAPixels[ offset ] = rgba.R; _centerStorage.RgbaPixels[ offset ] = rgba.R;
_centerStorage.RGBAPixels[ offset + 1 ] = rgba.G; _centerStorage.RgbaPixels[ offset + 1 ] = rgba.G;
_centerStorage.RGBAPixels[ offset + 2 ] = rgba.B; _centerStorage.RgbaPixels[ offset + 2 ] = rgba.B;
_centerStorage.RGBAPixels[ offset + 3 ] = rgba.A; _centerStorage.RgbaPixels[ offset + 3 ] = rgba.A;
} }
} }
@ -90,8 +89,8 @@ public partial class CombinedTexture
var (width, height) = _left.IsLoaded var (width, height) = _left.IsLoaded
? ( _left.TextureWrap!.Width, _left.TextureWrap!.Height ) ? ( _left.TextureWrap!.Width, _left.TextureWrap!.Height )
: ( _right.TextureWrap!.Width, _right.TextureWrap!.Height ); : ( _right.TextureWrap!.Width, _right.TextureWrap!.Height );
_centerStorage.RGBAPixels = new byte[width * height * 4]; _centerStorage.RgbaPixels = new byte[width * height * 4];
_centerStorage.Type = Texture.FileType.Bitmap; _centerStorage.Type = TextureType.Bitmap;
if( _left.IsLoaded ) if( _left.IsLoaded )
{ {
Parallel.For( 0, height, _right.IsLoaded ? AddPixelsMultiplied : MultiplyPixelsLeft ); Parallel.For( 0, height, _right.IsLoaded ? AddPixelsMultiplied : MultiplyPixelsLeft );
@ -103,7 +102,6 @@ public partial class CombinedTexture
return ( width, height ); return ( width, height );
} }
private static Vector4 CappedVector( IReadOnlyList< byte > bytes, int offset, Matrix4x4 transform, bool invert ) private static Vector4 CappedVector( IReadOnlyList< byte > bytes, int offset, Matrix4x4 transform, bool invert )
{ {
if( bytes.Count == 0 ) if( bytes.Count == 0 )

View file

@ -1,14 +1,5 @@
using System; using System;
using System.IO;
using System.Numerics; using System.Numerics;
using Dalamud.Interface;
using Lumina.Data.Files;
using OtterTex;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using DalamudUtil = Dalamud.Utility.Util;
using Image = SixLabors.ImageSharp.Image;
namespace Penumbra.Import.Textures; namespace Penumbra.Import.Textures;
@ -38,170 +29,58 @@ public partial class CombinedTexture : IDisposable
private readonly Texture _centerStorage = new(); private readonly Texture _centerStorage = new();
public Guid SaveGuid { get; private set; } = Guid.Empty;
public bool IsLoaded public bool IsLoaded
=> _mode != Mode.Empty; => _mode != Mode.Empty;
public bool IsLeftCopy
=> _mode == Mode.LeftCopy;
public Exception? SaveException { get; private set; } = null; public bool IsLeftCopy
=> _mode == Mode.LeftCopy;
public void Draw( UiBuilder builder, Vector2 size ) public void Draw(TextureManager textures, Vector2 size)
{ {
if( _mode == Mode.Custom && !_centerStorage.IsLoaded ) if (_mode == Mode.Custom && !_centerStorage.IsLoaded)
{ {
var (width, height) = CombineImage(); var (width, height) = CombineImage();
_centerStorage.TextureWrap = _centerStorage.TextureWrap = textures.LoadTextureWrap(_centerStorage.RgbaPixels, width, height);
builder.LoadImageRaw( _centerStorage.RGBAPixels, width, height, 4 );
} }
_current?.Draw( size ); if (_current != null)
TextureDrawer.Draw(_current, size);
} }
public void SaveAsPng( string path ) public void SaveAsPng(TextureManager textures, string path)
{ {
if( !IsLoaded || _current == null ) if (!IsLoaded || _current == null)
{
return; return;
}
try SaveGuid = textures.SavePng(_current.BaseImage, path, _current.RgbaPixels, _current.TextureWrap!.Width, _current.TextureWrap!.Height);
{
var image = Image.LoadPixelData< Rgba32 >( _current.RGBAPixels, _current.TextureWrap!.Width,
_current.TextureWrap!.Height );
image.Save( path, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression } );
SaveException = null;
}
catch( Exception e )
{
SaveException = e;
}
} }
private void SaveAs( string path, TextureSaveType type, bool mipMaps, bool writeTex ) private void SaveAs(TextureManager textures, string path, TextureSaveType type, bool mipMaps, bool writeTex)
{ {
if( _current == null || _mode == Mode.Empty ) if (!IsLoaded || _current == null)
{
return; return;
}
try SaveGuid = textures.SaveAs(type, mipMaps, writeTex, _current.BaseImage, path, _current.RgbaPixels, _current.TextureWrap!.Width,
{ _current.TextureWrap!.Height);
if( _current.BaseImage is not ScratchImage s )
{
s = ScratchImage.FromRGBA( _current.RGBAPixels, _current.TextureWrap!.Width,
_current.TextureWrap!.Height, out var i ).ThrowIfError( i );
}
var tex = type switch
{
TextureSaveType.AsIs => _current.Type is Texture.FileType.Bitmap or Texture.FileType.Png ? CreateUncompressed( s, mipMaps ) : s,
TextureSaveType.Bitmap => CreateUncompressed( s, mipMaps ),
TextureSaveType.BC3 => CreateCompressed( s, mipMaps, false ),
TextureSaveType.BC7 => CreateCompressed( s, mipMaps, true ),
_ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ),
};
if( !writeTex )
{
tex.SaveDDS( path );
}
else
{
SaveTex( path, tex );
}
SaveException = null;
}
catch( Exception e )
{
SaveException = e;
}
} }
private static void SaveTex( string path, ScratchImage input ) public void SaveAsTex(TextureManager textures, string path, TextureSaveType type, bool mipMaps)
{ => SaveAs(textures, path, type, mipMaps, true);
var header = input.ToTexHeader();
if( header.Format == TexFile.TextureFormat.Unknown )
{
throw new Exception( $"Could not save tex file with format {input.Meta.Format}, not convertible to a valid .tex formats." );
}
using var stream = File.Open( path, File.Exists(path) ? FileMode.Truncate : FileMode.CreateNew); public void SaveAsDds(TextureManager textures, string path, TextureSaveType type, bool mipMaps)
using var w = new BinaryWriter( stream ); => SaveAs(textures, path, type, mipMaps, false);
header.Write( w );
w.Write( input.Pixels );
}
private static ScratchImage AddMipMaps( ScratchImage input, bool mipMaps )
{
if( !mipMaps )
{
return input;
}
var numMips = Math.Min( 13, 1 + BitOperations.Log2( ( uint )Math.Max( input.Meta.Width, input.Meta.Height ) ) );
var ec = input.GenerateMipMaps( out var ret, numMips, ( DalamudUtil.IsLinux() ? FilterFlags.ForceNonWIC : 0 ) | FilterFlags.SeparateAlpha );
if (ec != ErrorCode.Ok)
{
throw new Exception( $"Could not create the requested {numMips} mip maps, maybe retry with the top-right checkbox unchecked:\n{ec}" );
}
return ret;
}
private static ScratchImage CreateUncompressed( ScratchImage input, bool mipMaps )
{
if( input.Meta.Format == DXGIFormat.B8G8R8A8UNorm )
{
return AddMipMaps( input, mipMaps );
}
if( input.Meta.Format.IsCompressed() )
{
input = input.Decompress( DXGIFormat.B8G8R8A8UNorm );
}
else
{
input = input.Convert( DXGIFormat.B8G8R8A8UNorm );
}
return AddMipMaps( input, mipMaps );
}
private static ScratchImage CreateCompressed( ScratchImage input, bool mipMaps, bool bc7 )
{
var format = bc7 ? DXGIFormat.BC7UNorm : DXGIFormat.BC3UNorm;
if( input.Meta.Format == format )
{
return input;
}
if( input.Meta.Format.IsCompressed() )
{
input = input.Decompress( DXGIFormat.B8G8R8A8UNorm );
}
input = AddMipMaps( input, mipMaps );
return input.Compress( format, CompressFlags.BC7Quick | CompressFlags.Parallel );
}
public void SaveAsTex( string path, TextureSaveType type, bool mipMaps )
=> SaveAs( path, type, mipMaps, true );
public void SaveAsDds( string path, TextureSaveType type, bool mipMaps )
=> SaveAs( path, type, mipMaps, false );
public CombinedTexture( Texture left, Texture right ) public CombinedTexture(Texture left, Texture right)
{ {
_left = left; _left = left;
_right = right; _right = right;
_left.Loaded += OnLoaded; _left.Loaded += OnLoaded;
_right.Loaded += OnLoaded; _right.Loaded += OnLoaded;
OnLoaded( false ); OnLoaded(false);
} }
public void Dispose() public void Dispose()
@ -211,20 +90,20 @@ public partial class CombinedTexture : IDisposable
_right.Loaded -= OnLoaded; _right.Loaded -= OnLoaded;
} }
private void OnLoaded( bool _ ) private void OnLoaded(bool _)
=> Update(); => Update();
public void Update() public void Update()
{ {
Clean(); Clean();
if( _left.IsLoaded ) if (_left.IsLoaded)
{ {
if( _right.IsLoaded ) if (_right.IsLoaded)
{ {
_current = _centerStorage; _current = _centerStorage;
_mode = Mode.Custom; _mode = Mode.Custom;
} }
else if( !_invertLeft && _multiplierLeft.IsIdentity ) else if (!_invertLeft && _multiplierLeft.IsIdentity)
{ {
_mode = Mode.LeftCopy; _mode = Mode.LeftCopy;
_current = _left; _current = _left;
@ -235,9 +114,9 @@ public partial class CombinedTexture : IDisposable
_mode = Mode.Custom; _mode = Mode.Custom;
} }
} }
else if( _right.IsLoaded ) else if (_right.IsLoaded)
{ {
if( !_invertRight && _multiplierRight.IsIdentity ) if (!_invertRight && _multiplierRight.IsIdentity)
{ {
_current = _right; _current = _right;
_mode = Mode.RightCopy; _mode = Mode.RightCopy;
@ -254,6 +133,7 @@ public partial class CombinedTexture : IDisposable
{ {
_centerStorage.Dispose(); _centerStorage.Dispose();
_current = null; _current = null;
SaveGuid = Guid.Empty;
_mode = Mode.Empty; _mode = Mode.Empty;
} }
} }

View file

@ -101,7 +101,7 @@ public static class TexFileParser
Height = (ushort)meta.Height, Height = (ushort)meta.Height,
Width = (ushort)meta.Width, Width = (ushort)meta.Width,
Depth = (ushort)Math.Max(meta.Depth, 1), Depth = (ushort)Math.Max(meta.Depth, 1),
MipLevels = (byte)Math.Min(meta.MipLevels, 12), MipLevels = (byte)Math.Min(meta.MipLevels, 13),
Format = meta.Format.ToTexFormat(), Format = meta.Format.ToTexFormat(),
Type = meta.Dimension switch Type = meta.Dimension switch
{ {

View file

@ -1,135 +1,61 @@
using System; using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using Dalamud.Data;
using Dalamud.Interface;
using ImGuiNET;
using ImGuiScene; using ImGuiScene;
using Lumina.Data.Files;
using OtterGui;
using OtterGui.Raii;
using OtterTex; using OtterTex;
using Penumbra.Services;
using Penumbra.String.Classes;
using Penumbra.UI;
using Penumbra.UI.Classes;
using SixLabors.ImageSharp.PixelFormats;
using Image = SixLabors.ImageSharp.Image;
namespace Penumbra.Import.Textures; namespace Penumbra.Import.Textures;
public enum TextureType
{
Unknown,
Dds,
Tex,
Png,
Bitmap,
}
public sealed class Texture : IDisposable public sealed class Texture : IDisposable
{ {
public enum FileType
{
Unknown,
Dds,
Tex,
Png,
Bitmap,
}
// Path to the file we tried to load. // Path to the file we tried to load.
public string Path = string.Empty; public string Path = string.Empty;
// Path for changing paths.
internal string? TmpPath;
// If the load failed, an exception is stored. // If the load failed, an exception is stored.
public Exception? LoadError = null; public Exception? LoadError = null;
// The pixels of the main image in RGBA order. // The pixels of the main image in RGBA order.
// Empty if LoadError != null or Path is empty. // Empty if LoadError != null or Path is empty.
public byte[] RGBAPixels = Array.Empty<byte>(); public byte[] RgbaPixels = Array.Empty<byte>();
// The ImGui wrapper to load the image. // The ImGui wrapper to load the image.
// null if LoadError != null or Path is empty. // null if LoadError != null or Path is empty.
public TextureWrap? TextureWrap = null; public TextureWrap? TextureWrap = null;
// The base image in whatever format it has. // The base image in whatever format it has.
public object? BaseImage = null; public BaseImage BaseImage;
// Original File Type. // Original File Type.
public FileType Type = FileType.Unknown; public TextureType Type = TextureType.Unknown;
// Whether the file is successfully loaded and drawable. // Whether the file is successfully loaded and drawable.
public bool IsLoaded public bool IsLoaded
=> TextureWrap != null; => TextureWrap != null;
public DXGIFormat Format public DXGIFormat Format
=> BaseImage switch => BaseImage.Format;
{
ScratchImage s => s.Meta.Format,
TexFile t => t.Header.Format.ToDXGI(),
_ => DXGIFormat.Unknown,
};
public int MipMaps public int MipMaps
=> BaseImage switch => BaseImage.MipMaps;
{
ScratchImage s => s.Meta.MipLevels,
TexFile t => t.Header.MipLevels,
_ => 1,
};
public void Draw(Vector2 size)
{
if (TextureWrap != null)
{
size = size.X < TextureWrap.Width
? size with { Y = TextureWrap.Height * size.X / TextureWrap.Width }
: new Vector2(TextureWrap.Width, TextureWrap.Height);
ImGui.Image(TextureWrap.ImGuiHandle, size);
DrawData();
}
else if (LoadError != null)
{
ImGui.TextUnformatted("Could not load file:");
ImGuiUtil.TextColored(Colors.RegexWarningBorder, LoadError.ToString());
}
}
public void DrawData()
{
using var table = ImRaii.Table("##data", 2, ImGuiTableFlags.SizingFixedFit);
ImGuiUtil.DrawTableColumn("Width");
ImGuiUtil.DrawTableColumn(TextureWrap!.Width.ToString());
ImGuiUtil.DrawTableColumn("Height");
ImGuiUtil.DrawTableColumn(TextureWrap!.Height.ToString());
ImGuiUtil.DrawTableColumn("File Type");
ImGuiUtil.DrawTableColumn(Type.ToString());
ImGuiUtil.DrawTableColumn("Bitmap Size");
ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(RGBAPixels.Length)} ({RGBAPixels.Length} Bytes)");
switch (BaseImage)
{
case ScratchImage s:
ImGuiUtil.DrawTableColumn("Format");
ImGuiUtil.DrawTableColumn(s.Meta.Format.ToString());
ImGuiUtil.DrawTableColumn("Mip Levels");
ImGuiUtil.DrawTableColumn(s.Meta.MipLevels.ToString());
ImGuiUtil.DrawTableColumn("Data Size");
ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(s.Pixels.Length)} ({s.Pixels.Length} Bytes)");
ImGuiUtil.DrawTableColumn("Number of Images");
ImGuiUtil.DrawTableColumn(s.Images.Length.ToString());
break;
case TexFile t:
ImGuiUtil.DrawTableColumn("Format");
ImGuiUtil.DrawTableColumn(t.Header.Format.ToString());
ImGuiUtil.DrawTableColumn("Mip Levels");
ImGuiUtil.DrawTableColumn(t.Header.MipLevels.ToString());
ImGuiUtil.DrawTableColumn("Data Size");
ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(t.ImageData.Length)} ({t.ImageData.Length} Bytes)");
break;
}
}
private void Clean() private void Clean()
{ {
RGBAPixels = Array.Empty<byte>(); RgbaPixels = Array.Empty<byte>();
TextureWrap?.Dispose(); TextureWrap?.Dispose();
TextureWrap = null; TextureWrap = null;
(BaseImage as IDisposable)?.Dispose(); BaseImage.Dispose();
BaseImage = null; BaseImage = new BaseImage();
Type = FileType.Unknown; Type = TextureType.Unknown;
Loaded?.Invoke(false); Loaded?.Invoke(false);
} }
@ -138,9 +64,9 @@ public sealed class Texture : IDisposable
public event Action<bool>? Loaded; public event Action<bool>? Loaded;
private void Load(DalamudServices dalamud, string path) public void Load(TextureManager textures, string path)
{ {
_tmpPath = null; TmpPath = null;
if (path == Path) if (path == Path)
return; return;
@ -151,13 +77,9 @@ public sealed class Texture : IDisposable
try try
{ {
var _ = System.IO.Path.GetExtension(Path).ToLowerInvariant() switch (BaseImage, Type) = textures.Load(path);
{ (RgbaPixels, var width, var height) = BaseImage.GetPixelData();
".dds" => LoadDds(dalamud), TextureWrap = textures.LoadTextureWrap(BaseImage, RgbaPixels);
".png" => LoadPng(dalamud),
".tex" => LoadTex(dalamud),
_ => throw new Exception($"Extension {System.IO.Path.GetExtension(Path)} unknown."),
};
Loaded?.Invoke(true); Loaded?.Invoke(true);
} }
catch (Exception e) catch (Exception e)
@ -167,130 +89,10 @@ public sealed class Texture : IDisposable
} }
} }
public void Reload(DalamudServices dalamud) public void Reload(TextureManager textures)
{ {
var path = Path; var path = Path;
Path = string.Empty; Path = string.Empty;
Load(dalamud, path); Load(textures, path);
}
private bool LoadDds(DalamudServices dalamud)
{
Type = FileType.Dds;
var scratch = ScratchImage.LoadDDS(Path);
BaseImage = scratch;
var rgba = scratch.GetRGBA(out var f).ThrowIfError(f);
RGBAPixels = rgba.Pixels[..(f.Meta.Width * f.Meta.Height * f.Meta.Format.BitsPerPixel() / 8)].ToArray();
CreateTextureWrap(dalamud.UiBuilder, f.Meta.Width, f.Meta.Height);
return true;
}
private bool LoadPng(DalamudServices dalamud)
{
Type = FileType.Png;
BaseImage = null;
using var stream = File.OpenRead(Path);
using var png = Image.Load<Rgba32>(stream);
RGBAPixels = new byte[png.Height * png.Width * 4];
png.CopyPixelDataTo(RGBAPixels);
CreateTextureWrap(dalamud.UiBuilder, png.Width, png.Height);
return true;
}
private bool LoadTex(DalamudServices dalamud)
{
Type = FileType.Tex;
using var stream = OpenTexStream(dalamud.GameData);
var scratch = TexFileParser.Parse(stream);
BaseImage = scratch;
var rgba = scratch.GetRGBA(out var f).ThrowIfError(f);
RGBAPixels = rgba.Pixels[..(f.Meta.Width * f.Meta.Height * (f.Meta.Format.BitsPerPixel() / 8))].ToArray();
CreateTextureWrap(dalamud.UiBuilder, scratch.Meta.Width, scratch.Meta.Height);
return true;
}
private Stream OpenTexStream(DataManager gameData)
{
if (System.IO.Path.IsPathRooted(Path))
return File.OpenRead(Path);
var file = gameData.GetFile(Path);
return file != null ? new MemoryStream(file.Data) : throw new Exception($"Unable to obtain \"{Path}\" from game files.");
}
private void CreateTextureWrap(UiBuilder builder, int width, int height)
=> TextureWrap = builder.LoadImageRaw(RGBAPixels, width, height, 4);
private string? _tmpPath;
public void PathSelectBox(DalamudServices dalamud, string label, string tooltip, IEnumerable<(string, bool)> paths, int skipPrefix)
{
ImGui.SetNextItemWidth(-0.0001f);
var startPath = Path.Length > 0 ? Path : "Choose a modded texture from this mod here...";
using var combo = ImRaii.Combo(label, startPath);
if (combo)
foreach (var ((path, game), idx) in paths.WithIndex())
{
if (game)
{
if (!dalamud.GameData.FileExists(path))
continue;
}
else if (!File.Exists(path))
{
continue;
}
using var id = ImRaii.PushId(idx);
using (var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value(), game))
{
var p = game ? $"--> {path}" : path[skipPrefix..];
if (ImGui.Selectable(p, path == startPath) && path != startPath)
Load(dalamud, path);
}
ImGuiUtil.HoverTooltip(game
? "This is a game path and refers to an unmanipulated file from your game data."
: "This is a path to a modded file on your file system.");
}
ImGuiUtil.HoverTooltip(tooltip);
}
public void PathInputBox(DalamudServices dalamud, string label, string hint, string tooltip, string startPath, FileDialogService fileDialog,
string defaultModImportPath)
{
_tmpPath ??= Path;
using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
new Vector2(UiHelpers.ScaleX3, ImGui.GetStyle().ItemSpacing.Y));
ImGui.SetNextItemWidth(-2 * ImGui.GetFrameHeight() - 7 * UiHelpers.Scale);
ImGui.InputTextWithHint(label, hint, ref _tmpPath, Utf8GamePath.MaxGamePathLength);
if (ImGui.IsItemDeactivatedAfterEdit())
Load(dalamud, _tmpPath);
ImGuiUtil.HoverTooltip(tooltip);
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Folder.ToIconString(), new Vector2(ImGui.GetFrameHeight()), string.Empty, false,
true))
{
if (defaultModImportPath.Length > 0)
startPath = defaultModImportPath;
var texture = this;
void UpdatePath(bool success, List<string> paths)
{
if (success && paths.Count > 0)
texture.Load(dalamud, paths[0]);
}
fileDialog.OpenFilePicker("Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath, false);
}
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), new Vector2(ImGui.GetFrameHeight()),
"Reload the currently selected path.", false,
true))
Reload(dalamud);
} }
} }

View file

@ -0,0 +1,139 @@
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using Dalamud.Interface;
using ImGuiNET;
using Lumina.Data.Files;
using OtterGui;
using OtterGui.Raii;
using OtterTex;
using Penumbra.String.Classes;
using Penumbra.UI;
using Penumbra.UI.Classes;
namespace Penumbra.Import.Textures;
public static class TextureDrawer
{
public static void Draw(Texture texture, Vector2 size)
{
if (texture.TextureWrap != null)
{
size = size.X < texture.TextureWrap.Width
? size with { Y = texture.TextureWrap.Height * size.X / texture.TextureWrap.Width }
: new Vector2(texture.TextureWrap.Width, texture.TextureWrap.Height);
ImGui.Image(texture.TextureWrap.ImGuiHandle, size);
DrawData(texture);
}
else if (texture.LoadError != null)
{
ImGui.TextUnformatted("Could not load file:");
ImGuiUtil.TextColored(Colors.RegexWarningBorder, texture.LoadError.ToString());
}
}
public static void PathSelectBox(TextureManager textures, Texture current, string label, string tooltip, IEnumerable<(string, bool)> paths,
int skipPrefix)
{
ImGui.SetNextItemWidth(-0.0001f);
var startPath = current.Path.Length > 0 ? current.Path : "Choose a modded texture from this mod here...";
using var combo = ImRaii.Combo(label, startPath);
if (combo)
foreach (var ((path, game), idx) in paths.WithIndex())
{
if (game)
{
if (!textures.GameFileExists(path))
continue;
}
else if (!File.Exists(path))
{
continue;
}
using var id = ImRaii.PushId(idx);
using (var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value(), game))
{
var p = game ? $"--> {path}" : path[skipPrefix..];
if (ImGui.Selectable(p, path == startPath) && path != startPath)
current.Load(textures, path);
}
ImGuiUtil.HoverTooltip(game
? "This is a game path and refers to an unmanipulated file from your game data."
: "This is a path to a modded file on your file system.");
}
ImGuiUtil.HoverTooltip(tooltip);
}
public static void PathInputBox(TextureManager textures, Texture current, ref string? tmpPath, string label, string hint, string tooltip,
string startPath, FileDialogService fileDialog, string defaultModImportPath)
{
tmpPath ??= current.Path;
using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
new Vector2(UiHelpers.ScaleX3, ImGui.GetStyle().ItemSpacing.Y));
ImGui.SetNextItemWidth(-2 * ImGui.GetFrameHeight() - 7 * UiHelpers.Scale);
ImGui.InputTextWithHint(label, hint, ref tmpPath, Utf8GamePath.MaxGamePathLength);
if (ImGui.IsItemDeactivatedAfterEdit())
current.Load(textures, tmpPath);
ImGuiUtil.HoverTooltip(tooltip);
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Folder.ToIconString(), new Vector2(ImGui.GetFrameHeight()), string.Empty, false,
true))
{
if (defaultModImportPath.Length > 0)
startPath = defaultModImportPath;
void UpdatePath(bool success, List<string> paths)
{
if (success && paths.Count > 0)
current.Load(textures, paths[0]);
}
fileDialog.OpenFilePicker("Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath, false);
}
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), new Vector2(ImGui.GetFrameHeight()),
"Reload the currently selected path.", false,
true))
current.Reload(textures);
}
private static void DrawData(Texture texture)
{
using var table = ImRaii.Table("##data", 2, ImGuiTableFlags.SizingFixedFit);
ImGuiUtil.DrawTableColumn("Width");
ImGuiUtil.DrawTableColumn(texture.TextureWrap!.Width.ToString());
ImGuiUtil.DrawTableColumn("Height");
ImGuiUtil.DrawTableColumn(texture.TextureWrap!.Height.ToString());
ImGuiUtil.DrawTableColumn("File Type");
ImGuiUtil.DrawTableColumn(texture.Type.ToString());
ImGuiUtil.DrawTableColumn("Bitmap Size");
ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(texture.RgbaPixels.Length)} ({texture.RgbaPixels.Length} Bytes)");
switch (texture.BaseImage.Image)
{
case ScratchImage s:
ImGuiUtil.DrawTableColumn("Format");
ImGuiUtil.DrawTableColumn(s.Meta.Format.ToString());
ImGuiUtil.DrawTableColumn("Mip Levels");
ImGuiUtil.DrawTableColumn(s.Meta.MipLevels.ToString());
ImGuiUtil.DrawTableColumn("Data Size");
ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(s.Pixels.Length)} ({s.Pixels.Length} Bytes)");
ImGuiUtil.DrawTableColumn("Number of Images");
ImGuiUtil.DrawTableColumn(s.Images.Length.ToString());
break;
case TexFile t:
ImGuiUtil.DrawTableColumn("Format");
ImGuiUtil.DrawTableColumn(t.Header.Format.ToString());
ImGuiUtil.DrawTableColumn("Mip Levels");
ImGuiUtil.DrawTableColumn(t.Header.MipLevels.ToString());
ImGuiUtil.DrawTableColumn("Data Size");
ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(t.ImageData.Length)} ({t.ImageData.Length} Bytes)");
break;
}
}
}

View file

@ -1,61 +0,0 @@
using System;
using System.IO;
using Lumina.Data.Files;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace Penumbra.Import.Textures;
public static class TextureImporter
{
private static void WriteHeader( byte[] target, int width, int height )
{
using var mem = new MemoryStream( target );
using var bw = new BinaryWriter( mem );
bw.Write( ( uint )TexFile.Attribute.TextureType2D );
bw.Write( ( uint )TexFile.TextureFormat.B8G8R8A8 );
bw.Write( ( ushort )width );
bw.Write( ( ushort )height );
bw.Write( ( ushort )1 );
bw.Write( ( ushort )1 );
bw.Write( 0 );
bw.Write( 1 );
bw.Write( 2 );
bw.Write( 80 );
for( var i = 1; i < 13; ++i )
{
bw.Write( 0 );
}
}
public static bool RgbaBytesToTex( byte[] rgba, int width, int height, out byte[] texData )
{
texData = Array.Empty< byte >();
if( rgba.Length != width * height * 4 )
{
return false;
}
texData = new byte[80 + width * height * 4];
WriteHeader( texData, width, height );
rgba.CopyTo( texData.AsSpan( 80 ) );
for( var i = 80; i < texData.Length; i += 4 )
(texData[ i ], texData[i + 2]) = (texData[ i + 2], texData[i]);
return true;
}
public static bool PngToTex( string inputFile, out byte[] texData )
{
using var file = File.OpenRead( inputFile );
var image = Image.Load< Bgra32 >( file );
var buffer = new byte[80 + image.Height * image.Width * 4];
WriteHeader( buffer, image.Width, image.Height );
var span = new Span< byte >( buffer, 80, buffer.Length - 80 );
image.CopyPixelDataTo( span );
texData = buffer;
return true;
}
}

View file

@ -0,0 +1,438 @@
using System;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Threading;
using Dalamud.Data;
using Dalamud.Interface;
using ImGuiScene;
using Lumina.Data.Files;
using OtterGui.Log;
using OtterGui.Tasks;
using OtterTex;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Image = SixLabors.ImageSharp.Image;
namespace Penumbra.Import.Textures;
public sealed class TextureManager : AsyncTaskManager
{
private readonly UiBuilder _uiBuilder;
private readonly DataManager _gameData;
public TextureManager(Logger logger, UiBuilder uiBuilder, DataManager gameData)
: base(logger)
{
_uiBuilder = uiBuilder;
_gameData = gameData;
}
public Guid SavePng(string input, string output)
=> Enqueue(new SavePngAction(this, input, output));
public Guid SavePng(BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0)
=> Enqueue(new SavePngAction(this, image, path, rgba, width, height));
public Guid SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, string input, string output)
=> Enqueue(new SaveAsAction(this, type, mipMaps, asTex, input, output));
public Guid SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, BaseImage image, string path, byte[]? rgba = null,
int width = 0, int height = 0)
=> Enqueue(new SaveAsAction(this, type, mipMaps, asTex, image, path, rgba, width, height));
private readonly struct ImageInputData
{
private readonly string? _inputPath;
private readonly BaseImage _image;
private readonly byte[]? _rgba;
private readonly int _width;
private readonly int _height;
public ImageInputData(string inputPath)
{
_inputPath = inputPath;
_image = new BaseImage();
_rgba = null;
_width = 0;
_height = 0;
}
public ImageInputData(BaseImage image, byte[]? rgba = null, int width = 0, int height = 0)
{
_inputPath = null;
_image = image.Width == 0 || image.Height == 0 ? new BaseImage() : image;
_rgba = rgba?.ToArray();
_width = width;
_height = height;
}
public (BaseImage Image, byte[]? Rgba, int Width, int Height) GetData(TextureManager textures)
{
if (_inputPath == null)
return (_image, _rgba, _width, _height);
if (!File.Exists(_inputPath))
throw new FileNotFoundException($"Input texture file {_inputPath} not Found.", _inputPath);
var (image, _) = textures.Load(_inputPath);
return (image, null, 0, 0);
}
public bool Equals(ImageInputData rhs)
{
if (_inputPath != null)
return string.Equals(_inputPath, rhs._inputPath, StringComparison.OrdinalIgnoreCase);
if (rhs._inputPath != null)
return false;
if (_image.Image != null)
return ReferenceEquals(_image.Image, rhs._image.Image);
return _width == rhs._width && _height == rhs._height && _rgba != null && rhs._rgba != null && _rgba.SequenceEqual(rhs._rgba);
}
public override string ToString()
=> _inputPath
?? _image.Type switch
{
TextureType.Unknown => $"Custom {_width} x {_height} RGBA Image",
TextureType.Dds => $"Custom {_width} x {_height} {_image.Format} Image",
TextureType.Tex => $"Custom {_width} x {_height} {_image.Format} Image",
TextureType.Png => $"Custom {_width} x {_height} .png Image",
TextureType.Bitmap => $"Custom {_width} x {_height} RGBA Image",
_ => "Unknown Image",
};
}
private class SavePngAction : IAction
{
private readonly TextureManager _textures;
private readonly string _outputPath;
private readonly ImageInputData _input;
public SavePngAction(TextureManager textures, string input, string output)
{
_textures = textures;
_input = new ImageInputData(input);
_outputPath = output;
}
public SavePngAction(TextureManager textures, BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0)
{
_textures = textures;
_input = new ImageInputData(image, rgba, width, height);
_outputPath = path;
}
public void Execute(CancellationToken cancel)
{
_textures.Logger.Information($"[{nameof(TextureManager)}] Saving {_input} as .png to {_outputPath}...");
var (image, rgba, width, height) = _input.GetData(_textures);
cancel.ThrowIfCancellationRequested();
Image<Rgba32>? png = null;
if (image.Type is TextureType.Unknown)
{
if (rgba != null && width > 0 && height > 0)
png = ConvertToPng(rgba, width, height).AsPng!;
}
else
{
png = ConvertToPng(image, cancel, rgba).AsPng!;
}
cancel.ThrowIfCancellationRequested();
png?.SaveAsync(_outputPath, cancel).Wait(cancel);
}
public bool Equals(IAction? other)
{
if (other is not SavePngAction rhs)
return false;
return string.Equals(_outputPath, rhs._outputPath, StringComparison.OrdinalIgnoreCase) && _input.Equals(rhs._input);
}
}
private class SaveAsAction : IAction
{
private readonly TextureManager _textures;
private readonly string _outputPath;
private readonly ImageInputData _input;
private readonly CombinedTexture.TextureSaveType _type;
private readonly bool _mipMaps;
private readonly bool _asTex;
public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, string input,
string output)
{
_textures = textures;
_input = new ImageInputData(input);
_outputPath = output;
_type = type;
_mipMaps = mipMaps;
_asTex = asTex;
}
public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, BaseImage image,
string path,
byte[]? rgba = null, int width = 0, int height = 0)
{
_textures = textures;
_input = new ImageInputData(image, rgba, width, height);
_outputPath = path;
_type = type;
_mipMaps = mipMaps;
_asTex = asTex;
}
public void Execute(CancellationToken cancel)
{
_textures.Logger.Information(
$"[{nameof(TextureManager)}] Saving {_input} as {_type} {(_asTex ? ".tex" : ".dds")} file{(_mipMaps ? " with mip maps" : string.Empty)} to {_outputPath}...");
var (image, rgba, width, height) = _input.GetData(_textures);
if (image.Type is TextureType.Unknown)
{
if (rgba != null && width > 0 && height > 0)
image = ConvertToDds(rgba, width, height);
else
return;
}
var dds = _type switch
{
CombinedTexture.TextureSaveType.AsIs when image.Type is TextureType.Png => ConvertToRgbaDds(image, _mipMaps, cancel, rgba,
width, height),
CombinedTexture.TextureSaveType.AsIs when image.Type is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps),
CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height),
CombinedTexture.TextureSaveType.BC3 => ConvertToCompressedDds(image, _mipMaps, false, cancel, rgba, width, height),
CombinedTexture.TextureSaveType.BC7 =>
ConvertToCompressedDds(image, _mipMaps, true, cancel, rgba, width, height),
_ => throw new Exception("Wrong save type."),
};
cancel.ThrowIfCancellationRequested();
if (_asTex)
SaveTex(_outputPath, dds.AsDds!);
else
dds.AsDds!.SaveDDS(_outputPath);
}
public bool Equals(IAction? other)
{
if (other is not SaveAsAction rhs)
return false;
return _type == rhs._type
&& _mipMaps == rhs._mipMaps
&& _asTex == rhs._asTex
&& string.Equals(_outputPath, rhs._outputPath, StringComparison.OrdinalIgnoreCase)
&& _input.Equals(rhs._input);
}
}
/// <summary> Load a texture wrap for a given image. </summary>
public TextureWrap LoadTextureWrap(BaseImage image, byte[]? rgba = null, int width = 0, int height = 0)
{
(rgba, width, height) = GetData(image, rgba, width, height);
return LoadTextureWrap(rgba, width, height);
}
/// <summary> Load a texture wrap for a given image. </summary>
public TextureWrap LoadTextureWrap(byte[] rgba, int width, int height)
=> _uiBuilder.LoadImageRaw(rgba, width, height, 4);
/// <summary> Load any supported file from game data or drive depending on extension and if the path is rooted. </summary>
public (BaseImage, TextureType) Load(string path)
=> Path.GetExtension(path).ToLowerInvariant() switch
{
".dds" => (LoadDds(path), TextureType.Dds),
".png" => (LoadPng(path), TextureType.Png),
".tex" => (LoadTex(path), TextureType.Tex),
_ => throw new Exception($"Extension {Path.GetExtension(path)} unknown."),
};
/// <summary> Load a .tex file from game data or drive depending on if the path is rooted. </summary>
public BaseImage LoadTex(string path)
{
using var stream = OpenTexStream(path);
return TexFileParser.Parse(stream);
}
/// <summary> Load a .dds file from drive using OtterTex. </summary>
public BaseImage LoadDds(string path)
=> ScratchImage.LoadDDS(path);
/// <summary> Load a .png file from drive using ImageSharp. </summary>
public BaseImage LoadPng(string path)
{
using var stream = File.OpenRead(path);
return Image.Load<Rgba32>(stream);
}
/// <summary> Convert an existing image to .png. Does not create a deep copy of an existing .png and just returns the existing one. </summary>
public static BaseImage ConvertToPng(BaseImage input, CancellationToken cancel, byte[]? rgba = null, int width = 0, int height = 0)
{
switch (input.Type)
{
case TextureType.Png: return input;
case TextureType.Dds:
{
(rgba, width, height) = GetData(input, rgba, width, height);
cancel.ThrowIfCancellationRequested();
return ConvertToPng(rgba, width, height);
}
default: return new BaseImage();
}
}
/// <summary> Convert an existing image to a RGBA32 .dds. Does not create a deep copy of an existing RGBA32 dds and just returns the existing one. </summary>
public static BaseImage ConvertToRgbaDds(BaseImage input, bool mipMaps, CancellationToken cancel, byte[]? rgba = null, int width = 0,
int height = 0)
{
switch (input.Type)
{
case TextureType.Png:
{
(rgba, width, height) = GetData(input, rgba, width, height);
cancel.ThrowIfCancellationRequested();
var dds = ConvertToDds(rgba, width, height).AsDds!;
cancel.ThrowIfCancellationRequested();
return AddMipMaps(dds, mipMaps);
}
case TextureType.Dds:
{
var scratch = input.AsDds!;
if (rgba == null)
return CreateUncompressed(scratch, mipMaps, cancel);
(rgba, width, height) = GetData(input, rgba, width, height);
cancel.ThrowIfCancellationRequested();
var dds = ConvertToDds(rgba, width, height).AsDds!;
cancel.ThrowIfCancellationRequested();
return AddMipMaps(dds, mipMaps);
}
default: return new BaseImage();
}
}
/// <summary> Convert an existing image to a block compressed .dds. Does not create a deep copy of an existing dds of the correct format and just returns the existing one. </summary>
public static BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, bool bc7, CancellationToken cancel, byte[]? rgba = null,
int width = 0, int height = 0)
{
switch (input.Type)
{
case TextureType.Png:
{
(rgba, width, height) = GetData(input, rgba, width, height);
cancel.ThrowIfCancellationRequested();
var dds = ConvertToDds(rgba, width, height).AsDds!;
cancel.ThrowIfCancellationRequested();
return CreateCompressed(dds, mipMaps, bc7, cancel);
}
case TextureType.Dds:
{
var scratch = input.AsDds!;
return CreateCompressed(scratch, mipMaps, bc7, cancel);
}
default: return new BaseImage();
}
}
public static BaseImage ConvertToPng(byte[] rgba, int width, int height)
=> Image.LoadPixelData<Rgba32>(rgba, width, height);
public static BaseImage ConvertToDds(byte[] rgba, int width, int height)
{
var scratch = ScratchImage.FromRGBA(rgba, width, height, out var i).ThrowIfError(i);
return scratch.Convert(DXGIFormat.B8G8R8A8UNorm);
}
public bool GameFileExists(string path)
=> _gameData.FileExists(path);
/// <summary> Add up to 13 mip maps to the input if mip maps is true, otherwise return input. </summary>
public static ScratchImage AddMipMaps(ScratchImage input, bool mipMaps)
{
var numMips = mipMaps ? Math.Min(13, 1 + BitOperations.Log2((uint)Math.Max(input.Meta.Width, input.Meta.Height))) : 1;
if (numMips == input.Meta.MipLevels)
return input;
var ec = input.GenerateMipMaps(out var ret, numMips,
(Dalamud.Utility.Util.IsLinux() ? FilterFlags.ForceNonWIC : 0) | FilterFlags.SeparateAlpha);
if (ec != ErrorCode.Ok)
throw new Exception($"Could not create the requested {numMips} mip maps, maybe retry with the top-right checkbox unchecked:\n{ec}");
return ret;
}
/// <summary> Create an uncompressed .dds (optionally with mip maps) from the input. Returns input (+ mipmaps) if it is already uncompressed. </summary>
public static ScratchImage CreateUncompressed(ScratchImage input, bool mipMaps, CancellationToken cancel)
{
if (input.Meta.Format == DXGIFormat.B8G8R8A8UNorm)
return AddMipMaps(input, mipMaps);
input = input.Meta.Format.IsCompressed()
? input.Decompress(DXGIFormat.B8G8R8A8UNorm)
: input.Convert(DXGIFormat.B8G8R8A8UNorm);
cancel.ThrowIfCancellationRequested();
return AddMipMaps(input, mipMaps);
}
/// <summary> Create a BC3 or BC7 block-compressed .dds from the input (optionally with mipmaps). Returns input (+ mipmaps) if it is already the correct format. </summary>
public static ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, bool bc7, CancellationToken cancel)
{
var format = bc7 ? DXGIFormat.BC7UNorm : DXGIFormat.BC3UNorm;
if (input.Meta.Format == format)
return input;
if (input.Meta.Format.IsCompressed())
{
input = input.Decompress(DXGIFormat.B8G8R8A8UNorm);
cancel.ThrowIfCancellationRequested();
}
input = AddMipMaps(input, mipMaps);
cancel.ThrowIfCancellationRequested();
return input.Compress(format, CompressFlags.BC7Quick | CompressFlags.Parallel);
}
/// <summary> Load a tex file either from game data if the path is not rooted, or from drive if it is rooted.</summary>
private Stream OpenTexStream(string path)
{
if (Path.IsPathRooted(path))
return File.OpenRead(path);
var file = _gameData.GetFile(path);
return file != null ? new MemoryStream(file.Data) : throw new Exception($"Unable to obtain \"{path}\" from game files.");
}
/// <summary> Obtain the checked rgba data, width and height for an image. </summary>
private static (byte[], int, int) GetData(BaseImage input, byte[]? rgba, int width, int height)
{
if (rgba == null)
return input.GetPixelData();
if (width == 0 || height == 0)
(width, height) = input.Dimensions;
return width * height * 4 != rgba.Length
? input.GetPixelData()
: (rgba, width, height);
}
/// <summary> Save a .dds file as .tex file with appropriately changed header. </summary>
public static void SaveTex(string path, ScratchImage input)
{
var header = input.ToTexHeader();
if (header.Format == TexFile.TextureFormat.Unknown)
throw new Exception($"Could not save tex file with format {input.Meta.Format}, not convertible to a valid .tex format.");
using var stream = File.Open(path, File.Exists(path) ? FileMode.Truncate : FileMode.CreateNew);
using var w = new BinaryWriter(stream);
header.Write(w);
w.Write(input.Pixels);
}
}

View file

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Threading.Tasks; using System.Threading.Tasks;
using OtterGui; using OtterGui.Tasks;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
namespace Penumbra.Mods.Editor; namespace Penumbra.Mods.Editor;

View file

@ -5,6 +5,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Notifications;
using OtterGui; using OtterGui;
using OtterGui.Tasks;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.String.Classes; using Penumbra.String.Classes;

View file

@ -22,7 +22,8 @@ using Penumbra.Collections.Manager;
using Penumbra.UI.Tabs; using Penumbra.UI.Tabs;
using ChangedItemClick = Penumbra.Communication.ChangedItemClick; using ChangedItemClick = Penumbra.Communication.ChangedItemClick;
using ChangedItemHover = Penumbra.Communication.ChangedItemHover; using ChangedItemHover = Penumbra.Communication.ChangedItemHover;
using OtterGui.Tasks;
namespace Penumbra; namespace Penumbra;
public class Penumbra : IDalamudPlugin public class Penumbra : IDalamudPlugin

View file

@ -7,6 +7,7 @@ using Penumbra.Collections.Cache;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.Import.Textures;
using Penumbra.Interop.PathResolving; using Penumbra.Interop.PathResolving;
using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.ResourceLoading;
using Penumbra.Interop.ResourceTree; using Penumbra.Interop.ResourceTree;
@ -173,7 +174,8 @@ public static class ServiceManager
.AddSingleton<ModSwapEditor>() .AddSingleton<ModSwapEditor>()
.AddSingleton<ModNormalizer>() .AddSingleton<ModNormalizer>()
.AddSingleton<ModMerger>() .AddSingleton<ModMerger>()
.AddSingleton<ModEditor>(); .AddSingleton<ModEditor>()
.AddSingleton<TextureManager>();
private static IServiceCollection AddApi(this IServiceCollection services) private static IServiceCollection AddApi(this IServiceCollection services)
=> services.AddSingleton<PenumbraApi>() => services.AddSingleton<PenumbraApi>()

View file

@ -1,6 +1,6 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using OtterGui; using OtterGui.Tasks;
using Penumbra.Util; using Penumbra.Util;
namespace Penumbra.Services; namespace Penumbra.Services;

View file

@ -5,6 +5,7 @@ using System.Numerics;
using ImGuiNET; using ImGuiNET;
using OtterGui; using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Tasks;
using OtterTex; using OtterTex;
using Penumbra.Import.Textures; using Penumbra.Import.Textures;
@ -12,13 +13,14 @@ namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow public partial class ModEditWindow
{ {
private readonly TextureManager _textures;
private readonly Texture _left = new(); private readonly Texture _left = new();
private readonly Texture _right = new(); private readonly Texture _right = new();
private readonly CombinedTexture _center; private readonly CombinedTexture _center;
private bool _overlayCollapsed = true; private bool _overlayCollapsed = true;
private bool _addMipMaps = true;
private bool _addMipMaps = true;
private int _currentSaveAs; private int _currentSaveAs;
private static readonly (string, string)[] SaveAsStrings = private static readonly (string, string)[] SaveAsStrings =
@ -42,13 +44,13 @@ public partial class ModEditWindow
ImGuiUtil.DrawTextButton(label, new Vector2(-1, 0), ImGui.GetColorU32(ImGuiCol.FrameBg)); ImGuiUtil.DrawTextButton(label, new Vector2(-1, 0), ImGui.GetColorU32(ImGuiCol.FrameBg));
ImGui.NewLine(); ImGui.NewLine();
tex.PathInputBox(_dalamud, "##input", "Import Image...", "Can import game paths as well as your own files.", _mod!.ModPath.FullName, TextureDrawer.PathInputBox(_textures, tex, ref tex.TmpPath, "##input", "Import Image...",
_fileDialog, _config.DefaultModImportPath); "Can import game paths as well as your own files.", _mod!.ModPath.FullName, _fileDialog, _config.DefaultModImportPath);
var files = _editor.Files.Tex.SelectMany(f => f.SubModUsage.Select(p => (p.Item2.ToString(), true)) var files = _editor.Files.Tex.SelectMany(f => f.SubModUsage.Select(p => (p.Item2.ToString(), true))
.Prepend((f.File.FullName, false))); .Prepend((f.File.FullName, false)));
tex.PathSelectBox(_dalamud, "##combo", TextureDrawer.PathSelectBox(_textures, tex, "##combo",
"Select the textures included in this mod on your drive or the ones they replace from the game files.", "Select the textures included in this mod on your drive or the ones they replace from the game files.", files,
files, _mod.ModPath.FullName.Length + 1); _mod.ModPath.FullName.Length + 1);
if (tex == _left) if (tex == _left)
_center.DrawMatrixInputLeft(size.X); _center.DrawMatrixInputLeft(size.X);
@ -58,7 +60,7 @@ public partial class ModEditWindow
ImGui.NewLine(); ImGui.NewLine();
using var child2 = ImRaii.Child("image"); using var child2 = ImRaii.Child("image");
if (child2) if (child2)
tex.Draw(imageSize); TextureDrawer.Draw(tex, imageSize);
} }
private void SaveAsCombo() private void SaveAsCombo()
@ -105,7 +107,7 @@ public partial class ModEditWindow
_fileDialog.OpenSavePicker("Save Texture as TEX...", ".tex", fileName, ".tex", (a, b) => _fileDialog.OpenSavePicker("Save Texture as TEX...", ".tex", fileName, ".tex", (a, b) =>
{ {
if (a) if (a)
_center.SaveAsTex(b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); _center.SaveAsTex(_textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps);
}, _mod!.ModPath.FullName, _forceTextureStartPath); }, _mod!.ModPath.FullName, _forceTextureStartPath);
_forceTextureStartPath = false; _forceTextureStartPath = false;
} }
@ -116,7 +118,7 @@ public partial class ModEditWindow
_fileDialog.OpenSavePicker("Save Texture as DDS...", ".dds", fileName, ".dds", (a, b) => _fileDialog.OpenSavePicker("Save Texture as DDS...", ".dds", fileName, ".dds", (a, b) =>
{ {
if (a) if (a)
_center.SaveAsDds(b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); _center.SaveAsDds(_textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps);
}, _mod!.ModPath.FullName, _forceTextureStartPath); }, _mod!.ModPath.FullName, _forceTextureStartPath);
_forceTextureStartPath = false; _forceTextureStartPath = false;
} }
@ -129,20 +131,20 @@ public partial class ModEditWindow
_fileDialog.OpenSavePicker("Save Texture as PNG...", ".png", fileName, ".png", (a, b) => _fileDialog.OpenSavePicker("Save Texture as PNG...", ".png", fileName, ".png", (a, b) =>
{ {
if (a) if (a)
_center.SaveAsPng(b); _center.SaveAsPng(_textures, b);
}, _mod!.ModPath.FullName, _forceTextureStartPath); }, _mod!.ModPath.FullName, _forceTextureStartPath);
_forceTextureStartPath = false; _forceTextureStartPath = false;
} }
if (_left.Type is Texture.FileType.Tex && _center.IsLeftCopy) if (_left.Type is TextureType.Tex && _center.IsLeftCopy)
{ {
var buttonSize = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X * 2) / 3, 0); var buttonSize = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X * 2) / 3, 0);
if (ImGuiUtil.DrawDisabledButton("Convert to BC7", buttonSize, if (ImGuiUtil.DrawDisabledButton("Convert to BC7", buttonSize,
"This converts the texture to BC7 format in place. This is not revertible.", "This converts the texture to BC7 format in place. This is not revertible.",
_left.Format is DXGIFormat.BC7Typeless or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB)) _left.Format is DXGIFormat.BC7Typeless or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB))
{ {
_center.SaveAsTex(_left.Path, CombinedTexture.TextureSaveType.BC7, _left.MipMaps > 1); _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC7, _left.MipMaps > 1);
_left.Reload(_dalamud); ReloadConvertedSubscribe(_left.Path, _center.SaveGuid);
} }
ImGui.SameLine(); ImGui.SameLine();
@ -150,8 +152,8 @@ public partial class ModEditWindow
"This converts the texture to BC3 format in place. This is not revertible.", "This converts the texture to BC3 format in place. This is not revertible.",
_left.Format is DXGIFormat.BC3Typeless or DXGIFormat.BC3UNorm or DXGIFormat.BC3UNormSRGB)) _left.Format is DXGIFormat.BC3Typeless or DXGIFormat.BC3UNorm or DXGIFormat.BC3UNormSRGB))
{ {
_center.SaveAsTex(_left.Path, CombinedTexture.TextureSaveType.BC3, _left.MipMaps > 1); _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC3, _left.MipMaps > 1);
_left.Reload(_dalamud); ReloadConvertedSubscribe(_left.Path, _center.SaveGuid);
} }
ImGui.SameLine(); ImGui.SameLine();
@ -159,27 +161,55 @@ public partial class ModEditWindow
"This converts the texture to RGBA format in place. This is not revertible.", "This converts the texture to RGBA format in place. This is not revertible.",
_left.Format is DXGIFormat.B8G8R8A8UNorm or DXGIFormat.B8G8R8A8Typeless or DXGIFormat.B8G8R8A8UNormSRGB)) _left.Format is DXGIFormat.B8G8R8A8UNorm or DXGIFormat.B8G8R8A8Typeless or DXGIFormat.B8G8R8A8UNormSRGB))
{ {
_center.SaveAsTex(_left.Path, CombinedTexture.TextureSaveType.Bitmap, _left.MipMaps > 1); _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.Bitmap, _left.MipMaps > 1);
_left.Reload(_dalamud); ReloadConvertedSubscribe(_left.Path, _center.SaveGuid);
} }
} }
else else
{ {
ImGui.NewLine(); ImGui.NewLine();
} }
ImGui.NewLine(); ImGui.NewLine();
} }
if (_center.SaveException != null) if (_center.SaveGuid != Guid.Empty)
{ {
ImGui.TextUnformatted("Could not save file:"); var state = _textures.GetState(_center.SaveGuid, out var saveException, out _, out _);
using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF0000FF); if (saveException != null)
ImGuiUtil.TextWrapped(_center.SaveException.ToString()); {
ImGui.TextUnformatted("Could not save file:");
using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF0000FF);
ImGuiUtil.TextWrapped(saveException.ToString());
}
else if (state == ActionState.Running)
{
ImGui.TextUnformatted("Computing...");
}
} }
using var child2 = ImRaii.Child("image"); using var child2 = ImRaii.Child("image");
if (child2) if (child2)
_center.Draw(_dalamud.UiBuilder, imageSize); _center.Draw(_textures, imageSize);
}
private void ReloadConvertedSubscribe(string path, Guid guid)
{
void Reload(Guid eventGuid, ActionState state, Exception? ex)
{
if (guid != eventGuid)
return;
if (_left.Path != path)
return;
if (state is ActionState.Succeeded)
_dalamud.Framework.RunOnFrameworkThread(() => _left.Reload(_textures));
_textures.Finished -= Reload;
}
_textures.Finished += Reload;
} }
private Vector2 GetChildWidth() private Vector2 GetChildWidth()

View file

@ -521,7 +521,7 @@ public partial class ModEditWindow : Window, IDisposable
public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData, public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData,
Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager,
StainService stainService, ActiveCollections activeCollections, UiBuilder uiBuilder, DalamudServices dalamud, ModMergeTab modMergeTab, StainService stainService, ActiveCollections activeCollections, UiBuilder uiBuilder, DalamudServices dalamud, ModMergeTab modMergeTab,
CommunicatorService communicator) CommunicatorService communicator, TextureManager textures)
: base(WindowBaseLabel) : base(WindowBaseLabel)
{ {
_performance = performance; _performance = performance;
@ -534,6 +534,7 @@ public partial class ModEditWindow : Window, IDisposable
_dalamud = dalamud; _dalamud = dalamud;
_modMergeTab = modMergeTab; _modMergeTab = modMergeTab;
_communicator = communicator; _communicator = communicator;
_textures = textures;
_fileDialog = fileDialog; _fileDialog = fileDialog;
_materialTab = new FileEditor<MtrlTab>(this, gameData, config, _fileDialog, "Materials", ".mtrl", _materialTab = new FileEditor<MtrlTab>(this, gameData, config, _fileDialog, "Materials", ".mtrl",
() => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty,