From 6e82242a72cae73b9967b07c0abeed25be2c71a9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 3 Sep 2022 16:10:04 +0200 Subject: [PATCH] Current Textures --- Penumbra/Import/TexToolsImport.cs | 4 +- Penumbra/Import/TexToolsImporter.Archives.cs | 67 +- .../Textures/CombinedTexture.Manipulation.cs | 228 +++++ Penumbra/Import/Textures/CombinedTexture.cs | 184 ++++ Penumbra/Import/Textures/Texture.cs | 183 ++++ .../{Dds => Textures}/TextureImporter.cs | 0 Penumbra/Mods/Mod.Creation.cs | 8 +- Penumbra/UI/Classes/ModEditWindow.FileEdit.cs | 24 +- Penumbra/UI/Classes/ModEditWindow.Textures.cs | 811 ++---------------- Penumbra/UI/Classes/ModEditWindow.cs | 11 + 10 files changed, 758 insertions(+), 762 deletions(-) create mode 100644 Penumbra/Import/Textures/CombinedTexture.Manipulation.cs create mode 100644 Penumbra/Import/Textures/CombinedTexture.cs create mode 100644 Penumbra/Import/Textures/Texture.cs rename Penumbra/Import/{Dds => Textures}/TextureImporter.cs (100%) diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index 3dcf5d47..fd659278 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -81,8 +81,8 @@ public partial class TexToolsImporter : IDisposable private void ImportFiles() { - State = ImporterState.None; - _currentModPackIdx = 0; + State = ImporterState.None; + _currentModPackIdx = 0; foreach( var file in _modPackFiles ) { _currentModDirectory = null; diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index 12f7428f..5c0371dd 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -29,6 +29,7 @@ public partial class TexToolsImporter using var archive = ArchiveFactory.Open( zfs ); var baseName = FindArchiveModMeta( archive, out var leadDir ); + var name = string.Empty; _currentOptionIdx = 0; _currentNumOptions = 1; _currentModName = modPackFile.Name; @@ -44,7 +45,7 @@ public partial class TexToolsImporter }; PluginLog.Log( $" -> Importing {archive.Type} Archive." ); - _currentModDirectory = Mod.CreateModFolder( _baseDirectory, baseName ); + _currentModDirectory = Mod.CreateModFolder( _baseDirectory, Path.GetRandomFileName() ); var options = new ExtractionOptions() { ExtractFullPath = true, @@ -55,32 +56,61 @@ public partial class TexToolsImporter _currentFileIdx = 0; var reader = archive.ExtractAllEntries(); - while(reader.MoveToNextEntry()) + while( reader.MoveToNextEntry() ) { _token.ThrowIfCancellationRequested(); if( reader.Entry.IsDirectory ) { - ++_currentFileIdx; + --_currentNumFiles; continue; } PluginLog.Log( " -> Extracting {0}", reader.Entry.Key ); - reader.WriteEntryToDirectory( _currentModDirectory.FullName, options ); + // Check that the mod has a valid name in the meta.json file. + if( Path.GetFileName( reader.Entry.Key ) == "meta.json" ) + { + using var s = new MemoryStream(); + using var e = reader.OpenEntryStream(); + e.CopyTo( s ); + s.Seek( 0, SeekOrigin.Begin ); + using var t = new StreamReader( s ); + using var j = new JsonTextReader( t ); + var obj = JObject.Load( j ); + name = obj[ nameof( Mod.Name ) ]?.Value< string >()?.RemoveInvalidPathSymbols() ?? string.Empty; + if( name.Length == 0 ) + { + throw new Exception( "Invalid mod archive: mod meta has no name." ); + } + + using var f = File.OpenWrite( Path.Combine( _currentModDirectory.FullName, reader.Entry.Key ) ); + s.Seek( 0, SeekOrigin.Begin ); + s.WriteTo( f ); + } + else + { + reader.WriteEntryToDirectory( _currentModDirectory.FullName, options ); + } ++_currentFileIdx; } + _token.ThrowIfCancellationRequested(); + var oldName = _currentModDirectory.FullName; + // Use either the top-level directory as the mods base name, or the (fixed for path) name in the json. if( leadDir ) { - _token.ThrowIfCancellationRequested(); - var oldName = _currentModDirectory.FullName; - var tmpName = oldName + "__tmp"; - Directory.Move( oldName, tmpName ); - Directory.Move( Path.Combine( tmpName, baseName ), oldName ); - Directory.Delete( tmpName ); - _currentModDirectory = new DirectoryInfo( oldName ); + _currentModDirectory = Mod.CreateModFolder( _baseDirectory, baseName, false ); + Directory.Move( Path.Combine( oldName, baseName ), _currentModDirectory.FullName ); + Directory.Delete( oldName ); } + else + { + _currentModDirectory = Mod.CreateModFolder( _baseDirectory, name, false ); + Directory.Move( oldName, _currentModDirectory.FullName ); + } + + _currentModDirectory.Refresh(); return _currentModDirectory; } @@ -88,7 +118,7 @@ public partial class TexToolsImporter // Search the archive for the meta.json file which needs to exist. private static string FindArchiveModMeta( IArchive archive, out bool leadDir ) { - var entry = archive.Entries.FirstOrDefault( e => !e.IsDirectory && e.Key.EndsWith( "meta.json" ) ); + var entry = archive.Entries.FirstOrDefault( e => !e.IsDirectory && Path.GetFileName( e.Key ) == "meta.json" ); // None found. if( entry == null ) { @@ -119,18 +149,7 @@ public partial class TexToolsImporter } } - // Check that the mod has a valid name in the meta.json file. - using var e = entry.OpenEntryStream(); - using var t = new StreamReader( e ); - using var j = new JsonTextReader( t ); - var obj = JObject.Load( j ); - var name = obj[ nameof( Mod.Name ) ]?.Value< string >()?.RemoveInvalidPathSymbols() ?? string.Empty; - if( name.Length == 0 ) - { - throw new Exception( "Invalid mod archive: mod meta has no name." ); - } - // Use either the top-level directory as the mods base name, or the (fixed for path) name in the json. - return ret.Length == 0 ? name : ret; + return ret; } } \ No newline at end of file diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs new file mode 100644 index 00000000..d0c3e5b2 --- /dev/null +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -0,0 +1,228 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Threading.Tasks; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui; +using SixLabors.ImageSharp.PixelFormats; + +namespace Penumbra.Import.Textures; + +public partial class CombinedTexture +{ + private Matrix4x4 _multiplierLeft = Matrix4x4.Identity; + private Matrix4x4 _multiplierRight = Matrix4x4.Identity; + private bool _invertLeft = false; + private bool _invertRight = false; + private int _offsetX = 0; + private int _offsetY = 0; + + + private Vector4 DataLeft( int offset ) + => CappedVector( _left.RGBAPixels, offset, _multiplierLeft, _invertLeft ); + + private Vector4 DataRight( int offset ) + => CappedVector( _right.RGBAPixels, offset, _multiplierRight, _invertRight ); + + private Vector4 DataRight( int x, int y ) + { + x += _offsetX; + y += _offsetY; + if( x < 0 || x >= _right.TextureWrap!.Width || y < 0 || y >= _right.TextureWrap!.Height ) + { + return Vector4.Zero; + } + + var offset = ( y * _right.TextureWrap!.Width + x ) * 4; + return CappedVector( _right.RGBAPixels, offset, _multiplierRight, _invertRight ); + } + + private void AddPixelsMultiplied( int y, ParallelLoopState _ ) + { + for( var x = 0; x < _left.TextureWrap!.Width; ++x ) + { + var offset = ( _left.TextureWrap!.Width * y + x ) * 4; + var left = DataLeft( offset ); + var right = DataRight( x, y ); + var alpha = right.W + left.W * ( 1 - right.W ); + if( alpha == 0 ) + { + return; + } + + var sum = ( right * right.W + left * left.W * ( 1 - right.W ) ) / alpha; + var rgba = new Rgba32( sum 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; + } + } + + private void MultiplyPixelsLeft( int y, ParallelLoopState _ ) + { + for( var x = 0; x < _left.TextureWrap!.Width; ++x ) + { + 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; + } + } + + private void MultiplyPixelsRight( int y, ParallelLoopState _ ) + { + for( var x = 0; x < _right.TextureWrap!.Width; ++x ) + { + 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; + } + } + + + private (int Width, int Height) CombineImage() + { + var (width, height) = _left.IsLoaded + ? ( _left.TextureWrap!.Width, _left.TextureWrap!.Height ) + : ( _right.TextureWrap!.Width, _right.TextureWrap!.Height ); + _centerStorage.RGBAPixels = new byte[width * height * 4]; + + if( _left.IsLoaded ) + { + Parallel.For( 0, height, _right.IsLoaded ? AddPixelsMultiplied : MultiplyPixelsLeft ); + } + else + { + Parallel.For( 0, height, MultiplyPixelsRight ); + } + + return ( width, height ); + } + + private static Vector4 CappedVector( IReadOnlyList< byte > bytes, int offset, Matrix4x4 transform, bool invert ) + { + if( bytes.Count == 0 ) + { + return Vector4.Zero; + } + + var rgba = new Rgba32( bytes[ offset ], bytes[ offset + 1 ], bytes[ offset + 2 ], bytes[ offset + 3 ] ); + var transformed = Vector4.Transform( rgba.ToVector4(), transform ); + if( invert ) + { + transformed = new Vector4( 1 - transformed.X, 1 - transformed.Y, 1 - transformed.Z, transformed.W ); + } + + transformed.X = Math.Clamp( transformed.X, 0, 1 ); + transformed.Y = Math.Clamp( transformed.Y, 0, 1 ); + transformed.Z = Math.Clamp( transformed.Z, 0, 1 ); + transformed.W = Math.Clamp( transformed.W, 0, 1 ); + return transformed; + } + + private static bool DragFloat( string label, float width, ref float value ) + { + var tmp = value; + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( width ); + if( ImGui.DragFloat( label, ref tmp, 0.001f, -1f, 1f ) ) + { + value = tmp; + } + + return ImGui.IsItemDeactivatedAfterEdit(); + } + + public void DrawMatrixInputLeft( float width ) + { + var ret = DrawMatrixInput( ref _multiplierLeft, width ); + ret |= ImGui.Checkbox( "Invert Colors##Left", ref _invertLeft ); + if( ret ) + { + Update(); + } + } + + public void DrawMatrixInputRight( float width ) + { + var ret = DrawMatrixInput( ref _multiplierRight, width ); + ret |= ImGui.Checkbox( "Invert Colors##Right", ref _invertRight ); + ImGui.SameLine(); + ImGui.SetNextItemWidth( 75 ); + ImGui.DragInt( "##XOffset", ref _offsetX, 0.5f ); + ret |= ImGui.IsItemDeactivatedAfterEdit(); + ImGui.SameLine(); + ImGui.SetNextItemWidth( 75 ); + ImGui.DragInt( "Offsets##YOffset", ref _offsetY, 0.5f ); + ret |= ImGui.IsItemDeactivatedAfterEdit(); + if( ret ) + { + Update(); + } + } + + private static bool DrawMatrixInput( ref Matrix4x4 multiplier, float width ) + { + using var table = ImRaii.Table( string.Empty, 5, ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingFixedFit ); + if( !table ) + { + return false; + } + + var changes = false; + + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGuiUtil.Center( "R" ); + ImGui.TableNextColumn(); + ImGuiUtil.Center( "G" ); + ImGui.TableNextColumn(); + ImGuiUtil.Center( "B" ); + ImGui.TableNextColumn(); + ImGuiUtil.Center( "A" ); + + var inputWidth = width / 6; + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Text( "R " ); + changes |= DragFloat( "##RR", inputWidth, ref multiplier.M11 ); + changes |= DragFloat( "##RG", inputWidth, ref multiplier.M12 ); + changes |= DragFloat( "##RB", inputWidth, ref multiplier.M13 ); + changes |= DragFloat( "##RA", inputWidth, ref multiplier.M14 ); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Text( "G " ); + changes |= DragFloat( "##GR", inputWidth, ref multiplier.M21 ); + changes |= DragFloat( "##GG", inputWidth, ref multiplier.M22 ); + changes |= DragFloat( "##GB", inputWidth, ref multiplier.M23 ); + changes |= DragFloat( "##GA", inputWidth, ref multiplier.M24 ); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Text( "B " ); + changes |= DragFloat( "##BR", inputWidth, ref multiplier.M31 ); + changes |= DragFloat( "##BG", inputWidth, ref multiplier.M32 ); + changes |= DragFloat( "##BB", inputWidth, ref multiplier.M33 ); + changes |= DragFloat( "##BA", inputWidth, ref multiplier.M34 ); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Text( "A " ); + changes |= DragFloat( "##AR", inputWidth, ref multiplier.M41 ); + changes |= DragFloat( "##AG", inputWidth, ref multiplier.M42 ); + changes |= DragFloat( "##AB", inputWidth, ref multiplier.M43 ); + changes |= DragFloat( "##AA", inputWidth, ref multiplier.M44 ); + + return changes; + } +} \ No newline at end of file diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs new file mode 100644 index 00000000..8688ce2f --- /dev/null +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -0,0 +1,184 @@ +using System; +using System.Numerics; +using OtterTex; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.PixelFormats; +using Image = SixLabors.ImageSharp.Image; + +namespace Penumbra.Import.Textures; + +public partial class CombinedTexture : IDisposable +{ + private enum Mode + { + Empty, + LeftCopy, + RightCopy, + Custom, + } + + private readonly Texture _left; + private readonly Texture _right; + + private Texture? _current; + private Mode _mode = Mode.Empty; + + private readonly Texture _centerStorage = new(); + + public bool IsLoaded + => _mode != Mode.Empty; + + public void Draw( Vector2 size ) + { + if( _mode == Mode.Custom && !_centerStorage.IsLoaded ) + { + var (width, height) = CombineImage(); + _centerStorage.TextureWrap = + Dalamud.PluginInterface.UiBuilder.LoadImageRaw( _centerStorage.RGBAPixels, width, height, 4 ); + } + + _current?.Draw( size ); + } + + + public void SaveAsPng( string path ) + { + if( !IsLoaded || _current == null ) + { + return; + } + + var image = Image.LoadPixelData< Rgba32 >( _current.RGBAPixels, _current.TextureWrap!.Width, + _current.TextureWrap!.Height ); + image.Save( path, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression } ); + } + + public void SaveAsDDS( string path, DXGIFormat format, bool fast, float threshold = 0.5f ) + { + if( _current == null ) + return; + switch( _mode ) + { + case Mode.Empty: return; + case Mode.LeftCopy: + case Mode.RightCopy: + if( _centerStorage.BaseImage is ScratchImage s ) + { + if( format != s.Meta.Format ) + { + s = s.Convert( format, threshold ); + } + + s.SaveDDS( path ); + } + else + { + var image = ScratchImage.FromRGBA( _current.RGBAPixels, _current.TextureWrap!.Width, + _current.TextureWrap!.Height, out var i ).ThrowIfError( i ); + image.SaveDDS( path ).ThrowIfError(); + } + + break; + } + } + + //private void SaveAs( bool success, string path, int type ) + //{ + // if( !success || _imageCenter == null || _wrapCenter == null ) + // { + // return; + // } + // + // try + // { + // switch( type ) + // { + // case 0: + // var img = Image.LoadPixelData< Rgba32 >( _imageCenter, _wrapCenter.Width, _wrapCenter.Height ); + // img.Save( path, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression } ); + // break; + // case 1: + // if( TextureImporter.RgbaBytesToTex( _imageCenter, _wrapCenter.Width, _wrapCenter.Height, out var tex ) ) + // { + // File.WriteAllBytes( path, tex ); + // } + // + // break; + // case 2: + // //ScratchImage.LoadDDS( _imageCenter, ) + // //if( TextureImporter.RgbaBytesToDds( _imageCenter, _wrapCenter.Width, _wrapCenter.Height, out var dds ) ) + ////{ + // // File.WriteAllBytes( path, dds ); + ////} + // + // break; + // } + // } + // catch( Exception e ) + // { + // PluginLog.Error( $"Could not save image to {path}:\n{e}" ); + // } + + public CombinedTexture( Texture left, Texture right ) + { + _left = left; + _right = right; + _left.Loaded += OnLoaded; + _right.Loaded += OnLoaded; + OnLoaded( false ); + } + + public void Dispose() + { + Clean(); + _left.Loaded -= OnLoaded; + _right.Loaded -= OnLoaded; + } + + private void OnLoaded( bool _ ) + => Update(); + + public void Update() + { + Clean(); + if( _left.IsLoaded ) + { + if( _right.IsLoaded ) + { + _current = _centerStorage; + _mode = Mode.Custom; + } + else if( !_invertLeft && _multiplierLeft.IsIdentity ) + { + _mode = Mode.LeftCopy; + _current = _left; + } + else + { + _current = _centerStorage; + _mode = Mode.Custom; + } + } + else if( _right.IsLoaded ) + { + if( !_invertRight && _multiplierRight.IsIdentity ) + { + _current = _right; + _mode = Mode.RightCopy; + } + else + { + _current = _centerStorage; + _mode = Mode.Custom; + } + } + } + + private void Clean() + { + _centerStorage.Dispose(); + _current = null; + _mode = Mode.Empty; + } +} \ No newline at end of file diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs new file mode 100644 index 00000000..cc3e656b --- /dev/null +++ b/Penumbra/Import/Textures/Texture.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Utility; +using ImGuiNET; +using ImGuiScene; +using Lumina.Data.Files; +using OtterGui; +using OtterGui.Raii; +using OtterTex; +using Penumbra.GameData.ByteString; +using Penumbra.UI.Classes; +using SixLabors.ImageSharp.PixelFormats; +using Image = SixLabors.ImageSharp.Image; + +namespace Penumbra.Import.Textures; + +public class Texture : IDisposable +{ + // Path to the file we tried to load. + public string Path = string.Empty; + + // 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 >(); + + // 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; + + // Whether the file is successfully loaded and drawable. + public bool IsLoaded + => TextureWrap != null; + + public Texture() + { } + + public void Draw( Vector2 size ) + { + if( TextureWrap != null ) + { + ImGui.TextUnformatted( $"Image Dimensions: {TextureWrap.Width} x {TextureWrap.Height}" ); + 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 ); + } + else if( LoadError != null ) + { + ImGui.TextUnformatted( "Could not load file:" ); + ImGuiUtil.TextColored( Colors.RegexWarningBorder, LoadError.ToString() ); + } + else + { + ImGui.Dummy( size ); + } + } + + private void Clean() + { + RGBAPixels = Array.Empty< byte >(); + TextureWrap?.Dispose(); + TextureWrap = null; + ( BaseImage as IDisposable )?.Dispose(); + BaseImage = null; + Loaded?.Invoke( false ); + } + + public void Dispose() + => Clean(); + + public event Action< bool >? Loaded; + + private void Load( string path ) + { + _tmpPath = null; + if( path == Path ) + { + return; + } + + Path = path; + Clean(); + try + { + var _ = System.IO.Path.GetExtension( Path ) switch + { + ".dds" => LoadDds(), + ".png" => LoadPng(), + ".tex" => LoadTex(), + _ => true, + }; + Loaded?.Invoke( true ); + } + catch( Exception e ) + { + LoadError = e; + Clean(); + } + } + + private bool LoadDds() + { + 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( f.Meta.Width, f.Meta.Height ); + return true; + } + + private bool LoadPng() + { + 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( png.Width, png.Height ); + return true; + } + + private bool LoadTex() + { + var tex = System.IO.Path.IsPathRooted( Path ) + ? Dalamud.GameData.GameData.GetFileFromDisk< TexFile >( Path ) + : Dalamud.GameData.GetFile< TexFile >( Path ); + BaseImage = tex ?? throw new Exception( "Could not read .tex file." ); + RGBAPixels = tex.GetRgbaImageData(); + CreateTextureWrap( tex.Header.Width, tex.Header.Height ); + return true; + } + + private void CreateTextureWrap( int width, int height ) + => TextureWrap = Dalamud.PluginInterface.UiBuilder.LoadImageRaw( RGBAPixels, width, height, 4 ); + + private string? _tmpPath; + + public void PathInputBox( string label, string hint, string tooltip, string startPath, FileDialogManager manager ) + { + _tmpPath ??= Path; + using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 3 * ImGuiHelpers.GlobalScale, 0 ) ); + ImGui.SetNextItemWidth( -ImGui.GetFrameHeight() - 3 * ImGuiHelpers.GlobalScale ); + ImGui.InputTextWithHint( label, hint, ref _tmpPath, Utf8GamePath.MaxGamePathLength ); + if( ImGui.IsItemDeactivatedAfterEdit() ) + { + Load( _tmpPath ); + } + + ImGuiUtil.HoverTooltip( tooltip ); + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Folder.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), string.Empty, false, + true ) ) + { + if( Penumbra.Config.DefaultModImportPath.Length > 0 ) + { + startPath = Penumbra.Config.DefaultModImportPath; + } + + var texture = this; + + void UpdatePath( bool success, List< string > paths ) + { + if( success && paths.Count > 0 ) + { + texture.Load( paths[ 0 ] ); + } + } + + manager.OpenFileDialog( "Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath ); + } + } +} \ No newline at end of file diff --git a/Penumbra/Import/Dds/TextureImporter.cs b/Penumbra/Import/Textures/TextureImporter.cs similarity index 100% rename from Penumbra/Import/Dds/TextureImporter.cs rename to Penumbra/Import/Textures/TextureImporter.cs diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index d9c64cc1..f56f9572 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -16,7 +16,7 @@ public partial class Mod // - Not Empty // - Unique, by appending (digit) for duplicates. // - Containing no symbols invalid for FFXIV or windows paths. - internal static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName ) + internal static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName, bool create = true ) { var name = modListName; if( name.Length == 0 ) @@ -31,7 +31,11 @@ public partial class Mod throw new IOException( "Could not create mod folder: too many folders of the same name exist." ); } - Directory.CreateDirectory( newModFolder ); + if( create ) + { + Directory.CreateDirectory( newModFolder ); + } + return new DirectoryInfo( newModFolder ); } diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs index 0f91ec56..cadae96e 100644 --- a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs +++ b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs @@ -368,6 +368,16 @@ public partial class ModEditWindow private static bool DrawColorSetRow( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled ) { + static bool FixFloat( ref float val, float current ) + { + if( val < 0 ) + { + val = 0; + } + + return val != current; + } + using var id = ImRaii.PushId( rowIdx ); var row = file.ColorSets[ colorSetIdx ].Rows[ rowIdx ]; var hasDye = file.ColorDyeSets.Length > colorSetIdx; @@ -383,7 +393,7 @@ public partial class ModEditWindow ImGui.TextUnformatted( $"#{rowIdx + 1:D2}" ); ImGui.TableNextColumn(); - using var dis = ImRaii.Disabled(disabled); + using var dis = ImRaii.Disabled( disabled ); ret |= ColorPicker( "##Diffuse", "Diffuse Color", row.Diffuse, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = c ); if( hasDye ) { @@ -397,7 +407,7 @@ public partial class ModEditWindow ImGui.SameLine(); var tmpFloat = row.SpecularStrength; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.SpecularStrength ) + if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat(ref tmpFloat, row.SpecularStrength) ) { file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat; ret = true; @@ -427,7 +437,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); tmpFloat = row.GlossStrength; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.GlossStrength ) + if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.GlossStrength ) ) { file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = tmpFloat; ret = true; @@ -455,7 +465,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); tmpFloat = row.MaterialRepeat.X; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.MaterialRepeat.X ) + if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, 0f ) && FixFloat(ref tmpFloat, row.MaterialRepeat.X) ) { file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; ret = true; @@ -465,7 +475,7 @@ public partial class ModEditWindow ImGui.SameLine(); tmpFloat = row.MaterialRepeat.Y; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.MaterialRepeat.Y ) + if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.Y ) ) { file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; ret = true; @@ -476,7 +486,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); tmpFloat = row.MaterialSkew.X; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.MaterialSkew.X ) + if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.X ) ) { file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat }; ret = true; @@ -487,7 +497,7 @@ public partial class ModEditWindow ImGui.SameLine(); tmpFloat = row.MaterialSkew.Y; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.MaterialSkew.Y ) + if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.Y ) ) { file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; ret = true; diff --git a/Penumbra/UI/Classes/ModEditWindow.Textures.cs b/Penumbra/UI/Classes/ModEditWindow.Textures.cs index 9b07b8a1..1cebaa0e 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Textures.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Textures.cs @@ -1,702 +1,99 @@ using System; -using System.Collections.Generic; using System.IO; using System.Numerics; -using System.Threading.Tasks; -using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Logging; -using Dalamud.Utility; using ImGuiNET; -using ImGuiScene; -using Lumina.Data.Files; using OtterGui; using OtterGui.Raii; using OtterTex; -using Penumbra.GameData.ByteString; -using Penumbra.Import.Dds; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.PixelFormats; -using Image = SixLabors.ImageSharp.Image; +using Penumbra.Import.Textures; namespace Penumbra.UI.Classes; -public class Texture : IDisposable -{ - // Path to the file we tried to load. - public string Path = string.Empty; - - // 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 >(); - - // 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 Texture() - { } - - public void Draw( Vector2 size ) - { - if( TextureWrap != null ) - { - ImGui.TextUnformatted( $"Image Dimensions: {TextureWrap.Width} x {TextureWrap.Height}" ); - 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 ); - } - else if( LoadError != null ) - { - ImGui.TextUnformatted( "Could not load file:" ); - ImGuiUtil.TextColored( Colors.RegexWarningBorder, LoadError.ToString() ); - } - else - { - ImGui.Dummy( size ); - } - } - - private void Clean() - { - RGBAPixels = Array.Empty< byte >(); - TextureWrap?.Dispose(); - TextureWrap = null; - ( BaseImage as IDisposable )?.Dispose(); - BaseImage = null; - Loaded?.Invoke( false ); - } - - public void Dispose() - => Clean(); - - public event Action< bool >? Loaded; - - private void Load( string path ) - { - _tmpPath = null; - if( path == Path ) - { - return; - } - - Path = path; - Clean(); - try - { - var _ = System.IO.Path.GetExtension( Path ) switch - { - ".dds" => LoadDds(), - ".png" => LoadPng(), - ".tex" => LoadTex(), - _ => true, - }; - Loaded?.Invoke( true ); - } - catch( Exception e ) - { - LoadError = e; - Clean(); - } - } - - private bool LoadDds() - { - 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( f.Meta.Width, f.Meta.Height ); - return true; - } - - private bool LoadPng() - { - 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( png.Width, png.Height ); - return true; - } - - private bool LoadTex() - { - var tex = System.IO.Path.IsPathRooted( Path ) - ? Dalamud.GameData.GameData.GetFileFromDisk< TexFile >( Path ) - : Dalamud.GameData.GetFile< TexFile >( Path ); - BaseImage = tex ?? throw new Exception( "Could not read .tex file." ); - RGBAPixels = tex.GetRgbaImageData(); - CreateTextureWrap( tex.Header.Width, tex.Header.Height ); - return true; - } - - private void CreateTextureWrap( int width, int height ) - => TextureWrap = Dalamud.PluginInterface.UiBuilder.LoadImageRaw( RGBAPixels, width, height, 4 ); - - private string? _tmpPath; - - public void PathInputBox( string label, string hint, string tooltip, string startPath, FileDialogManager manager ) - { - _tmpPath ??= Path; - using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 3 * ImGuiHelpers.GlobalScale, 0 ) ); - ImGui.SetNextItemWidth( -ImGui.GetFrameHeight() - 3 * ImGuiHelpers.GlobalScale ); - ImGui.InputTextWithHint( label, hint, ref _tmpPath, Utf8GamePath.MaxGamePathLength ); - if( ImGui.IsItemDeactivatedAfterEdit() ) - { - Load( _tmpPath ); - } - - ImGuiUtil.HoverTooltip( tooltip ); - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Folder.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), string.Empty, false, - true ) ) - { - if( Penumbra.Config.DefaultModImportPath.Length > 0 ) - { - startPath = Penumbra.Config.DefaultModImportPath; - } - - var texture = this; - - void UpdatePath( bool success, List< string > paths ) - { - if( success && paths.Count > 0 ) - { - texture.Load( paths[ 0 ] ); - } - } - - manager.OpenFileDialog( "Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath ); - } - } - - public static Texture Combined( Texture left, Texture right, InputManipulations leftManips, InputManipulations rightManips ) - => new(); -} - -public struct InputManipulations -{ - public InputManipulations() - { } - - public Matrix4x4 _multiplier = Matrix4x4.Identity; - public bool _invert = false; - public int _offsetX = 0; - public int _offsetY = 0; - public int _outputWidth = 0; - public int _outputHeight = 0; - - - private static Vector4 CappedVector( IReadOnlyList< byte >? bytes, int offset, Matrix4x4 transform, bool invert ) - { - if( bytes == null ) - { - return Vector4.Zero; - } - - var rgba = new Rgba32( bytes[ offset ], bytes[ offset + 1 ], bytes[ offset + 2 ], bytes[ offset + 3 ] ); - var transformed = Vector4.Transform( rgba.ToVector4(), transform ); - if( invert ) - { - transformed = new Vector4( 1 - transformed.X, 1 - transformed.Y, 1 - transformed.Z, transformed.W ); - } - - transformed.X = Math.Clamp( transformed.X, 0, 1 ); - transformed.Y = Math.Clamp( transformed.Y, 0, 1 ); - transformed.Z = Math.Clamp( transformed.Z, 0, 1 ); - transformed.W = Math.Clamp( transformed.W, 0, 1 ); - return transformed; - } - - private static bool DragFloat( string label, float width, ref float value ) - { - var tmp = value; - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( width ); - if( ImGui.DragFloat( label, ref tmp, 0.001f, -1f, 1f ) ) - { - value = tmp; - } - - return ImGui.IsItemDeactivatedAfterEdit(); - } - - - public bool DrawMatrixInput( float width ) - { - using var table = ImRaii.Table( string.Empty, 5, ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { - return false; - } - - var changes = false; - - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGuiUtil.Center( "R" ); - ImGui.TableNextColumn(); - ImGuiUtil.Center( "G" ); - ImGui.TableNextColumn(); - ImGuiUtil.Center( "B" ); - ImGui.TableNextColumn(); - ImGuiUtil.Center( "A" ); - - var inputWidth = width / 6; - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Text( "R " ); - changes |= DragFloat( "##RR", inputWidth, ref _multiplier.M11 ); - changes |= DragFloat( "##RG", inputWidth, ref _multiplier.M12 ); - changes |= DragFloat( "##RB", inputWidth, ref _multiplier.M13 ); - changes |= DragFloat( "##RA", inputWidth, ref _multiplier.M14 ); - - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Text( "G " ); - changes |= DragFloat( "##GR", inputWidth, ref _multiplier.M21 ); - changes |= DragFloat( "##GG", inputWidth, ref _multiplier.M22 ); - changes |= DragFloat( "##GB", inputWidth, ref _multiplier.M23 ); - changes |= DragFloat( "##GA", inputWidth, ref _multiplier.M24 ); - - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Text( "B " ); - changes |= DragFloat( "##BR", inputWidth, ref _multiplier.M31 ); - changes |= DragFloat( "##BG", inputWidth, ref _multiplier.M32 ); - changes |= DragFloat( "##BB", inputWidth, ref _multiplier.M33 ); - changes |= DragFloat( "##BA", inputWidth, ref _multiplier.M34 ); - - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Text( "A " ); - changes |= DragFloat( "##AR", inputWidth, ref _multiplier.M41 ); - changes |= DragFloat( "##AG", inputWidth, ref _multiplier.M42 ); - changes |= DragFloat( "##AB", inputWidth, ref _multiplier.M43 ); - changes |= DragFloat( "##AA", inputWidth, ref _multiplier.M44 ); - - return changes; - } -} - public partial class ModEditWindow { - private string _pathLeft = string.Empty; - private string _pathRight = string.Empty; + private readonly Texture _left = new(); + private readonly Texture _right = new(); + private readonly CombinedTexture _center; - private byte[]? _imageLeft; - private byte[]? _imageRight; - private byte[]? _imageCenter; + private readonly FileDialogManager _dialogManager = ConfigWindow.SetupFileManager(); + private bool _overlayCollapsed = true; + private DXGIFormat _currentFormat = DXGIFormat.R8G8B8A8UNorm; - private TextureWrap? _wrapLeft; - private TextureWrap? _wrapRight; - private TextureWrap? _wrapCenter; - - private Matrix4x4 _multiplierLeft = Matrix4x4.Identity; - private Matrix4x4 _multiplierRight = Matrix4x4.Identity; - private bool _invertLeft = false; - private bool _invertRight = false; - private int _offsetX = 0; - private int _offsetY = 0; - - private readonly FileDialogManager _dialogManager = ConfigWindow.SetupFileManager(); - - private static bool DragFloat( string label, float width, ref float value ) + private void DrawInputChild( string label, Texture tex, Vector2 size, Vector2 imageSize ) { - var tmp = value; - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( width ); - if( ImGui.DragFloat( label, ref tmp, 0.001f, -1f, 1f ) ) - { - value = tmp; - } - - return ImGui.IsItemDeactivatedAfterEdit(); - } - - private static bool DrawMatrixInput( float width, ref Matrix4x4 matrix ) - { - using var table = ImRaii.Table( string.Empty, 5, ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { - return false; - } - - var changes = false; - - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGuiUtil.Center( "R" ); - ImGui.TableNextColumn(); - ImGuiUtil.Center( "G" ); - ImGui.TableNextColumn(); - ImGuiUtil.Center( "B" ); - ImGui.TableNextColumn(); - ImGuiUtil.Center( "A" ); - - var inputWidth = width / 6; - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Text( "R " ); - changes |= DragFloat( "##RR", inputWidth, ref matrix.M11 ); - changes |= DragFloat( "##RG", inputWidth, ref matrix.M12 ); - changes |= DragFloat( "##RB", inputWidth, ref matrix.M13 ); - changes |= DragFloat( "##RA", inputWidth, ref matrix.M14 ); - - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Text( "G " ); - changes |= DragFloat( "##GR", inputWidth, ref matrix.M21 ); - changes |= DragFloat( "##GG", inputWidth, ref matrix.M22 ); - changes |= DragFloat( "##GB", inputWidth, ref matrix.M23 ); - changes |= DragFloat( "##GA", inputWidth, ref matrix.M24 ); - - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Text( "B " ); - changes |= DragFloat( "##BR", inputWidth, ref matrix.M31 ); - changes |= DragFloat( "##BG", inputWidth, ref matrix.M32 ); - changes |= DragFloat( "##BB", inputWidth, ref matrix.M33 ); - changes |= DragFloat( "##BA", inputWidth, ref matrix.M34 ); - - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Text( "A " ); - changes |= DragFloat( "##AR", inputWidth, ref matrix.M41 ); - changes |= DragFloat( "##AG", inputWidth, ref matrix.M42 ); - changes |= DragFloat( "##AB", inputWidth, ref matrix.M43 ); - changes |= DragFloat( "##AA", inputWidth, ref matrix.M44 ); - - return changes; - } - - private void PathInputBox( string label, string hint, string tooltip, int which ) - { - var tmp = which == 0 ? _pathLeft : _pathRight; - using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 3 * ImGuiHelpers.GlobalScale, 0 ) ); - ImGui.SetNextItemWidth( -ImGui.GetFrameHeight() - 3 * ImGuiHelpers.GlobalScale ); - ImGui.InputTextWithHint( label, hint, ref tmp, Utf8GamePath.MaxGamePathLength ); - if( ImGui.IsItemDeactivatedAfterEdit() ) - { - UpdateImage( tmp, which ); - } - - ImGuiUtil.HoverTooltip( tooltip ); - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Folder.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), string.Empty, false, - true ) ) - { - var startPath = Penumbra.Config.DefaultModImportPath.Length > 0 ? Penumbra.Config.DefaultModImportPath : _mod?.ModPath.FullName; - - void UpdatePath( bool success, List< string > paths ) - { - if( success && paths.Count > 0 ) - { - UpdateImage( paths[ 0 ], which ); - } - } - - _dialogManager.OpenFileDialog( "Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath ); - } - } - - private static (byte[]?, int, int) GetDdsRgbaData( string path ) - { - try - { - if( !ScratchImage.LoadDDS( path, out var f ) ) - { - return ( null, 0, 0 ); - } - - if( !f.GetRGBA( out f ) ) - { - return ( null, 0, 0 ); - } - - return ( f.Pixels[ ..( f.Meta.Width * f.Meta.Height * 4 ) ].ToArray(), f.Meta.Width, f.Meta.Height ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not parse DDS {path} to RGBA:\n{e}" ); - return ( null, 0, 0 ); - } - } - - private static ( byte[]?, int, int) GetTexRgbaData( string path, bool fromDisk ) - { - try - { - var tex = fromDisk ? Dalamud.GameData.GameData.GetFileFromDisk< TexFile >( path ) : Dalamud.GameData.GetFile< TexFile >( path ); - if( tex == null ) - { - return ( null, 0, 0 ); - } - - var rgba = tex.GetRgbaImageData(); - return ( rgba, tex.Header.Width, tex.Header.Height ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not parse TEX {path} to RGBA:\n{e}" ); - return ( null, 0, 0 ); - } - } - - private static (byte[]?, int, int) GetPngRgbaData( string path ) - { - try - { - using var stream = File.OpenRead( path ); - using var png = Image.Load< Rgba32 >( stream ); - var bytes = new byte[png.Height * png.Width * 4]; - png.CopyPixelDataTo( bytes ); - return ( bytes, png.Width, png.Height ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not parse PNG {path} to RGBA:\n{e}" ); - return ( null, 0, 0 ); - } - } - - private void UpdateImage( string newPath, int which ) - { - if( which is < 0 or > 1 ) + using var child = ImRaii.Child( label, size, true ); + if( !child ) { return; } - ref var path = ref which == 0 ? ref _pathLeft : ref _pathRight; - if( path == newPath ) + using var id = ImRaii.PushId( label ); + ImGuiUtil.DrawTextButton( label, new Vector2( -1, 0 ), ImGui.GetColorU32( ImGuiCol.FrameBg ) ); + ImGui.NewLine(); + + tex.PathInputBox( "##input", "Import Image...", "Can import game paths as well as your own files.", _mod!.ModPath.FullName, + _dialogManager ); + + if( tex == _left ) { - return; - } - - path = newPath; - ref var data = ref which == 0 ? ref _imageLeft : ref _imageRight; - ref var wrap = ref which == 0 ? ref _wrapLeft : ref _wrapRight; - - data = null; - wrap?.Dispose(); - wrap = null; - var width = 0; - var height = 0; - - if( Path.IsPathRooted( path ) ) - { - if( File.Exists( path ) ) - { - ( data, width, height ) = Path.GetExtension( path ) switch - { - ".dds" => GetDdsRgbaData( path ), - ".png" => GetPngRgbaData( path ), - ".tex" => GetTexRgbaData( path, true ), - _ => ( null, 0, 0 ), - }; - } + _center.DrawMatrixInputLeft( size.X ); } else { - ( data, width, height ) = GetTexRgbaData( path, false ); + _center.DrawMatrixInputRight( size.X ); } - if( data != null ) + ImGui.NewLine(); + tex.Draw( imageSize ); + } + + private void DrawOutputChild( Vector2 size, Vector2 imageSize ) + { + using var child = ImRaii.Child( "Output", size, true ); + if( !child ) { - try + return; + } + + if( _center.IsLoaded ) + { + if( ImGui.Button( "Save as TEX", -Vector2.UnitX ) ) { - wrap = Dalamud.PluginInterface.UiBuilder.LoadImageRaw( data, width, height, 4 ); + var fileName = Path.GetFileNameWithoutExtension( _left.Path.Length > 0 ? _left.Path : _right.Path ); + _dialogManager.SaveFileDialog( "Save Texture as TEX...", ".tex", fileName, ".tex", ( a, b ) => { }, _mod!.ModPath.FullName ); } - catch( Exception e ) + + if( ImGui.Button( "Save as DDS", -Vector2.UnitX ) ) { - PluginLog.Error( $"Could not load raw image:\n{e}" ); + var fileName = Path.GetFileNameWithoutExtension( _right.Path.Length > 0 ? _right.Path : _left.Path ); + _dialogManager.SaveFileDialog( "Save Texture as DDS...", ".dds", fileName, ".dds", ( a, b ) => { if( a ) _center.SaveAsDDS( b, _currentFormat, false ); }, _mod!.ModPath.FullName ); } - } - UpdateCenter(); - } - - private static Vector4 CappedVector( IReadOnlyList< byte >? bytes, int offset, Matrix4x4 transform, bool invert ) - { - if( bytes == null ) - { - return Vector4.Zero; - } - - var rgba = new Rgba32( bytes[ offset ], bytes[ offset + 1 ], bytes[ offset + 2 ], bytes[ offset + 3 ] ); - var transformed = Vector4.Transform( rgba.ToVector4(), transform ); - if( invert ) - { - transformed = new Vector4( 1 - transformed.X, 1 - transformed.Y, 1 - transformed.Z, transformed.W ); - } - - transformed.X = Math.Clamp( transformed.X, 0, 1 ); - transformed.Y = Math.Clamp( transformed.Y, 0, 1 ); - transformed.Z = Math.Clamp( transformed.Z, 0, 1 ); - transformed.W = Math.Clamp( transformed.W, 0, 1 ); - return transformed; - } - - private Vector4 DataLeft( int offset ) - => CappedVector( _imageLeft, offset, _multiplierLeft, _invertLeft ); - - private Vector4 DataRight( int x, int y ) - { - if( _imageRight == null ) - { - return Vector4.Zero; - } - - x -= _offsetX; - y -= _offsetY; - if( x < 0 || x >= _wrapRight!.Width || y < 0 || y >= _wrapRight!.Height ) - { - return Vector4.Zero; - } - - var offset = ( y * _wrapRight!.Width + x ) * 4; - return CappedVector( _imageRight, offset, _multiplierRight, _invertRight ); - } - - private void AddPixels( int width, int x, int y ) - { - var offset = ( width * y + x ) * 4; - var left = DataLeft( offset ); - var right = DataRight( x, y ); - var alpha = right.W + left.W * ( 1 - right.W ); - if( alpha == 0 ) - { - return; - } - - var sum = ( right * right.W + left * left.W * ( 1 - right.W ) ) / alpha; - var rgba = new Rgba32( sum with { W = alpha } ); - _imageCenter![ offset ] = rgba.R; - _imageCenter![ offset + 1 ] = rgba.G; - _imageCenter![ offset + 2 ] = rgba.B; - _imageCenter![ offset + 3 ] = rgba.A; - } - - private void UpdateCenter() - { - if( _imageLeft != null && _imageRight == null && _multiplierLeft.IsIdentity && !_invertLeft ) - { - _imageCenter = _imageLeft; - _wrapCenter = _wrapLeft; - return; - } - - if( _imageLeft == null && _imageRight != null && _multiplierRight.IsIdentity && !_invertRight ) - { - _imageCenter = _imageRight; - _wrapCenter = _wrapRight; - return; - } - - if( !ReferenceEquals( _imageCenter, _imageLeft ) && !ReferenceEquals( _imageCenter, _imageRight ) ) - { - _wrapCenter?.Dispose(); - } - - if( _imageLeft != null || _imageRight != null ) - { - var (totalWidth, totalHeight) = - _imageLeft != null ? ( _wrapLeft!.Width, _wrapLeft.Height ) : ( _wrapRight!.Width, _wrapRight.Height ); - _imageCenter = new byte[4 * totalWidth * totalHeight]; - - Parallel.For( 0, totalHeight - 1, ( y, _ ) => + if( ImGui.Button( "Save as PNG", -Vector2.UnitX ) ) { - for( var x = 0; x < totalWidth; ++x ) - { - AddPixels( totalWidth, x, y ); - } - } ); - _wrapCenter = Dalamud.PluginInterface.UiBuilder.LoadImageRaw( _imageCenter, totalWidth, totalHeight, 4 ); - return; - } - - _imageCenter = null; - _wrapCenter = null; - } - - private static void ScaledImage( string path, TextureWrap? wrap, Vector2 size ) - { - if( wrap != null ) - { - ImGui.TextUnformatted( $"Image Dimensions: {wrap.Width} x {wrap.Height}" ); - size = size.X < wrap.Width - ? size with { Y = wrap.Height * size.X / wrap.Width } - : new Vector2( wrap.Width, wrap.Height ); - - ImGui.Image( wrap.ImGuiHandle, size ); - } - else if( path.Length > 0 ) - { - ImGui.TextUnformatted( "Could not load file." ); - } - else - { - ImGui.Dummy( size ); - } - } - - private void SaveAs( bool success, string path, int type ) - { - if( !success || _imageCenter == null || _wrapCenter == null ) - { - return; - } - - try - { - switch( type ) - { - case 0: - var img = Image.LoadPixelData< Rgba32 >( _imageCenter, _wrapCenter.Width, _wrapCenter.Height ); - img.Save( path, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression } ); - break; - case 1: - if( TextureImporter.RgbaBytesToTex( _imageCenter, _wrapCenter.Width, _wrapCenter.Height, out var tex ) ) - { - File.WriteAllBytes( path, tex ); - } - - break; - case 2: - //ScratchImage.LoadDDS( _imageCenter, ) - //if( TextureImporter.RgbaBytesToDds( _imageCenter, _wrapCenter.Width, _wrapCenter.Height, out var dds ) ) - //{ - // File.WriteAllBytes( path, dds ); - //} - - break; + var fileName = Path.GetFileNameWithoutExtension( _right.Path.Length > 0 ? _right.Path : _left.Path ); + _dialogManager.SaveFileDialog( "Save Texture as PNG...", ".png", fileName, ".png", ( a, b ) => { if (a) _center.SaveAsPng( b ); }, _mod!.ModPath.FullName ); } + + ImGui.NewLine(); } - catch( Exception e ) - { - PluginLog.Error( $"Could not save image to {path}:\n{e}" ); - } + + _center.Draw( imageSize ); } - private void SaveAsPng( bool success, string path ) - => SaveAs( success, path, 0 ); + private Vector2 GetChildWidth() + { + var windowWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - ImGui.GetTextLineHeight(); + if( _overlayCollapsed ) + { + var width = windowWidth - ImGui.GetStyle().FramePadding.X * 3; + return new Vector2( width / 2, -1 ); + } - private void SaveAsTex( bool success, string path ) - => SaveAs( success, path, 1 ); - - private void SaveAsDds( bool success, string path ) - => SaveAs( success, path, 2 ); + return new Vector2( ( windowWidth - ImGui.GetStyle().FramePadding.X * 5 ) / 3, -1 ); + } private void DrawTextureTab() { @@ -708,78 +105,38 @@ public partial class ModEditWindow return; } - var leftRightWidth = - new Vector2( - ( ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - ImGui.GetStyle().FramePadding.X * 4 ) / 3, -1 ); - var imageSize = new Vector2( leftRightWidth.X - ImGui.GetStyle().FramePadding.X * 2 ); - using( var child = ImRaii.Child( "ImageLeft", leftRightWidth, true ) ) + try { - if( child ) + var childWidth = GetChildWidth(); + var imageSize = new Vector2( childWidth.X - ImGui.GetStyle().FramePadding.X * 2 ); + DrawInputChild( "Input Texture", _left, childWidth, imageSize ); + ImGui.SameLine(); + DrawOutputChild( childWidth, imageSize ); + if( !_overlayCollapsed ) { - PathInputBox( "##ImageLeft", "Import Image...", string.Empty, 0 ); - - ImGui.NewLine(); - if( DrawMatrixInput( leftRightWidth.X, ref _multiplierLeft ) || ImGui.Checkbox( "Invert##Left", ref _invertLeft ) ) - { - UpdateCenter(); - } - - ImGui.NewLine(); - ScaledImage( _pathLeft, _wrapLeft, imageSize ); + ImGui.SameLine(); + DrawInputChild( "Overlay Texture", _right, childWidth, imageSize ); } + + ImGui.SameLine(); + DrawOverlayCollapseButton(); } - - ImGui.SameLine(); - using( var child = ImRaii.Child( "ImageMix", leftRightWidth, true ) ) + catch( Exception e ) { - if( child ) - { - if( _wrapCenter == null && _wrapLeft != null && _wrapRight != null ) - { - ImGui.TextUnformatted( "Images have incompatible resolutions." ); - } - else if( _wrapCenter != null ) - { - if( ImGui.Button( "Save as TEX", -Vector2.UnitX ) ) - { - var fileName = Path.GetFileNameWithoutExtension( _pathLeft.Length > 0 ? _pathLeft : _pathRight ); - _dialogManager.SaveFileDialog( "Save Texture as TEX...", ".tex", fileName, ".tex", SaveAsTex, _mod!.ModPath.FullName ); - } - - if( ImGui.Button( "Save as PNG", -Vector2.UnitX ) ) - { - var fileName = Path.GetFileNameWithoutExtension( _pathRight.Length > 0 ? _pathRight : _pathLeft ); - _dialogManager.SaveFileDialog( "Save Texture as PNG...", ".png", fileName, ".png", SaveAsPng, _mod!.ModPath.FullName ); - } - - if( ImGui.Button( "Save as DDS", -Vector2.UnitX ) ) - { - var fileName = Path.GetFileNameWithoutExtension( _pathRight.Length > 0 ? _pathRight : _pathLeft ); - _dialogManager.SaveFileDialog( "Save Texture as DDS...", ".dds", fileName, ".dds", SaveAsDds, _mod!.ModPath.FullName ); - } - - ImGui.NewLine(); - ScaledImage( string.Empty, _wrapCenter, imageSize ); - } - } - } - - ImGui.SameLine(); - using( var child = ImRaii.Child( "ImageRight", leftRightWidth, true ) ) - { - if( child ) - { - PathInputBox( "##ImageRight", "Import Image...", string.Empty, 1 ); - - ImGui.NewLine(); - if( DrawMatrixInput( leftRightWidth.X, ref _multiplierRight ) || ImGui.Checkbox( "Invert##Right", ref _invertRight ) ) - { - UpdateCenter(); - } - - ImGui.NewLine(); - ScaledImage( _pathRight, _wrapRight, imageSize ); - } + PluginLog.Error( $"Unknown Error while drawing textures:\n{e}" ); } } + + private void DrawOverlayCollapseButton() + { + var (label, tooltip) = _overlayCollapsed + ? ( ">", "Show a third panel in which you can import an additional texture as an overlay for the primary texture." ) + : ( "<", "Hide the overlay texture panel and clear the currently loaded overlay texture, if any." ); + if( ImGui.Button( label, new Vector2( ImGui.GetTextLineHeight(), ImGui.GetContentRegionAvail().Y ) ) ) + { + _overlayCollapsed = !_overlayCollapsed; + } + + ImGuiUtil.HoverTooltip( tooltip ); + } } \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 4e3693a4..36aae29d 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -11,6 +11,7 @@ using OtterGui.Raii; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; +using Penumbra.Import.Textures; using Penumbra.Mods; using Penumbra.Util; using static Penumbra.Mods.Mod; @@ -121,6 +122,12 @@ public partial class ModEditWindow : Window, IDisposable WindowName = sb.ToString(); } + public override void OnClose() + { + _left.Dispose(); + _right.Dispose(); + } + public override void Draw() { using var tabBar = ImRaii.TabBar( "##tabs" ); @@ -508,10 +515,14 @@ public partial class ModEditWindow : Window, IDisposable DrawMaterialPanel ); _modelTab = new FileEditor< MdlFile >( "Models (WIP)", ".mdl", () => _editor?.MdlFiles ?? Array.Empty< Editor.FileRegistry >(), DrawModelPanel ); + _center = new CombinedTexture( _left, _right ); } public void Dispose() { _editor?.Dispose(); + _left.Dispose(); + _right.Dispose(); + _center.Dispose(); } } \ No newline at end of file