Current Textures

This commit is contained in:
Ottermandias 2022-09-03 16:10:04 +02:00
parent 1fe334e33a
commit 6e82242a72
10 changed files with 758 additions and 762 deletions

View file

@ -81,8 +81,8 @@ public partial class TexToolsImporter : IDisposable
private void ImportFiles() private void ImportFiles()
{ {
State = ImporterState.None; State = ImporterState.None;
_currentModPackIdx = 0; _currentModPackIdx = 0;
foreach( var file in _modPackFiles ) foreach( var file in _modPackFiles )
{ {
_currentModDirectory = null; _currentModDirectory = null;

View file

@ -29,6 +29,7 @@ public partial class TexToolsImporter
using var archive = ArchiveFactory.Open( zfs ); using var archive = ArchiveFactory.Open( zfs );
var baseName = FindArchiveModMeta( archive, out var leadDir ); var baseName = FindArchiveModMeta( archive, out var leadDir );
var name = string.Empty;
_currentOptionIdx = 0; _currentOptionIdx = 0;
_currentNumOptions = 1; _currentNumOptions = 1;
_currentModName = modPackFile.Name; _currentModName = modPackFile.Name;
@ -44,7 +45,7 @@ public partial class TexToolsImporter
}; };
PluginLog.Log( $" -> Importing {archive.Type} Archive." ); PluginLog.Log( $" -> Importing {archive.Type} Archive." );
_currentModDirectory = Mod.CreateModFolder( _baseDirectory, baseName ); _currentModDirectory = Mod.CreateModFolder( _baseDirectory, Path.GetRandomFileName() );
var options = new ExtractionOptions() var options = new ExtractionOptions()
{ {
ExtractFullPath = true, ExtractFullPath = true,
@ -55,32 +56,61 @@ public partial class TexToolsImporter
_currentFileIdx = 0; _currentFileIdx = 0;
var reader = archive.ExtractAllEntries(); var reader = archive.ExtractAllEntries();
while(reader.MoveToNextEntry()) while( reader.MoveToNextEntry() )
{ {
_token.ThrowIfCancellationRequested(); _token.ThrowIfCancellationRequested();
if( reader.Entry.IsDirectory ) if( reader.Entry.IsDirectory )
{ {
++_currentFileIdx; --_currentNumFiles;
continue; continue;
} }
PluginLog.Log( " -> Extracting {0}", reader.Entry.Key ); 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; ++_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 ) if( leadDir )
{ {
_token.ThrowIfCancellationRequested(); _currentModDirectory = Mod.CreateModFolder( _baseDirectory, baseName, false );
var oldName = _currentModDirectory.FullName; Directory.Move( Path.Combine( oldName, baseName ), _currentModDirectory.FullName );
var tmpName = oldName + "__tmp"; Directory.Delete( oldName );
Directory.Move( oldName, tmpName );
Directory.Move( Path.Combine( tmpName, baseName ), oldName );
Directory.Delete( tmpName );
_currentModDirectory = new DirectoryInfo( oldName );
} }
else
{
_currentModDirectory = Mod.CreateModFolder( _baseDirectory, name, false );
Directory.Move( oldName, _currentModDirectory.FullName );
}
_currentModDirectory.Refresh();
return _currentModDirectory; return _currentModDirectory;
} }
@ -88,7 +118,7 @@ public partial class TexToolsImporter
// Search the archive for the meta.json file which needs to exist. // Search the archive for the meta.json file which needs to exist.
private static string FindArchiveModMeta( IArchive archive, out bool leadDir ) 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. // None found.
if( entry == null ) 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;
return ret.Length == 0 ? name : ret;
} }
} }

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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 );
}
}
}

View file

@ -16,7 +16,7 @@ public partial class Mod
// - Not Empty // - Not Empty
// - Unique, by appending (digit) for duplicates. // - Unique, by appending (digit) for duplicates.
// - Containing no symbols invalid for FFXIV or windows paths. // - 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; var name = modListName;
if( name.Length == 0 ) 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." ); 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 ); return new DirectoryInfo( newModFolder );
} }

View file

@ -368,6 +368,16 @@ public partial class ModEditWindow
private static bool DrawColorSetRow( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled ) 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 ); using var id = ImRaii.PushId( rowIdx );
var row = file.ColorSets[ colorSetIdx ].Rows[ rowIdx ]; var row = file.ColorSets[ colorSetIdx ].Rows[ rowIdx ];
var hasDye = file.ColorDyeSets.Length > colorSetIdx; var hasDye = file.ColorDyeSets.Length > colorSetIdx;
@ -383,7 +393,7 @@ public partial class ModEditWindow
ImGui.TextUnformatted( $"#{rowIdx + 1:D2}" ); ImGui.TextUnformatted( $"#{rowIdx + 1:D2}" );
ImGui.TableNextColumn(); 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 ); ret |= ColorPicker( "##Diffuse", "Diffuse Color", row.Diffuse, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = c );
if( hasDye ) if( hasDye )
{ {
@ -397,7 +407,7 @@ public partial class ModEditWindow
ImGui.SameLine(); ImGui.SameLine();
var tmpFloat = row.SpecularStrength; var tmpFloat = row.SpecularStrength;
ImGui.SetNextItemWidth( floatSize ); 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; file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat;
ret = true; ret = true;
@ -427,7 +437,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
tmpFloat = row.GlossStrength; tmpFloat = row.GlossStrength;
ImGui.SetNextItemWidth( floatSize ); 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; file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = tmpFloat;
ret = true; ret = true;
@ -455,7 +465,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
tmpFloat = row.MaterialRepeat.X; tmpFloat = row.MaterialRepeat.X;
ImGui.SetNextItemWidth( floatSize ); 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 }; file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat };
ret = true; ret = true;
@ -465,7 +475,7 @@ public partial class ModEditWindow
ImGui.SameLine(); ImGui.SameLine();
tmpFloat = row.MaterialRepeat.Y; tmpFloat = row.MaterialRepeat.Y;
ImGui.SetNextItemWidth( floatSize ); 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 }; file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat };
ret = true; ret = true;
@ -476,7 +486,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
tmpFloat = row.MaterialSkew.X; tmpFloat = row.MaterialSkew.X;
ImGui.SetNextItemWidth( floatSize ); 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 }; file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat };
ret = true; ret = true;
@ -487,7 +497,7 @@ public partial class ModEditWindow
ImGui.SameLine(); ImGui.SameLine();
tmpFloat = row.MaterialSkew.Y; tmpFloat = row.MaterialSkew.Y;
ImGui.SetNextItemWidth( floatSize ); 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 }; file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat };
ret = true; ret = true;

View file

@ -1,702 +1,99 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Numerics; using System.Numerics;
using System.Threading.Tasks;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Logging; using Dalamud.Logging;
using Dalamud.Utility;
using ImGuiNET; using ImGuiNET;
using ImGuiScene;
using Lumina.Data.Files;
using OtterGui; using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using OtterTex; using OtterTex;
using Penumbra.GameData.ByteString; using Penumbra.Import.Textures;
using Penumbra.Import.Dds;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using Image = SixLabors.ImageSharp.Image;
namespace Penumbra.UI.Classes; 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 public partial class ModEditWindow
{ {
private string _pathLeft = string.Empty; private readonly Texture _left = new();
private string _pathRight = string.Empty; private readonly Texture _right = new();
private readonly CombinedTexture _center;
private byte[]? _imageLeft; private readonly FileDialogManager _dialogManager = ConfigWindow.SetupFileManager();
private byte[]? _imageRight; private bool _overlayCollapsed = true;
private byte[]? _imageCenter; private DXGIFormat _currentFormat = DXGIFormat.R8G8B8A8UNorm;
private TextureWrap? _wrapLeft; private void DrawInputChild( string label, Texture tex, Vector2 size, Vector2 imageSize )
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 )
{ {
var tmp = value; using var child = ImRaii.Child( label, size, true );
ImGui.TableNextColumn(); if( !child )
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 )
{ {
return; return;
} }
ref var path = ref which == 0 ? ref _pathLeft : ref _pathRight; using var id = ImRaii.PushId( label );
if( path == newPath ) 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; _center.DrawMatrixInputLeft( size.X );
}
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 ),
};
}
} }
else 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(); if( ImGui.Button( "Save as PNG", -Vector2.UnitX ) )
}
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, _ ) =>
{ {
for( var x = 0; x < totalWidth; ++x ) 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 );
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;
} }
ImGui.NewLine();
} }
catch( Exception e )
{ _center.Draw( imageSize );
PluginLog.Error( $"Could not save image to {path}:\n{e}" );
}
} }
private void SaveAsPng( bool success, string path ) private Vector2 GetChildWidth()
=> SaveAs( success, path, 0 ); {
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 ) return new Vector2( ( windowWidth - ImGui.GetStyle().FramePadding.X * 5 ) / 3, -1 );
=> SaveAs( success, path, 1 ); }
private void SaveAsDds( bool success, string path )
=> SaveAs( success, path, 2 );
private void DrawTextureTab() private void DrawTextureTab()
{ {
@ -708,78 +105,38 @@ public partial class ModEditWindow
return; return;
} }
var leftRightWidth = try
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 ) )
{ {
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.SameLine();
DrawInputChild( "Overlay Texture", _right, childWidth, imageSize );
ImGui.NewLine();
if( DrawMatrixInput( leftRightWidth.X, ref _multiplierLeft ) || ImGui.Checkbox( "Invert##Left", ref _invertLeft ) )
{
UpdateCenter();
}
ImGui.NewLine();
ScaledImage( _pathLeft, _wrapLeft, imageSize );
} }
ImGui.SameLine();
DrawOverlayCollapseButton();
} }
catch( Exception e )
ImGui.SameLine();
using( var child = ImRaii.Child( "ImageMix", leftRightWidth, true ) )
{ {
if( child ) PluginLog.Error( $"Unknown Error while drawing textures:\n{e}" );
{
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 );
}
} }
} }
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 );
}
} }

View file

@ -11,6 +11,7 @@ using OtterGui.Raii;
using Penumbra.GameData.ByteString; using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.Import.Textures;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Util; using Penumbra.Util;
using static Penumbra.Mods.Mod; using static Penumbra.Mods.Mod;
@ -121,6 +122,12 @@ public partial class ModEditWindow : Window, IDisposable
WindowName = sb.ToString(); WindowName = sb.ToString();
} }
public override void OnClose()
{
_left.Dispose();
_right.Dispose();
}
public override void Draw() public override void Draw()
{ {
using var tabBar = ImRaii.TabBar( "##tabs" ); using var tabBar = ImRaii.TabBar( "##tabs" );
@ -508,10 +515,14 @@ public partial class ModEditWindow : Window, IDisposable
DrawMaterialPanel ); DrawMaterialPanel );
_modelTab = new FileEditor< MdlFile >( "Models (WIP)", ".mdl", () => _editor?.MdlFiles ?? Array.Empty< Editor.FileRegistry >(), _modelTab = new FileEditor< MdlFile >( "Models (WIP)", ".mdl", () => _editor?.MdlFiles ?? Array.Empty< Editor.FileRegistry >(),
DrawModelPanel ); DrawModelPanel );
_center = new CombinedTexture( _left, _right );
} }
public void Dispose() public void Dispose()
{ {
_editor?.Dispose(); _editor?.Dispose();
_left.Dispose();
_right.Dispose();
_center.Dispose();
} }
} }