mirror of
https://github.com/xivdev/Penumbra.git
synced 2026-01-03 14:23:43 +01:00
Current Textures
This commit is contained in:
parent
1fe334e33a
commit
6e82242a72
10 changed files with 758 additions and 762 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
228
Penumbra/Import/Textures/CombinedTexture.Manipulation.cs
Normal file
228
Penumbra/Import/Textures/CombinedTexture.Manipulation.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
184
Penumbra/Import/Textures/CombinedTexture.cs
Normal file
184
Penumbra/Import/Textures/CombinedTexture.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
183
Penumbra/Import/Textures/Texture.cs
Normal file
183
Penumbra/Import/Textures/Texture.cs
Normal 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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue