mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Initial Texture rework.
This commit is contained in:
parent
2f836426d6
commit
e24a535a93
16 changed files with 831 additions and 489 deletions
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
|||
Subproject commit e3d26f16234a4295bf3c7802d87ce43293c6ffe0
|
||||
Subproject commit 8d61845cd900fc0a3b58d475c43303b13c1165f4
|
||||
111
Penumbra/Import/Textures/BaseImage.cs
Normal file
111
Penumbra/Import/Textures/BaseImage.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
|
|
@ -18,12 +18,11 @@ public partial class CombinedTexture
|
|||
private int _offsetX = 0;
|
||||
private int _offsetY = 0;
|
||||
|
||||
|
||||
private Vector4 DataLeft( int offset )
|
||||
=> CappedVector( _left.RGBAPixels, offset, _multiplierLeft, _invertLeft );
|
||||
=> CappedVector( _left.RgbaPixels, offset, _multiplierLeft, _invertLeft );
|
||||
|
||||
private Vector4 DataRight( int offset )
|
||||
=> CappedVector( _right.RGBAPixels, offset, _multiplierRight, _invertRight );
|
||||
=> CappedVector( _right.RgbaPixels, offset, _multiplierRight, _invertRight );
|
||||
|
||||
private Vector4 DataRight( int x, int y )
|
||||
{
|
||||
|
|
@ -35,7 +34,7 @@ public partial class CombinedTexture
|
|||
}
|
||||
|
||||
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 _ )
|
||||
|
|
@ -49,10 +48,10 @@ public partial class CombinedTexture
|
|||
var rgba = alpha == 0
|
||||
? new Rgba32()
|
||||
: new Rgba32( ( ( right * right.W + left * left.W * ( 1 - right.W ) ) / alpha ) with { W = alpha } );
|
||||
_centerStorage.RGBAPixels[ offset ] = rgba.R;
|
||||
_centerStorage.RGBAPixels[ offset + 1 ] = rgba.G;
|
||||
_centerStorage.RGBAPixels[ offset + 2 ] = rgba.B;
|
||||
_centerStorage.RGBAPixels[ offset + 3 ] = rgba.A;
|
||||
_centerStorage.RgbaPixels[ offset ] = rgba.R;
|
||||
_centerStorage.RgbaPixels[ offset + 1 ] = rgba.G;
|
||||
_centerStorage.RgbaPixels[ offset + 2 ] = rgba.B;
|
||||
_centerStorage.RgbaPixels[ offset + 3 ] = rgba.A;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -63,10 +62,10 @@ public partial class CombinedTexture
|
|||
var offset = ( _left.TextureWrap!.Width * y + x ) * 4;
|
||||
var left = DataLeft( offset );
|
||||
var rgba = new Rgba32( left );
|
||||
_centerStorage.RGBAPixels[ offset ] = rgba.R;
|
||||
_centerStorage.RGBAPixels[ offset + 1 ] = rgba.G;
|
||||
_centerStorage.RGBAPixels[ offset + 2 ] = rgba.B;
|
||||
_centerStorage.RGBAPixels[ offset + 3 ] = rgba.A;
|
||||
_centerStorage.RgbaPixels[ offset ] = rgba.R;
|
||||
_centerStorage.RgbaPixels[ offset + 1 ] = rgba.G;
|
||||
_centerStorage.RgbaPixels[ offset + 2 ] = rgba.B;
|
||||
_centerStorage.RgbaPixels[ offset + 3 ] = rgba.A;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -77,10 +76,10 @@ public partial class CombinedTexture
|
|||
var offset = ( _right.TextureWrap!.Width * y + x ) * 4;
|
||||
var left = DataRight( offset );
|
||||
var rgba = new Rgba32( left );
|
||||
_centerStorage.RGBAPixels[ offset ] = rgba.R;
|
||||
_centerStorage.RGBAPixels[ offset + 1 ] = rgba.G;
|
||||
_centerStorage.RGBAPixels[ offset + 2 ] = rgba.B;
|
||||
_centerStorage.RGBAPixels[ offset + 3 ] = rgba.A;
|
||||
_centerStorage.RgbaPixels[ offset ] = rgba.R;
|
||||
_centerStorage.RgbaPixels[ offset + 1 ] = rgba.G;
|
||||
_centerStorage.RgbaPixels[ offset + 2 ] = rgba.B;
|
||||
_centerStorage.RgbaPixels[ offset + 3 ] = rgba.A;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,8 +89,8 @@ public partial class CombinedTexture
|
|||
var (width, height) = _left.IsLoaded
|
||||
? ( _left.TextureWrap!.Width, _left.TextureWrap!.Height )
|
||||
: ( _right.TextureWrap!.Width, _right.TextureWrap!.Height );
|
||||
_centerStorage.RGBAPixels = new byte[width * height * 4];
|
||||
_centerStorage.Type = Texture.FileType.Bitmap;
|
||||
_centerStorage.RgbaPixels = new byte[width * height * 4];
|
||||
_centerStorage.Type = TextureType.Bitmap;
|
||||
if( _left.IsLoaded )
|
||||
{
|
||||
Parallel.For( 0, height, _right.IsLoaded ? AddPixelsMultiplied : MultiplyPixelsLeft );
|
||||
|
|
@ -103,7 +102,6 @@ public partial class CombinedTexture
|
|||
|
||||
return ( width, height );
|
||||
}
|
||||
|
||||
private static Vector4 CappedVector( IReadOnlyList< byte > bytes, int offset, Matrix4x4 transform, bool invert )
|
||||
{
|
||||
if( bytes.Count == 0 )
|
||||
|
|
|
|||
|
|
@ -1,14 +1,5 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
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;
|
||||
|
||||
|
|
@ -38,170 +29,58 @@ public partial class CombinedTexture : IDisposable
|
|||
|
||||
private readonly Texture _centerStorage = new();
|
||||
|
||||
public Guid SaveGuid { get; private set; } = Guid.Empty;
|
||||
|
||||
public bool IsLoaded
|
||||
=> _mode != Mode.Empty;
|
||||
|
||||
public bool IsLeftCopy
|
||||
=> _mode == Mode.LeftCopy;
|
||||
|
||||
public Exception? SaveException { get; private set; } = null;
|
||||
|
||||
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();
|
||||
_centerStorage.TextureWrap =
|
||||
builder.LoadImageRaw( _centerStorage.RGBAPixels, width, height, 4 );
|
||||
_centerStorage.TextureWrap = textures.LoadTextureWrap(_centerStorage.RgbaPixels, width, height);
|
||||
}
|
||||
|
||||
_current?.Draw( size );
|
||||
if (_current != null)
|
||||
TextureDrawer.Draw(_current, size);
|
||||
}
|
||||
|
||||
|
||||
public void SaveAsPng( string path )
|
||||
{
|
||||
if( !IsLoaded || _current == null )
|
||||
public void SaveAsPng(TextureManager textures, string path)
|
||||
{
|
||||
if (!IsLoaded || _current == null)
|
||||
return;
|
||||
|
||||
SaveGuid = textures.SavePng(_current.BaseImage, path, _current.RgbaPixels, _current.TextureWrap!.Width, _current.TextureWrap!.Height);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
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 )
|
||||
{
|
||||
if( _current == null || _mode == Mode.Empty )
|
||||
private void SaveAs(TextureManager textures, string path, TextureSaveType type, bool mipMaps, bool writeTex)
|
||||
{
|
||||
if (!IsLoaded || _current == null)
|
||||
return;
|
||||
|
||||
SaveGuid = textures.SaveAs(type, mipMaps, writeTex, _current.BaseImage, path, _current.RgbaPixels, _current.TextureWrap!.Width,
|
||||
_current.TextureWrap!.Height);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if( _current.BaseImage is not ScratchImage s )
|
||||
{
|
||||
s = ScratchImage.FromRGBA( _current.RGBAPixels, _current.TextureWrap!.Width,
|
||||
_current.TextureWrap!.Height, out var i ).ThrowIfError( i );
|
||||
}
|
||||
public void SaveAsTex(TextureManager textures, string path, TextureSaveType type, bool mipMaps)
|
||||
=> SaveAs(textures, path, type, mipMaps, true);
|
||||
|
||||
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 )
|
||||
{
|
||||
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);
|
||||
using var w = new BinaryWriter( stream );
|
||||
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 void SaveAsDds(TextureManager textures, string path, TextureSaveType type, bool mipMaps)
|
||||
=> SaveAs(textures, path, type, mipMaps, false);
|
||||
|
||||
|
||||
public CombinedTexture( Texture left, Texture right )
|
||||
public CombinedTexture(Texture left, Texture right)
|
||||
{
|
||||
_left = left;
|
||||
_right = right;
|
||||
_left.Loaded += OnLoaded;
|
||||
_right.Loaded += OnLoaded;
|
||||
OnLoaded( false );
|
||||
OnLoaded(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
|
@ -211,20 +90,20 @@ public partial class CombinedTexture : IDisposable
|
|||
_right.Loaded -= OnLoaded;
|
||||
}
|
||||
|
||||
private void OnLoaded( bool _ )
|
||||
private void OnLoaded(bool _)
|
||||
=> Update();
|
||||
|
||||
public void Update()
|
||||
{
|
||||
Clean();
|
||||
if( _left.IsLoaded )
|
||||
if (_left.IsLoaded)
|
||||
{
|
||||
if( _right.IsLoaded )
|
||||
if (_right.IsLoaded)
|
||||
{
|
||||
_current = _centerStorage;
|
||||
_mode = Mode.Custom;
|
||||
}
|
||||
else if( !_invertLeft && _multiplierLeft.IsIdentity )
|
||||
else if (!_invertLeft && _multiplierLeft.IsIdentity)
|
||||
{
|
||||
_mode = Mode.LeftCopy;
|
||||
_current = _left;
|
||||
|
|
@ -235,9 +114,9 @@ public partial class CombinedTexture : IDisposable
|
|||
_mode = Mode.Custom;
|
||||
}
|
||||
}
|
||||
else if( _right.IsLoaded )
|
||||
else if (_right.IsLoaded)
|
||||
{
|
||||
if( !_invertRight && _multiplierRight.IsIdentity )
|
||||
if (!_invertRight && _multiplierRight.IsIdentity)
|
||||
{
|
||||
_current = _right;
|
||||
_mode = Mode.RightCopy;
|
||||
|
|
@ -254,6 +133,7 @@ public partial class CombinedTexture : IDisposable
|
|||
{
|
||||
_centerStorage.Dispose();
|
||||
_current = null;
|
||||
SaveGuid = Guid.Empty;
|
||||
_mode = Mode.Empty;
|
||||
}
|
||||
}
|
||||
|
|
@ -101,7 +101,7 @@ public static class TexFileParser
|
|||
Height = (ushort)meta.Height,
|
||||
Width = (ushort)meta.Width,
|
||||
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(),
|
||||
Type = meta.Dimension switch
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,135 +1,61 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using ImGuiScene;
|
||||
using Lumina.Data.Files;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
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;
|
||||
|
||||
public sealed class Texture : IDisposable
|
||||
public enum TextureType
|
||||
{
|
||||
public enum FileType
|
||||
{
|
||||
Unknown,
|
||||
Dds,
|
||||
Tex,
|
||||
Png,
|
||||
Bitmap,
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class Texture : IDisposable
|
||||
{
|
||||
// Path to the file we tried to load.
|
||||
public string Path = string.Empty;
|
||||
|
||||
// Path for changing paths.
|
||||
internal string? TmpPath;
|
||||
|
||||
// If the load failed, an exception is stored.
|
||||
public Exception? LoadError = null;
|
||||
|
||||
// The pixels of the main image in RGBA order.
|
||||
// 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.
|
||||
// null if LoadError != null or Path is empty.
|
||||
public TextureWrap? TextureWrap = null;
|
||||
|
||||
// The base image in whatever format it has.
|
||||
public object? BaseImage = null;
|
||||
public BaseImage BaseImage;
|
||||
|
||||
// Original File Type.
|
||||
public FileType Type = FileType.Unknown;
|
||||
public TextureType Type = TextureType.Unknown;
|
||||
|
||||
// Whether the file is successfully loaded and drawable.
|
||||
public bool IsLoaded
|
||||
=> TextureWrap != null;
|
||||
|
||||
public DXGIFormat Format
|
||||
=> BaseImage switch
|
||||
{
|
||||
ScratchImage s => s.Meta.Format,
|
||||
TexFile t => t.Header.Format.ToDXGI(),
|
||||
_ => DXGIFormat.Unknown,
|
||||
};
|
||||
=> BaseImage.Format;
|
||||
|
||||
public int MipMaps
|
||||
=> BaseImage switch
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
=> BaseImage.MipMaps;
|
||||
|
||||
private void Clean()
|
||||
{
|
||||
RGBAPixels = Array.Empty<byte>();
|
||||
RgbaPixels = Array.Empty<byte>();
|
||||
TextureWrap?.Dispose();
|
||||
TextureWrap = null;
|
||||
(BaseImage as IDisposable)?.Dispose();
|
||||
BaseImage = null;
|
||||
Type = FileType.Unknown;
|
||||
BaseImage.Dispose();
|
||||
BaseImage = new BaseImage();
|
||||
Type = TextureType.Unknown;
|
||||
Loaded?.Invoke(false);
|
||||
}
|
||||
|
||||
|
|
@ -138,9 +64,9 @@ public sealed class Texture : IDisposable
|
|||
|
||||
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)
|
||||
return;
|
||||
|
||||
|
|
@ -151,13 +77,9 @@ public sealed class Texture : IDisposable
|
|||
|
||||
try
|
||||
{
|
||||
var _ = System.IO.Path.GetExtension(Path).ToLowerInvariant() switch
|
||||
{
|
||||
".dds" => LoadDds(dalamud),
|
||||
".png" => LoadPng(dalamud),
|
||||
".tex" => LoadTex(dalamud),
|
||||
_ => throw new Exception($"Extension {System.IO.Path.GetExtension(Path)} unknown."),
|
||||
};
|
||||
(BaseImage, Type) = textures.Load(path);
|
||||
(RgbaPixels, var width, var height) = BaseImage.GetPixelData();
|
||||
TextureWrap = textures.LoadTextureWrap(BaseImage, RgbaPixels);
|
||||
Loaded?.Invoke(true);
|
||||
}
|
||||
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;
|
||||
Path = string.Empty;
|
||||
Load(dalamud, 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);
|
||||
Load(textures, path);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
139
Penumbra/Import/Textures/TextureDrawer.cs
Normal file
139
Penumbra/Import/Textures/TextureDrawer.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
438
Penumbra/Import/Textures/TextureManager.cs
Normal file
438
Penumbra/Import/Textures/TextureManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Threading.Tasks;
|
||||
using OtterGui;
|
||||
using OtterGui.Tasks;
|
||||
using Penumbra.Mods.Manager;
|
||||
|
||||
namespace Penumbra.Mods.Editor;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System.Linq;
|
|||
using System.Threading.Tasks;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using OtterGui;
|
||||
using OtterGui.Tasks;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ using Penumbra.Collections.Manager;
|
|||
using Penumbra.UI.Tabs;
|
||||
using ChangedItemClick = Penumbra.Communication.ChangedItemClick;
|
||||
using ChangedItemHover = Penumbra.Communication.ChangedItemHover;
|
||||
using OtterGui.Tasks;
|
||||
|
||||
namespace Penumbra;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using Penumbra.Collections.Cache;
|
|||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.Import.Textures;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Interop.ResourceLoading;
|
||||
using Penumbra.Interop.ResourceTree;
|
||||
|
|
@ -173,7 +174,8 @@ public static class ServiceManager
|
|||
.AddSingleton<ModSwapEditor>()
|
||||
.AddSingleton<ModNormalizer>()
|
||||
.AddSingleton<ModMerger>()
|
||||
.AddSingleton<ModEditor>();
|
||||
.AddSingleton<ModEditor>()
|
||||
.AddSingleton<TextureManager>();
|
||||
|
||||
private static IServiceCollection AddApi(this IServiceCollection services)
|
||||
=> services.AddSingleton<PenumbraApi>()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using OtterGui;
|
||||
using OtterGui.Tasks;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Services;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System.Numerics;
|
|||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Tasks;
|
||||
using OtterTex;
|
||||
using Penumbra.Import.Textures;
|
||||
|
||||
|
|
@ -12,12 +13,13 @@ namespace Penumbra.UI.AdvancedWindow;
|
|||
|
||||
public partial class ModEditWindow
|
||||
{
|
||||
private readonly TextureManager _textures;
|
||||
|
||||
private readonly Texture _left = new();
|
||||
private readonly Texture _right = new();
|
||||
private readonly CombinedTexture _center;
|
||||
|
||||
private bool _overlayCollapsed = true;
|
||||
|
||||
private bool _addMipMaps = true;
|
||||
private int _currentSaveAs;
|
||||
|
||||
|
|
@ -42,13 +44,13 @@ public partial class ModEditWindow
|
|||
ImGuiUtil.DrawTextButton(label, new Vector2(-1, 0), ImGui.GetColorU32(ImGuiCol.FrameBg));
|
||||
ImGui.NewLine();
|
||||
|
||||
tex.PathInputBox(_dalamud, "##input", "Import Image...", "Can import game paths as well as your own files.", _mod!.ModPath.FullName,
|
||||
_fileDialog, _config.DefaultModImportPath);
|
||||
TextureDrawer.PathInputBox(_textures, tex, ref tex.TmpPath, "##input", "Import Image...",
|
||||
"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))
|
||||
.Prepend((f.File.FullName, false)));
|
||||
tex.PathSelectBox(_dalamud, "##combo",
|
||||
"Select the textures included in this mod on your drive or the ones they replace from the game files.",
|
||||
files, _mod.ModPath.FullName.Length + 1);
|
||||
TextureDrawer.PathSelectBox(_textures, tex, "##combo",
|
||||
"Select the textures included in this mod on your drive or the ones they replace from the game files.", files,
|
||||
_mod.ModPath.FullName.Length + 1);
|
||||
|
||||
if (tex == _left)
|
||||
_center.DrawMatrixInputLeft(size.X);
|
||||
|
|
@ -58,7 +60,7 @@ public partial class ModEditWindow
|
|||
ImGui.NewLine();
|
||||
using var child2 = ImRaii.Child("image");
|
||||
if (child2)
|
||||
tex.Draw(imageSize);
|
||||
TextureDrawer.Draw(tex, imageSize);
|
||||
}
|
||||
|
||||
private void SaveAsCombo()
|
||||
|
|
@ -105,7 +107,7 @@ public partial class ModEditWindow
|
|||
_fileDialog.OpenSavePicker("Save Texture as TEX...", ".tex", fileName, ".tex", (a, b) =>
|
||||
{
|
||||
if (a)
|
||||
_center.SaveAsTex(b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps);
|
||||
_center.SaveAsTex(_textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps);
|
||||
}, _mod!.ModPath.FullName, _forceTextureStartPath);
|
||||
_forceTextureStartPath = false;
|
||||
}
|
||||
|
|
@ -116,7 +118,7 @@ public partial class ModEditWindow
|
|||
_fileDialog.OpenSavePicker("Save Texture as DDS...", ".dds", fileName, ".dds", (a, b) =>
|
||||
{
|
||||
if (a)
|
||||
_center.SaveAsDds(b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps);
|
||||
_center.SaveAsDds(_textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps);
|
||||
}, _mod!.ModPath.FullName, _forceTextureStartPath);
|
||||
_forceTextureStartPath = false;
|
||||
}
|
||||
|
|
@ -129,20 +131,20 @@ public partial class ModEditWindow
|
|||
_fileDialog.OpenSavePicker("Save Texture as PNG...", ".png", fileName, ".png", (a, b) =>
|
||||
{
|
||||
if (a)
|
||||
_center.SaveAsPng(b);
|
||||
_center.SaveAsPng(_textures, b);
|
||||
}, _mod!.ModPath.FullName, _forceTextureStartPath);
|
||||
_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);
|
||||
if (ImGuiUtil.DrawDisabledButton("Convert to BC7", buttonSize,
|
||||
"This converts the texture to BC7 format in place. This is not revertible.",
|
||||
_left.Format is DXGIFormat.BC7Typeless or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB))
|
||||
{
|
||||
_center.SaveAsTex(_left.Path, CombinedTexture.TextureSaveType.BC7, _left.MipMaps > 1);
|
||||
_left.Reload(_dalamud);
|
||||
_center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC7, _left.MipMaps > 1);
|
||||
ReloadConvertedSubscribe(_left.Path, _center.SaveGuid);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
|
@ -150,8 +152,8 @@ public partial class ModEditWindow
|
|||
"This converts the texture to BC3 format in place. This is not revertible.",
|
||||
_left.Format is DXGIFormat.BC3Typeless or DXGIFormat.BC3UNorm or DXGIFormat.BC3UNormSRGB))
|
||||
{
|
||||
_center.SaveAsTex(_left.Path, CombinedTexture.TextureSaveType.BC3, _left.MipMaps > 1);
|
||||
_left.Reload(_dalamud);
|
||||
_center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC3, _left.MipMaps > 1);
|
||||
ReloadConvertedSubscribe(_left.Path, _center.SaveGuid);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
|
@ -159,27 +161,55 @@ public partial class ModEditWindow
|
|||
"This converts the texture to RGBA format in place. This is not revertible.",
|
||||
_left.Format is DXGIFormat.B8G8R8A8UNorm or DXGIFormat.B8G8R8A8Typeless or DXGIFormat.B8G8R8A8UNormSRGB))
|
||||
{
|
||||
_center.SaveAsTex(_left.Path, CombinedTexture.TextureSaveType.Bitmap, _left.MipMaps > 1);
|
||||
_left.Reload(_dalamud);
|
||||
_center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.Bitmap, _left.MipMaps > 1);
|
||||
ReloadConvertedSubscribe(_left.Path, _center.SaveGuid);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.NewLine();
|
||||
}
|
||||
|
||||
ImGui.NewLine();
|
||||
}
|
||||
|
||||
if (_center.SaveException != null)
|
||||
if (_center.SaveGuid != Guid.Empty)
|
||||
{
|
||||
var state = _textures.GetState(_center.SaveGuid, out var saveException, out _, out _);
|
||||
if (saveException != null)
|
||||
{
|
||||
ImGui.TextUnformatted("Could not save file:");
|
||||
using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF0000FF);
|
||||
ImGuiUtil.TextWrapped(_center.SaveException.ToString());
|
||||
ImGuiUtil.TextWrapped(saveException.ToString());
|
||||
}
|
||||
else if (state == ActionState.Running)
|
||||
{
|
||||
ImGui.TextUnformatted("Computing...");
|
||||
}
|
||||
}
|
||||
|
||||
using var child2 = ImRaii.Child("image");
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -521,7 +521,7 @@ public partial class ModEditWindow : Window, IDisposable
|
|||
public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData,
|
||||
Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager,
|
||||
StainService stainService, ActiveCollections activeCollections, UiBuilder uiBuilder, DalamudServices dalamud, ModMergeTab modMergeTab,
|
||||
CommunicatorService communicator)
|
||||
CommunicatorService communicator, TextureManager textures)
|
||||
: base(WindowBaseLabel)
|
||||
{
|
||||
_performance = performance;
|
||||
|
|
@ -534,6 +534,7 @@ public partial class ModEditWindow : Window, IDisposable
|
|||
_dalamud = dalamud;
|
||||
_modMergeTab = modMergeTab;
|
||||
_communicator = communicator;
|
||||
_textures = textures;
|
||||
_fileDialog = fileDialog;
|
||||
_materialTab = new FileEditor<MtrlTab>(this, gameData, config, _fileDialog, "Materials", ".mtrl",
|
||||
() => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue