diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 90ee1a16..6dc005ee 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -65,26 +65,34 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide private ResourceNode CreateNodeFromGamePath(ResourceType type, nint sourceAddress, Utf8GamePath gamePath, bool @internal) => new(null, type, sourceAddress, gamePath, FilterFullPath(Collection.ResolvePath(gamePath) ?? new FullPath(gamePath)), @internal); - private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint sourceAddress, ResourceHandle* handle, bool @internal, - bool withName) + public static unsafe FullPath GetResourceHandlePath(ResourceHandle* handle) { - if (handle == null) - return null; - var name = handle->FileName(); if (name.IsEmpty) - return null; + return FullPath.Empty; if (name[0] == (byte)'|') { var pos = name.IndexOf((byte)'|', 1); if (pos < 0) - return null; + return FullPath.Empty; name = name.Substring(pos + 1); } - var fullPath = new FullPath(Utf8GamePath.FromByteString(name, out var p) ? p : Utf8GamePath.Empty); + return new FullPath(Utf8GamePath.FromByteString(name, out var p) ? p : Utf8GamePath.Empty); + } + + private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint sourceAddress, ResourceHandle* handle, bool @internal, + bool withName) + { + if (handle == null) + return null; + + var fullPath = GetResourceHandlePath(handle); + if (fullPath.InternalName.IsEmpty) + return null; + var gamePaths = Collection.ReverseResolvePath(fullPath).ToList(); fullPath = FilterFullPath(fullPath); @@ -161,7 +169,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (mtrl == null) return null; - var resource = (MtrlResource*)mtrl->ResourceHandle; + var resource = mtrl->ResourceHandle; var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint) mtrl, &resource->Handle, false, WithNames); if (node == null) return null; diff --git a/Penumbra/Interop/Structs/ConstantBuffer.cs b/Penumbra/Interop/Structs/ConstantBuffer.cs new file mode 100644 index 00000000..52df6477 --- /dev/null +++ b/Penumbra/Interop/Structs/ConstantBuffer.cs @@ -0,0 +1,31 @@ +using System; +using System.Runtime.InteropServices; + +namespace Penumbra.Interop.Structs; + +[StructLayout(LayoutKind.Explicit, Size = 0x70)] +public unsafe struct ConstantBuffer +{ + [FieldOffset(0x20)] + public int Size; + + [FieldOffset(0x24)] + public int Flags; + + [FieldOffset(0x28)] + private void* _maybeSourcePointer; + + public bool TryGetBuffer(out Span buffer) + { + if ((Flags & 0x4003) == 0 && _maybeSourcePointer != null) + { + buffer = new Span(_maybeSourcePointer, Size >> 2); + return true; + } + else + { + buffer = null; + return false; + } + } +} diff --git a/Penumbra/Interop/Structs/Material.cs b/Penumbra/Interop/Structs/Material.cs index 7cee271e..7b66531c 100644 --- a/Penumbra/Interop/Structs/Material.cs +++ b/Penumbra/Interop/Structs/Material.cs @@ -3,17 +3,42 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Render; namespace Penumbra.Interop.Structs; -[StructLayout( LayoutKind.Explicit )] +[StructLayout( LayoutKind.Explicit, Size = 0x40 )] public unsafe struct Material { [FieldOffset( 0x10 )] - public ResourceHandle* ResourceHandle; + public MtrlResource* ResourceHandle; + + [FieldOffset( 0x18 )] + public uint ShaderPackageFlags; + + [FieldOffset( 0x20 )] + public uint* ShaderKeys; + + public int ShaderKeyCount + => (int)((uint*)Textures - ShaderKeys); [FieldOffset( 0x28 )] - public void* MaterialData; + public ConstantBuffer* MaterialParameter; [FieldOffset( 0x30 )] - public void** Textures; + public TextureEntry* Textures; - public Texture* Texture( int index ) => ( Texture* )Textures[3 * index + 1]; + [FieldOffset( 0x38 )] + public ushort TextureCount; + + public Texture* Texture( int index ) => Textures[index].ResourceHandle->KernelTexture; + + [StructLayout( LayoutKind.Explicit, Size = 0x18 )] + public struct TextureEntry + { + [FieldOffset( 0x00 )] + public uint Id; + + [FieldOffset( 0x08 )] + public TextureResourceHandle* ResourceHandle; + + [FieldOffset( 0x10 )] + public uint SamplerFlags; + } } \ No newline at end of file diff --git a/Penumbra/Interop/Structs/MtrlResource.cs b/Penumbra/Interop/Structs/MtrlResource.cs index 28756877..424adfe4 100644 --- a/Penumbra/Interop/Structs/MtrlResource.cs +++ b/Penumbra/Interop/Structs/MtrlResource.cs @@ -8,8 +8,11 @@ public unsafe struct MtrlResource [FieldOffset( 0x00 )] public ResourceHandle Handle; + [FieldOffset( 0xC8 )] + public ShaderPackageResourceHandle* ShpkResourceHandle; + [FieldOffset( 0xD0 )] - public ushort* TexSpace; // Contains the offsets for the tex files inside the string list. + public TextureEntry* TexSpace; // Contains the offsets for the tex files inside the string list. [FieldOffset( 0xE0 )] public byte* StringList; @@ -24,8 +27,21 @@ public unsafe struct MtrlResource => StringList + ShpkOffset; public byte* TexString( int idx ) - => StringList + *( TexSpace + 4 + idx * 8 ); + => StringList + TexSpace[idx].PathOffset; public bool TexIsDX11( int idx ) - => *(TexSpace + 5 + idx * 8) >= 0x8000; + => TexSpace[idx].Flags >= 0x8000; + + [StructLayout(LayoutKind.Explicit, Size = 0x10)] + public struct TextureEntry + { + [FieldOffset( 0x00 )] + public TextureResourceHandle* ResourceHandle; + + [FieldOffset( 0x08 )] + public ushort PathOffset; + + [FieldOffset( 0x0A )] + public ushort Flags; + } } \ No newline at end of file diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 4de81903..5db0f8e1 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -1,5 +1,7 @@ using System; using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData; using Penumbra.GameData.Enums; @@ -18,12 +20,22 @@ public unsafe struct TextureResourceHandle public IntPtr Unk; [FieldOffset( 0x118 )] - public IntPtr KernelTexture; + public Texture* KernelTexture; [FieldOffset( 0x20 )] public IntPtr NewKernelTexture; } +[StructLayout(LayoutKind.Explicit)] +public unsafe struct ShaderPackageResourceHandle +{ + [FieldOffset( 0x0 )] + public ResourceHandle Handle; + + [FieldOffset( 0xB0 )] + public ShaderPackage* ShaderPackage; +} + [StructLayout( LayoutKind.Explicit )] public unsafe struct ResourceHandle { diff --git a/Penumbra/Interop/Structs/ShaderPackageUtility.cs b/Penumbra/Interop/Structs/ShaderPackageUtility.cs new file mode 100644 index 00000000..5bf95f5b --- /dev/null +++ b/Penumbra/Interop/Structs/ShaderPackageUtility.cs @@ -0,0 +1,19 @@ +using System.Runtime.InteropServices; + +namespace Penumbra.Interop.Structs; + +public static class ShaderPackageUtility +{ + [StructLayout(LayoutKind.Explicit, Size = 0xC)] + public unsafe struct Sampler + { + [FieldOffset(0x0)] + public uint Crc; + + [FieldOffset(0x4)] + public uint Id; + + [FieldOffset(0xA)] + public ushort Slot; + } +} diff --git a/Penumbra/Interop/Structs/TextureUtility.cs b/Penumbra/Interop/Structs/TextureUtility.cs new file mode 100644 index 00000000..ec9c4b71 --- /dev/null +++ b/Penumbra/Interop/Structs/TextureUtility.cs @@ -0,0 +1,36 @@ +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; + +namespace Penumbra.Interop.Structs; + +public unsafe static class TextureUtility +{ + private static readonly Functions Funcs = new(); + + public static Texture* Create2D(Device* device, int* size, byte mipLevel, uint textureFormat, uint flags, uint unk) + => ((delegate* unmanaged)Funcs.TextureCreate2D)(device, size, mipLevel, textureFormat, flags, unk); + + public static bool InitializeContents(Texture* texture, void* contents) + => ((delegate* unmanaged)Funcs.TextureInitializeContents)(texture, contents); + + public static void IncRef(Texture* texture) + => ((delegate* unmanaged)(*(void***)texture)[2])(texture); + + public static void DecRef(Texture* texture) + => ((delegate* unmanaged)(*(void***)texture)[3])(texture); + + private sealed class Functions + { + [Signature("E8 ?? ?? ?? ?? 8B 0F 48 8D 54 24")] + public nint TextureCreate2D = nint.Zero; + + [Signature("E9 ?? ?? ?? ?? 8B 02 25")] + public nint TextureInitializeContents = nint.Zero; + + public Functions() + { + SignatureHelper.Initialise(this); + } + } +} diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index acead332..ac873ce2 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -18,7 +18,7 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; -public class FileEditor where T : class, IWritable +public class FileEditor : IDisposable where T : class, IWritable { private readonly FileDialogService _fileDialog; private readonly IDataManager _gameData; @@ -26,7 +26,7 @@ public class FileEditor where T : class, IWritable public FileEditor(ModEditWindow owner, IDataManager gameData, Configuration config, FileDialogService fileDialog, string tabName, string fileType, Func> getFiles, Func drawEdit, Func getInitialPath, - Func parseFile) + Func parseFile) { _owner = owner; _gameData = gameData; @@ -39,6 +39,11 @@ public class FileEditor where T : class, IWritable _combo = new Combo(config, getFiles); } + ~FileEditor() + { + DoDispose(); + } + public void Draw() { using var tab = ImRaii.TabItem(_tabName); @@ -60,11 +65,23 @@ public class FileEditor where T : class, IWritable DrawFilePanel(); } - private readonly string _tabName; - private readonly string _fileType; - private readonly Func _drawEdit; - private readonly Func _getInitialPath; - private readonly Func _parseFile; + public void Dispose() + { + DoDispose(); + GC.SuppressFinalize(this); + } + + private void DoDispose() + { + (_currentFile as IDisposable)?.Dispose(); + _currentFile = null; + } + + private readonly string _tabName; + private readonly string _fileType; + private readonly Func _drawEdit; + private readonly Func _getInitialPath; + private readonly Func _parseFile; private FileRegistry? _currentPath; private T? _currentFile; @@ -99,7 +116,9 @@ public class FileEditor where T : class, IWritable if (file != null) { _defaultException = null; - _defaultFile = _parseFile(file.Data); + (_defaultFile as IDisposable)?.Dispose(); + _defaultFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file. + _defaultFile = _parseFile(file.Data, _defaultPath, false); } else { @@ -158,6 +177,7 @@ public class FileEditor where T : class, IWritable { _currentException = null; _currentPath = null; + (_currentFile as IDisposable)?.Dispose(); _currentFile = null; _changed = false; } @@ -181,10 +201,13 @@ public class FileEditor where T : class, IWritable try { var bytes = File.ReadAllBytes(_currentPath.File.FullName); - _currentFile = _parseFile(bytes); + (_currentFile as IDisposable)?.Dispose(); + _currentFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file. + _currentFile = _parseFile(bytes, _currentPath.File.FullName, true); } catch (Exception e) { + (_currentFile as IDisposable)?.Dispose(); _currentFile = null; _currentException = e; } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs index c03272ef..1d6c480a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs @@ -13,20 +13,20 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private bool DrawMaterialColorSetChange( MtrlFile file, bool disabled ) + private bool DrawMaterialColorSetChange( MtrlTab tab, bool disabled ) { - if( !file.ColorSets.Any( c => c.HasRows ) ) + if( !tab.Mtrl.ColorSets.Any( c => c.HasRows ) ) { return false; } - ColorSetCopyAllClipboardButton( file, 0 ); + ColorSetCopyAllClipboardButton( tab.Mtrl, 0 ); ImGui.SameLine(); - var ret = ColorSetPasteAllClipboardButton( file, 0 ); + var ret = ColorSetPasteAllClipboardButton( tab, 0 ); ImGui.SameLine(); ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); ImGui.SameLine(); - ret |= DrawPreviewDye( file, disabled ); + ret |= DrawPreviewDye( tab, disabled ); using var table = ImRaii.Table( "##ColorSets", 11, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV ); @@ -58,12 +58,12 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.TableHeader( "Dye Preview" ); - for( var j = 0; j < file.ColorSets.Length; ++j ) + for( var j = 0; j < tab.Mtrl.ColorSets.Length; ++j ) { using var _ = ImRaii.PushId( j ); for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) { - ret |= DrawColorSetRow( file, j, i, disabled ); + ret |= DrawColorSetRow( tab, j, i, disabled ); ImGui.TableNextRow(); } } @@ -95,33 +95,36 @@ public partial class ModEditWindow } } - private bool DrawPreviewDye( MtrlFile file, bool disabled ) + private bool DrawPreviewDye( MtrlTab tab, bool disabled ) { var (dyeId, (name, dyeColor, gloss)) = _stainService.StainCombo.CurrentSelection; var tt = dyeId == 0 ? "Select a preview dye first." : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."; if( ImGuiUtil.DrawDisabledButton( "Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0 ) ) { var ret = false; - for( var j = 0; j < file.ColorDyeSets.Length; ++j ) + for( var j = 0; j < tab.Mtrl.ColorDyeSets.Length; ++j ) { for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) { - ret |= file.ApplyDyeTemplate( _stainService.StmFile, j, i, dyeId ); + ret |= tab.Mtrl.ApplyDyeTemplate( _stainService.StmFile, j, i, dyeId ); } } + tab.UpdateColorSetPreview(); + return ret; } ImGui.SameLine(); var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye"; - _stainService.StainCombo.Draw( label, dyeColor, string.Empty, true, gloss); + if (_stainService.StainCombo.Draw(label, dyeColor, string.Empty, true, gloss)) + tab.UpdateColorSetPreview(); return false; } - private static unsafe bool ColorSetPasteAllClipboardButton( MtrlFile file, int colorSetIdx ) + private static unsafe bool ColorSetPasteAllClipboardButton( MtrlTab tab, int colorSetIdx ) { - if( !ImGui.Button( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) || file.ColorSets.Length <= colorSetIdx ) + if( !ImGui.Button( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) || tab.Mtrl.ColorSets.Length <= colorSetIdx ) { return false; } @@ -135,14 +138,14 @@ public partial class ModEditWindow return false; } - ref var rows = ref file.ColorSets[ colorSetIdx ].Rows; + ref var rows = ref tab.Mtrl.ColorSets[ colorSetIdx ].Rows; fixed( void* ptr = data, output = &rows ) { MemoryUtility.MemCpyUnchecked( output, ptr, Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() ); if( data.Length >= Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() + Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() - && file.ColorDyeSets.Length > colorSetIdx ) + && tab.Mtrl.ColorDyeSets.Length > colorSetIdx ) { - ref var dyeRows = ref file.ColorDyeSets[ colorSetIdx ].Rows; + ref var dyeRows = ref tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows; fixed( void* output2 = &dyeRows ) { MemoryUtility.MemCpyUnchecked( output2, ( byte* )ptr + Marshal.SizeOf< MtrlFile.ColorSet.RowArray >(), Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() ); @@ -150,6 +153,8 @@ public partial class ModEditWindow } } + tab.UpdateColorSetPreview(); + return true; } catch @@ -182,7 +187,7 @@ public partial class ModEditWindow } } - private static unsafe bool ColorSetPasteFromClipboardButton( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled ) + private static unsafe bool ColorSetPasteFromClipboardButton( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled ) { if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, "Import an exported row from your clipboard onto this row.", disabled, true ) ) @@ -192,20 +197,22 @@ public partial class ModEditWindow var text = ImGui.GetClipboardText(); var data = Convert.FromBase64String( text ); if( data.Length != MtrlFile.ColorSet.Row.Size + 2 - || file.ColorSets.Length <= colorSetIdx ) + || tab.Mtrl.ColorSets.Length <= colorSetIdx ) { return false; } fixed( byte* ptr = data ) { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorSet.Row* )ptr; - if( colorSetIdx < file.ColorDyeSets.Length ) + tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorSet.Row* )ptr; + if( colorSetIdx < tab.Mtrl.ColorDyeSets.Length ) { - file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorDyeSet.Row* )( ptr + MtrlFile.ColorSet.Row.Size ); + tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorDyeSet.Row* )( ptr + MtrlFile.ColorSet.Row.Size ); } } + tab.UpdateColorSetRowPreview(rowIdx); + return true; } catch @@ -217,7 +224,18 @@ public partial class ModEditWindow return false; } - private bool DrawColorSetRow( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled ) + private static void ColorSetHighlightButton( MtrlTab tab, int rowIdx, bool disabled ) + { + ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Crosshairs.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Highlight this row on your character, if possible.", disabled || tab.ColorSetPreviewers.Count == 0, true ); + + if( ImGui.IsItemHovered() ) + tab.HighlightColorSetRow( rowIdx ); + else if( tab.HighlightedColorSetRow == rowIdx ) + tab.CancelColorSetHighlight(); + } + + private bool DrawColorSetRow( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled ) { static bool FixFloat( ref float val, float current ) { @@ -226,38 +244,41 @@ public partial class ModEditWindow } using var id = ImRaii.PushId( rowIdx ); - var row = file.ColorSets[ colorSetIdx ].Rows[ rowIdx ]; - var hasDye = file.ColorDyeSets.Length > colorSetIdx; - var dye = hasDye ? file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] : new MtrlFile.ColorDyeSet.Row(); + var row = tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ]; + var hasDye = tab.Mtrl.ColorDyeSets.Length > colorSetIdx; + var dye = hasDye ? tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] : new MtrlFile.ColorDyeSet.Row(); var floatSize = 70 * UiHelpers.Scale; var intSize = 45 * UiHelpers.Scale; ImGui.TableNextColumn(); ColorSetCopyClipboardButton( row, dye ); ImGui.SameLine(); - var ret = ColorSetPasteFromClipboardButton( file, colorSetIdx, rowIdx, disabled ); + var ret = ColorSetPasteFromClipboardButton( tab, colorSetIdx, rowIdx, disabled ); + ImGui.SameLine(); + ColorSetHighlightButton( tab, rowIdx, disabled ); ImGui.TableNextColumn(); ImGui.TextUnformatted( $"#{rowIdx + 1:D2}" ); ImGui.TableNextColumn(); 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 => { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = c; tab.UpdateColorSetRowPreview(rowIdx); } ); if( hasDye ) { ImGui.SameLine(); ret |= ImGuiUtil.Checkbox( "##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = b, ImGuiHoveredFlags.AllowWhenDisabled ); + b => { tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = b; tab.UpdateColorSetRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled ); } ImGui.TableNextColumn(); - ret |= ColorPicker( "##Specular", "Specular Color", row.Specular, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Specular = c ); + ret |= ColorPicker( "##Specular", "Specular Color", row.Specular, c => { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Specular = c; tab.UpdateColorSetRowPreview(rowIdx); } ); ImGui.SameLine(); var tmpFloat = row.SpecularStrength; ImGui.SetNextItemWidth( floatSize ); if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.SpecularStrength ) ) { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat; - ret = true; + tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat; + ret = true; + tab.UpdateColorSetRowPreview(rowIdx); } ImGuiUtil.HoverTooltip( "Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled ); @@ -266,19 +287,19 @@ public partial class ModEditWindow { ImGui.SameLine(); ret |= ImGuiUtil.Checkbox( "##dyeSpecular", "Apply Specular Color on Dye", dye.Specular, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Specular = b, ImGuiHoveredFlags.AllowWhenDisabled ); + b => { tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Specular = b; tab.UpdateColorSetRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled ); ImGui.SameLine(); ret |= ImGuiUtil.Checkbox( "##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = b, ImGuiHoveredFlags.AllowWhenDisabled ); + b => { tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = b; tab.UpdateColorSetRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled ); } ImGui.TableNextColumn(); - ret |= ColorPicker( "##Emissive", "Emissive Color", row.Emissive, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = c ); + ret |= ColorPicker( "##Emissive", "Emissive Color", row.Emissive, c => { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = c; tab.UpdateColorSetRowPreview(rowIdx); } ); if( hasDye ) { ImGui.SameLine(); ret |= ImGuiUtil.Checkbox( "##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = b, ImGuiHoveredFlags.AllowWhenDisabled ); + b => { tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = b; tab.UpdateColorSetRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled ); } ImGui.TableNextColumn(); @@ -286,8 +307,9 @@ public partial class ModEditWindow ImGui.SetNextItemWidth( floatSize ); if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.GlossStrength ) ) { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = tmpFloat; - ret = true; + tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = tmpFloat; + ret = true; + tab.UpdateColorSetRowPreview(rowIdx); } ImGuiUtil.HoverTooltip( "Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled ); @@ -295,7 +317,7 @@ public partial class ModEditWindow { ImGui.SameLine(); ret |= ImGuiUtil.Checkbox( "##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Gloss = b, ImGuiHoveredFlags.AllowWhenDisabled ); + b => { tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Gloss = b; tab.UpdateColorSetRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled ); } ImGui.TableNextColumn(); @@ -303,8 +325,9 @@ public partial class ModEditWindow ImGui.SetNextItemWidth( intSize ); if( ImGui.InputInt( "##TileSet", ref tmpInt, 0, 0 ) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue ) { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )tmpInt; - ret = true; + tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )tmpInt; + ret = true; + tab.UpdateColorSetRowPreview(rowIdx); } ImGuiUtil.HoverTooltip( "Tile Set", ImGuiHoveredFlags.AllowWhenDisabled ); @@ -314,8 +337,9 @@ public partial class ModEditWindow ImGui.SetNextItemWidth( floatSize ); if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.X ) ) { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; - ret = true; + tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; + ret = true; + tab.UpdateColorSetRowPreview(rowIdx); } ImGuiUtil.HoverTooltip( "Repeat X", ImGuiHoveredFlags.AllowWhenDisabled ); @@ -324,8 +348,9 @@ public partial class ModEditWindow ImGui.SetNextItemWidth( floatSize ); if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.Y ) ) { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; - ret = true; + tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; + ret = true; + tab.UpdateColorSetRowPreview(rowIdx); } ImGuiUtil.HoverTooltip( "Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled ); @@ -335,8 +360,9 @@ public partial class ModEditWindow ImGui.SetNextItemWidth( floatSize ); if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.X ) ) { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat }; - ret = true; + tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat }; + ret = true; + tab.UpdateColorSetRowPreview(rowIdx); } ImGuiUtil.HoverTooltip( "Skew X", ImGuiHoveredFlags.AllowWhenDisabled ); @@ -346,8 +372,9 @@ public partial class ModEditWindow ImGui.SetNextItemWidth( floatSize ); if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.Y ) ) { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; - ret = true; + tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; + ret = true; + tab.UpdateColorSetRowPreview(rowIdx); } ImGuiUtil.HoverTooltip( "Skew Y", ImGuiHoveredFlags.AllowWhenDisabled ); @@ -358,14 +385,15 @@ public partial class ModEditWindow if(_stainService.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) ) { - file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = _stainService.TemplateCombo.CurrentSelection; - ret = true; + tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = _stainService.TemplateCombo.CurrentSelection; + ret = true; + tab.UpdateColorSetRowPreview(rowIdx); } ImGuiUtil.HoverTooltip( "Dye Template", ImGuiHoveredFlags.AllowWhenDisabled ); ImGui.TableNextColumn(); - ret |= DrawDyePreview( file, colorSetIdx, rowIdx, disabled, dye, floatSize ); + ret |= DrawDyePreview( tab, colorSetIdx, rowIdx, disabled, dye, floatSize ); } else { @@ -376,7 +404,7 @@ public partial class ModEditWindow return ret; } - private bool DrawDyePreview( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize ) + private bool DrawDyePreview( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize ) { var stain = _stainService.StainCombo.CurrentSelection.Key; if( stain == 0 || !_stainService.StmFile.Entries.TryGetValue( dye.Template, out var entry ) ) @@ -390,7 +418,9 @@ public partial class ModEditWindow var ret = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), "Apply the selected dye to this row.", disabled, true ); - ret = ret && file.ApplyDyeTemplate(_stainService.StmFile, colorSetIdx, rowIdx, stain ); + ret = ret && tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, colorSetIdx, rowIdx, stain ); + if (ret) + tab.UpdateColorSetRowPreview(rowIdx); ImGui.SameLine(); ColorPicker( "##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D" ); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs new file mode 100644 index 00000000..376e656f --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs @@ -0,0 +1,484 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using Dalamud.Game; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Penumbra.GameData.Files; +using Penumbra.Interop.ResourceTree; +using Structs = Penumbra.Interop.Structs; + +namespace Penumbra.UI.AdvancedWindow; + +public partial class ModEditWindow +{ + private static unsafe Character* FindLocalPlayer(IObjectTable objects) + { + var localPlayer = objects[0]; + if (localPlayer is not Dalamud.Game.ClientState.Objects.Types.Character) + return null; + + return (Character*)localPlayer.Address; + } + + private static unsafe Character* FindSubActor(Character* character, int subActorType) + { + if (character == null) + return null; + + switch (subActorType) + { + case -1: + return character; + case 0: + return character->Mount.MountObject; + case 1: + var companion = character->Companion.CompanionObject; + if (companion == null) + return null; + return &companion->Character; + case 2: + var ornament = character->Ornament.OrnamentObject; + if (ornament == null) + return null; + return &ornament->Character; + default: + return null; + } + } + + private static unsafe List<(int SubActorType, int ChildObjectIndex, int ModelSlot, int MaterialSlot)> FindMaterial(CharacterBase* drawObject, int subActorType, string materialPath) + { + static void CollectMaterials(List<(int, int, int, int)> result, int subActorType, int childObjectIndex, CharacterBase* drawObject, string materialPath) + { + for (var i = 0; i < drawObject->SlotCount; ++i) + { + var model = drawObject->Models[i]; + if (model == null) + continue; + + for (var j = 0; j < model->MaterialCount; ++j) + { + var material = model->Materials[j]; + if (material == null) + continue; + + var mtrlHandle = material->MaterialResourceHandle; + if (mtrlHandle == null) + continue; + + var path = ResolveContext.GetResourceHandlePath((Structs.ResourceHandle*)mtrlHandle); + if (path.ToString() == materialPath) + result.Add((subActorType, childObjectIndex, i, j)); + } + } + } + + var result = new List<(int, int, int, int)>(); + + if (drawObject == null) + return result; + + materialPath = materialPath.Replace('/', '\\').ToLowerInvariant(); + + CollectMaterials(result, subActorType, -1, drawObject, materialPath); + + var firstChildObject = (CharacterBase*)drawObject->DrawObject.Object.ChildObject; + if (firstChildObject != null) + { + var childObject = firstChildObject; + var childObjectIndex = 0; + do + { + CollectMaterials(result, subActorType, childObjectIndex, childObject, materialPath); + + childObject = (CharacterBase*)childObject->DrawObject.Object.NextSiblingObject; + ++childObjectIndex; + } + while (childObject != null && childObject != firstChildObject); + } + + return result; + } + + private static unsafe CharacterBase* GetChildObject(CharacterBase* drawObject, int index) + { + if (drawObject == null) + return null; + + if (index >= 0) + { + drawObject = (CharacterBase*)drawObject->DrawObject.Object.ChildObject; + if (drawObject == null) + return null; + } + + var first = drawObject; + while (index-- > 0) + { + drawObject = (CharacterBase*)drawObject->DrawObject.Object.NextSiblingObject; + if (drawObject == null || drawObject == first) + return null; + } + + return drawObject; + } + + private static unsafe Material* GetDrawObjectMaterial(CharacterBase* drawObject, int modelSlot, int materialSlot) + { + if (drawObject == null) + return null; + + if (modelSlot < 0 || modelSlot >= drawObject->SlotCount) + return null; + + var model = drawObject->Models[modelSlot]; + if (model == null) + return null; + + if (materialSlot < 0 || materialSlot >= model->MaterialCount) + return null; + + return model->Materials[materialSlot]; + } + + private abstract unsafe class LiveMaterialPreviewerBase : IDisposable + { + private readonly IObjectTable _objects; + + protected readonly int SubActorType; + protected readonly int ChildObjectIndex; + protected readonly int ModelSlot; + protected readonly int MaterialSlot; + + protected readonly CharacterBase* DrawObject; + protected readonly Material* Material; + + protected bool Valid; + + public LiveMaterialPreviewerBase(IObjectTable objects, int subActorType, int childObjectIndex, int modelSlot, int materialSlot) + { + _objects = objects; + + SubActorType = subActorType; + ChildObjectIndex = childObjectIndex; + ModelSlot = modelSlot; + MaterialSlot = materialSlot; + + var localPlayer = FindLocalPlayer(objects); + if (localPlayer == null) + throw new InvalidOperationException("Cannot retrieve local player object"); + + var subActor = FindSubActor(localPlayer, subActorType); + if (subActor == null) + throw new InvalidOperationException("Cannot retrieve sub-actor (mount, companion or ornament)"); + + DrawObject = GetChildObject((CharacterBase*)subActor->GameObject.GetDrawObject(), childObjectIndex); + if (DrawObject == null) + throw new InvalidOperationException("Cannot retrieve draw object"); + + Material = GetDrawObjectMaterial(DrawObject, modelSlot, materialSlot); + if (Material == null) + throw new InvalidOperationException("Cannot retrieve material"); + + Valid = true; + } + + ~LiveMaterialPreviewerBase() + { + if (Valid) + Dispose(false, IsStillValid()); + } + + public void Dispose() + { + if (Valid) + Dispose(true, IsStillValid()); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing, bool reset) + { + Valid = false; + } + + public bool CheckValidity() + { + if (Valid && !IsStillValid()) + Dispose(false, false); + + return Valid; + } + + protected virtual bool IsStillValid() + { + var localPlayer = FindLocalPlayer(_objects); + if (localPlayer == null) + return false; + + var subActor = FindSubActor(localPlayer, SubActorType); + if (subActor == null) + return false; + + if (DrawObject != GetChildObject((CharacterBase*)subActor->GameObject.GetDrawObject(), ChildObjectIndex)) + return false; + + if (Material != GetDrawObjectMaterial(DrawObject, ModelSlot, MaterialSlot)) + return false; + + return true; + } + } + + private sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase + { + private readonly ShaderPackage* _shaderPackage; + + private readonly uint _originalShPkFlags; + private readonly float[] _originalMaterialParameter; + private readonly uint[] _originalSamplerFlags; + + public LiveMaterialPreviewer(IObjectTable objects, int subActorType, int childObjectIndex, int modelSlot, int materialSlot) : base(objects, subActorType, childObjectIndex, modelSlot, materialSlot) + { + var mtrlHandle = Material->MaterialResourceHandle; + if (mtrlHandle == null) + throw new InvalidOperationException("Material doesn't have a resource handle"); + + var shpkHandle = ((Structs.MtrlResource*)mtrlHandle)->ShpkResourceHandle; + if (shpkHandle == null) + throw new InvalidOperationException("Material doesn't have a ShPk resource handle"); + + _shaderPackage = shpkHandle->ShaderPackage; + if (_shaderPackage == null) + throw new InvalidOperationException("Material doesn't have a shader package"); + + var material = (Structs.Material*)Material; + + _originalShPkFlags = material->ShaderPackageFlags; + + if (material->MaterialParameter->TryGetBuffer(out var materialParameter)) + _originalMaterialParameter = materialParameter.ToArray(); + else + _originalMaterialParameter = Array.Empty(); + + _originalSamplerFlags = new uint[material->TextureCount]; + for (var i = 0; i < _originalSamplerFlags.Length; ++i) + _originalSamplerFlags[i] = material->Textures[i].SamplerFlags; + } + + protected override void Dispose(bool disposing, bool reset) + { + base.Dispose(disposing, reset); + + if (reset) + { + var material = (Structs.Material*)Material; + + material->ShaderPackageFlags = _originalShPkFlags; + + if (material->MaterialParameter->TryGetBuffer(out var materialParameter)) + _originalMaterialParameter.AsSpan().CopyTo(materialParameter); + + for (var i = 0; i < _originalSamplerFlags.Length; ++i) + material->Textures[i].SamplerFlags = _originalSamplerFlags[i]; + } + } + + public void SetShaderPackageFlags(uint shPkFlags) + { + if (!CheckValidity()) + return; + + ((Structs.Material*)Material)->ShaderPackageFlags = shPkFlags; + } + + public void SetMaterialParameter(uint parameterCrc, Index offset, Span value) + { + if (!CheckValidity()) + return; + + var cbuffer = ((Structs.Material*)Material)->MaterialParameter; + if (cbuffer == null) + return; + + if (!cbuffer->TryGetBuffer(out var buffer)) + return; + + for (var i = 0; i < _shaderPackage->MaterialElementCount; ++i) + { + ref var parameter = ref _shaderPackage->MaterialElements[i]; + if (parameter.CRC == parameterCrc) + { + if ((parameter.Offset & 0x3) != 0 || (parameter.Size & 0x3) != 0 || (parameter.Offset + parameter.Size) >> 2 > buffer.Length) + return; + + value.TryCopyTo(buffer.Slice(parameter.Offset >> 2, parameter.Size >> 2)[offset..]); + return; + } + } + } + + public void SetSamplerFlags(uint samplerCrc, uint samplerFlags) + { + if (!CheckValidity()) + return; + + var id = 0u; + var found = false; + + var samplers = (Structs.ShaderPackageUtility.Sampler*)_shaderPackage->Samplers; + for (var i = 0; i < _shaderPackage->SamplerCount; ++i) + { + if (samplers[i].Crc == samplerCrc) + { + id = samplers[i].Id; + found = true; + break; + } + } + + if (!found) + return; + + var material = (Structs.Material*)Material; + for (var i = 0; i < material->TextureCount; ++i) + { + if (material->Textures[i].Id == id) + { + material->Textures[i].SamplerFlags = (samplerFlags & 0xFFFFFDFF) | 0x000001C0; + break; + } + } + } + + protected override bool IsStillValid() + { + if (!base.IsStillValid()) + return false; + + var mtrlHandle = Material->MaterialResourceHandle; + if (mtrlHandle == null) + return false; + + var shpkHandle = ((Structs.MtrlResource*)mtrlHandle)->ShpkResourceHandle; + if (shpkHandle == null) + return false; + + if (_shaderPackage != shpkHandle->ShaderPackage) + return false; + + return true; + } + } + + private sealed unsafe class LiveColorSetPreviewer : LiveMaterialPreviewerBase + { + public const int TextureWidth = 4; + public const int TextureHeight = MtrlFile.ColorSet.RowArray.NumRows; + public const int TextureLength = TextureWidth * TextureHeight * 4; + + private readonly Framework _framework; + + private readonly Texture** _colorSetTexture; + private readonly Texture* _originalColorSetTexture; + + private Half[] _colorSet; + private bool _updatePending; + + public Half[] ColorSet => _colorSet; + + public LiveColorSetPreviewer(IObjectTable objects, Framework framework, int subActorType, int childObjectIndex, int modelSlot, int materialSlot) : base(objects, subActorType, childObjectIndex, modelSlot, materialSlot) + { + _framework = framework; + + var mtrlHandle = Material->MaterialResourceHandle; + if (mtrlHandle == null) + throw new InvalidOperationException("Material doesn't have a resource handle"); + + var colorSetTextures = *(Texture***)((nint)DrawObject + 0x258); + if (colorSetTextures == null) + throw new InvalidOperationException("Draw object doesn't have color set textures"); + + _colorSetTexture = colorSetTextures + (modelSlot * 4 + materialSlot); + + _originalColorSetTexture = *_colorSetTexture; + if (_originalColorSetTexture == null) + throw new InvalidOperationException("Material doesn't have a color set"); + Structs.TextureUtility.IncRef(_originalColorSetTexture); + + _colorSet = new Half[TextureLength]; + _updatePending = true; + + framework.Update += OnFrameworkUpdate; + } + + protected override void Dispose(bool disposing, bool reset) + { + _framework.Update -= OnFrameworkUpdate; + + base.Dispose(disposing, reset); + + if (reset) + { + var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)_originalColorSetTexture); + Structs.TextureUtility.DecRef(oldTexture); + } + else + Structs.TextureUtility.DecRef(_originalColorSetTexture); + } + + public void ScheduleUpdate() + { + _updatePending = true; + } + + private void OnFrameworkUpdate(Framework _) + { + if (!_updatePending) + return; + _updatePending = false; + + if (!CheckValidity()) + return; + + var textureSize = stackalloc int[2]; + textureSize[0] = TextureWidth; + textureSize[1] = TextureHeight; + + var newTexture = Structs.TextureUtility.Create2D(Device.Instance(), textureSize, 1, 0x2460, 0x80000804, 7); + if (newTexture == null) + return; + + bool success; + lock (_colorSet) + fixed (Half* colorSet = _colorSet) + success = Structs.TextureUtility.InitializeContents(newTexture, colorSet); + + if (success) + { + var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)newTexture); + Structs.TextureUtility.DecRef(oldTexture); + } + else + Structs.TextureUtility.DecRef(newTexture); + } + + protected override bool IsStillValid() + { + if (!base.IsStillValid()) + return false; + + var colorSetTextures = *(Texture***)((nint)DrawObject + 0x258); + if (colorSetTextures == null) + return false; + + if (_colorSetTexture != colorSetTextures + (ModelSlot * 4 + MaterialSlot)) + return false; + + return true; + } + } +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 753ad8e9..9cff681a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -2,15 +2,20 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Files; -using Penumbra.Services; +using Penumbra.GameData.Structs; +using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.Util; using static Penumbra.GameData.Files.ShpkFile; @@ -19,10 +24,12 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private sealed class MtrlTab : IWritable + private sealed class MtrlTab : IWritable, IDisposable { private readonly ModEditWindow _edit; public readonly MtrlFile Mtrl; + public readonly string FilePath; + public readonly bool Writable; public uint NewKeyId; public uint NewKeyDefault; @@ -57,11 +64,17 @@ public partial class ModEditWindow public bool HasMalformedMaterialConstants; // Samplers - public readonly List< (string Label, string FileName) > Samplers = new(4); - public readonly List< (string Name, uint Id) > MissingSamplers = new(4); - public readonly HashSet< uint > DefinedSamplers = new(4); - public IndexSet OrphanedSamplers = new(0, false); - public int AliasedSamplerCount; + public readonly List< (string Label, string FileName, uint Id) > Samplers = new(4); + public readonly List< (string Name, uint Id) > MissingSamplers = new(4); + public readonly HashSet< uint > DefinedSamplers = new(4); + public IndexSet OrphanedSamplers = new(0, false); + public int AliasedSamplerCount; + + // Live-Previewers + public readonly List MaterialPreviewers = new(4); + public readonly List ColorSetPreviewers = new(4); + public int HighlightedColorSetRow = -1; + public int HighlightTime = -1; public FullPath FindAssociatedShpk( out string defaultPath, out Utf8GamePath defaultGamePath ) { @@ -243,7 +256,7 @@ public partial class ModEditWindow ? $"#{idx}: {shpk.Value.Name} (ID: 0x{sampler.SamplerId:X8})##{sampler.SamplerId}" : $"#{idx} (ID: 0x{sampler.SamplerId:X8})##{sampler.SamplerId}"; var fileName = $"Texture #{sampler.TextureIndex} - {Path.GetFileName( Mtrl.Textures[ sampler.TextureIndex ].Path )}"; - Samplers.Add( ( label, fileName ) ); + Samplers.Add( ( label, fileName, sampler.SamplerId ) ); } MissingSamplers.Clear(); @@ -269,6 +282,220 @@ public partial class ModEditWindow } } + public unsafe void BindToMaterialInstances() + { + UnbindFromMaterialInstances(); + + var localPlayer = FindLocalPlayer(_edit._dalamud.Objects); + if (null == localPlayer) + return; + + var drawObject = (CharacterBase*)localPlayer->GameObject.GetDrawObject(); + if (null == drawObject) + return; + + var instances = FindMaterial(drawObject, -1, FilePath); + + var drawObjects = stackalloc CharacterBase*[4]; + drawObjects[0] = drawObject; + + for (var i = 0; i < 3; ++i) + { + var subActor = FindSubActor(localPlayer, i); + if (null == subActor) + continue; + + var subDrawObject = (CharacterBase*)subActor->GameObject.GetDrawObject(); + if (null == subDrawObject) + continue; + + instances.AddRange(FindMaterial(subDrawObject, i, FilePath)); + drawObjects[i + 1] = subDrawObject; + } + + var foundMaterials = new HashSet(); + foreach (var (subActorType, childObjectIndex, modelSlot, materialSlot) in instances) + { + var material = GetDrawObjectMaterial(drawObjects[subActorType + 1], modelSlot, materialSlot); + if (foundMaterials.Contains((nint)material)) + continue; + try + { + MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._dalamud.Objects, subActorType, childObjectIndex, modelSlot, materialSlot)); + foundMaterials.Add((nint)material); + } + catch (InvalidOperationException) + { + // Carry on without that previewer. + } + } + + var colorSet = Mtrl.ColorSets.FirstOrNull(colorSet => colorSet.HasRows); + + if (colorSet.HasValue) + { + foreach (var (subActorType, childObjectIndex, modelSlot, materialSlot) in instances) + { + try + { + ColorSetPreviewers.Add(new LiveColorSetPreviewer(_edit._dalamud.Objects, _edit._dalamud.Framework, subActorType, childObjectIndex, modelSlot, materialSlot)); + } + catch (InvalidOperationException) + { + // Carry on without that previewer. + } + } + UpdateColorSetPreview(); + } + } + + public void UnbindFromMaterialInstances() + { + foreach (var previewer in MaterialPreviewers) + previewer.Dispose(); + MaterialPreviewers.Clear(); + + foreach (var previewer in ColorSetPreviewers) + previewer.Dispose(); + ColorSetPreviewers.Clear(); + } + + public void SetShaderPackageFlags(uint shPkFlags) + { + foreach (var previewer in MaterialPreviewers) + previewer.SetShaderPackageFlags(shPkFlags); + } + + public void SetMaterialParameter(uint parameterCrc, Index offset, Span value) + { + foreach (var previewer in MaterialPreviewers) + previewer.SetMaterialParameter(parameterCrc, offset, value); + } + + public void SetSamplerFlags(uint samplerCrc, uint samplerFlags) + { + foreach (var previewer in MaterialPreviewers) + previewer.SetSamplerFlags(samplerCrc, samplerFlags); + } + + public void HighlightColorSetRow(int rowIdx) + { + var oldRowIdx = HighlightedColorSetRow; + + HighlightedColorSetRow = rowIdx; + HighlightTime = (HighlightTime + 1) % 32; + + if (oldRowIdx >= 0) + UpdateColorSetRowPreview(oldRowIdx); + if (rowIdx >= 0) + UpdateColorSetRowPreview(rowIdx); + } + + public void CancelColorSetHighlight() + { + var rowIdx = HighlightedColorSetRow; + + HighlightedColorSetRow = -1; + HighlightTime = -1; + + if (rowIdx >= 0) + UpdateColorSetRowPreview(rowIdx); + } + + public unsafe void UpdateColorSetRowPreview(int rowIdx) + { + if (ColorSetPreviewers.Count == 0) + return; + + var maybeColorSet = Mtrl.ColorSets.FirstOrNull(colorSet => colorSet.HasRows); + if (!maybeColorSet.HasValue) + return; + + var colorSet = maybeColorSet.Value; + var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index); + + var row = colorSet.Rows[rowIdx]; + if (maybeColorDyeSet.HasValue) + { + var stm = _edit._stainService.StmFile; + var dye = maybeColorDyeSet.Value.Rows[rowIdx]; + if (stm.TryGetValue(dye.Template, (StainId)_edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) + ApplyDye(ref row, dye, dyes); + } + + if (HighlightedColorSetRow == rowIdx) + ApplyHighlight(ref row, HighlightTime); + + foreach (var previewer in ColorSetPreviewers) + { + fixed (Half* pDest = previewer.ColorSet) + Buffer.MemoryCopy(&row, pDest + LiveColorSetPreviewer.TextureWidth * 4 * rowIdx, LiveColorSetPreviewer.TextureWidth * 4 * sizeof(Half), sizeof(MtrlFile.ColorSet.Row)); + previewer.ScheduleUpdate(); + } + } + + public unsafe void UpdateColorSetPreview() + { + if (ColorSetPreviewers.Count == 0) + return; + + var maybeColorSet = Mtrl.ColorSets.FirstOrNull(colorSet => colorSet.HasRows); + if (!maybeColorSet.HasValue) + return; + + var colorSet = maybeColorSet.Value; + var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index); + + var rows = colorSet.Rows; + if (maybeColorDyeSet.HasValue) + { + var stm = _edit._stainService.StmFile; + var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; + var colorDyeSet = maybeColorDyeSet.Value; + for (var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i) + { + ref var row = ref rows[i]; + var dye = colorDyeSet.Rows[i]; + if (stm.TryGetValue(dye.Template, stainId, out var dyes)) + ApplyDye(ref row, dye, dyes); + } + } + + if (HighlightedColorSetRow >= 0) + ApplyHighlight(ref rows[HighlightedColorSetRow], HighlightTime); + + foreach (var previewer in ColorSetPreviewers) + { + fixed (Half* pDest = previewer.ColorSet) + Buffer.MemoryCopy(&rows, pDest, LiveColorSetPreviewer.TextureLength * sizeof(Half), sizeof(MtrlFile.ColorSet.RowArray)); + previewer.ScheduleUpdate(); + } + } + + private static void ApplyDye(ref MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye, StmFile.DyePack dyes) + { + if (dye.Diffuse) + row.Diffuse = dyes.Diffuse; + if (dye.Specular) + row.Specular = dyes.Specular; + if (dye.SpecularStrength) + row.SpecularStrength = dyes.SpecularPower; + if (dye.Emissive) + row.Emissive = dyes.Emissive; + if (dye.Gloss) + row.GlossStrength = dyes.Gloss; + } + + private static void ApplyHighlight(ref MtrlFile.ColorSet.Row row, int time) + { + var level = Math.Sin(time * Math.PI / 16) * 0.5 + 0.5; + var levelSq = (float)(level * level); + + row.Diffuse = Vector3.Zero; + row.Specular = Vector3.Zero; + row.Emissive = new Vector3(levelSq); + } + public void Update() { UpdateTextureLabels(); @@ -277,11 +504,31 @@ public partial class ModEditWindow UpdateSamplers(); } - public MtrlTab( ModEditWindow edit, MtrlFile file ) + public MtrlTab( ModEditWindow edit, MtrlFile file, string filePath, bool writable ) { - _edit = edit; - Mtrl = file; + _edit = edit; + Mtrl = file; + FilePath = filePath; + Writable = writable; LoadShpk( FindAssociatedShpk( out _, out _ ) ); + if (writable) + BindToMaterialInstances(); + } + + ~MtrlTab() + { + DoDispose(); + } + + public void Dispose() + { + DoDispose(); + GC.SuppressFinalize(this); + } + + private void DoDispose() + { + UnbindFromMaterialInstances(); } public bool Valid diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs index 16ad708c..2d1859bd 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs @@ -37,16 +37,17 @@ public partial class ModEditWindow return ret; } - private static bool DrawShaderFlagsInput(MtrlFile file, bool disabled) + private static bool DrawShaderFlagsInput(MtrlTab tab, bool disabled) { var ret = false; - var shpkFlags = (int)file.ShaderPackage.Flags; + var shpkFlags = (int)tab.Mtrl.ShaderPackage.Flags; ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); if (ImGui.InputInt("Shader Package Flags", ref shpkFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) { - file.ShaderPackage.Flags = (uint)shpkFlags; - ret = true; + tab.Mtrl.ShaderPackage.Flags = (uint)shpkFlags; + ret = true; + tab.SetShaderPackageFlags((uint)shpkFlags); } return ret; @@ -221,6 +222,7 @@ public partial class ModEditWindow { ret = true; tab.UpdateConstantLabels(); + tab.SetMaterialParameter(constant.Id, valueIdx, values.Slice(valueIdx, 1)); } } } @@ -247,6 +249,7 @@ public partial class ModEditWindow ret = true; tab.UpdateConstantLabels(); + tab.SetMaterialParameter(constant.Id, 0, new float[constant.ByteSize >> 2]); } return ret; @@ -336,7 +339,7 @@ public partial class ModEditWindow private static bool DrawMaterialSampler(MtrlTab tab, bool disabled, ref int idx) { - var (label, filename) = tab.Samplers[idx]; + var (label, filename, samplerCrc) = tab.Samplers[idx]; using var tree = ImRaii.TreeNode(label); if (!tree) return false; @@ -366,6 +369,7 @@ public partial class ModEditWindow { tab.Mtrl.ShaderPackage.Samplers[idx].Flags = (uint)samplerFlags; ret = true; + tab.SetSamplerFlags(samplerCrc, (uint)samplerFlags); } if (!disabled @@ -410,9 +414,10 @@ public partial class ModEditWindow if (!ImGui.Button("Add Sampler")) return false; + var newSamplerId = tab.NewSamplerId; tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.AddItem(new Sampler { - SamplerId = tab.NewSamplerId, + SamplerId = newSamplerId, TextureIndex = (byte)tab.Mtrl.Textures.Length, Flags = 0, }); @@ -423,6 +428,7 @@ public partial class ModEditWindow }); tab.UpdateSamplers(); tab.UpdateTextureLabels(); + tab.SetSamplerFlags(newSamplerId, 0); return true; } @@ -467,7 +473,7 @@ public partial class ModEditWindow return ret; ret |= DrawPackageNameInput(tab, disabled); - ret |= DrawShaderFlagsInput(tab.Mtrl, disabled); + ret |= DrawShaderFlagsInput(tab, disabled); DrawCustomAssociations(tab); ret |= DrawMaterialShaderKeys(tab, disabled); DrawMaterialShaders(tab); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index d7e23ac3..e4de66a8 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -15,13 +15,16 @@ public partial class ModEditWindow private bool DrawMaterialPanel( MtrlTab tab, bool disabled ) { + DrawMaterialLivePreviewRebind( tab, disabled ); + + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); var ret = DrawMaterialTextureChange( tab, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawBackFaceAndTransparency( tab.Mtrl, disabled ); + ret |= DrawBackFaceAndTransparency( tab, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawMaterialColorSetChange( tab.Mtrl, disabled ); + ret |= DrawMaterialColorSetChange( tab, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); ret |= DrawMaterialShaderResources( tab, disabled ); @@ -32,6 +35,15 @@ public partial class ModEditWindow return !disabled && ret; } + private static void DrawMaterialLivePreviewRebind( MtrlTab tab, bool disabled ) + { + if (disabled) + return; + + if (ImGui.Button("Reload live-preview")) + tab.BindToMaterialInstances(); + } + private static bool DrawMaterialTextureChange( MtrlTab tab, bool disabled ) { var ret = false; @@ -62,7 +74,7 @@ public partial class ModEditWindow return ret; } - private static bool DrawBackFaceAndTransparency( MtrlFile file, bool disabled ) + private static bool DrawBackFaceAndTransparency( MtrlTab tab, bool disabled ) { const uint transparencyBit = 0x10; const uint backfaceBit = 0x01; @@ -71,19 +83,21 @@ public partial class ModEditWindow using var dis = ImRaii.Disabled( disabled ); - var tmp = ( file.ShaderPackage.Flags & transparencyBit ) != 0; + var tmp = ( tab.Mtrl.ShaderPackage.Flags & transparencyBit ) != 0; if( ImGui.Checkbox( "Enable Transparency", ref tmp ) ) { - file.ShaderPackage.Flags = tmp ? file.ShaderPackage.Flags | transparencyBit : file.ShaderPackage.Flags & ~transparencyBit; - ret = true; + tab.Mtrl.ShaderPackage.Flags = tmp ? tab.Mtrl.ShaderPackage.Flags | transparencyBit : tab.Mtrl.ShaderPackage.Flags & ~transparencyBit; + ret = true; + tab.SetShaderPackageFlags(tab.Mtrl.ShaderPackage.Flags); } ImGui.SameLine( 200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X ); - tmp = ( file.ShaderPackage.Flags & backfaceBit ) != 0; + tmp = ( tab.Mtrl.ShaderPackage.Flags & backfaceBit ) != 0; if( ImGui.Checkbox( "Hide Backfaces", ref tmp ) ) { - file.ShaderPackage.Flags = tmp ? file.ShaderPackage.Flags | backfaceBit : file.ShaderPackage.Flags & ~backfaceBit; - ret = true; + tab.Mtrl.ShaderPackage.Flags = tmp ? tab.Mtrl.ShaderPackage.Flags | backfaceBit : tab.Mtrl.ShaderPackage.Flags & ~backfaceBit; + ret = true; + tab.SetShaderPackageFlags(tab.Mtrl.ShaderPackage.Flags); } return ret; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 59cf8b80..a37363e3 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -137,6 +137,9 @@ public partial class ModEditWindow : Window, IDisposable { _left.Dispose(); _right.Dispose(); + _materialTab.Reset(); + _modelTab.Reset(); + _shaderPackageTab.Reset(); } public override void Draw() @@ -541,12 +544,12 @@ public partial class ModEditWindow : Window, IDisposable _fileDialog = fileDialog; _materialTab = new FileEditor(this, gameData, config, _fileDialog, "Materials", ".mtrl", () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, - bytes => new MtrlTab(this, new MtrlFile(bytes))); + (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); _modelTab = new FileEditor(this, gameData, config, _fileDialog, "Models", ".mdl", - () => _editor.Files.Mdl, DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new MdlFile(bytes)); + () => _editor.Files.Mdl, DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new MdlFile(bytes)); _shaderPackageTab = new FileEditor(this, gameData, config, _fileDialog, "Shaders", ".shpk", () => _editor.Files.Shpk, DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, - bytes => new ShpkTab(_fileDialog, bytes)); + (bytes, _, _) => new ShpkTab(_fileDialog, bytes)); _center = new CombinedTexture(_left, _right); _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor); _quickImportViewer = new ResourceTreeViewer(_config, resourceTreeFactory, 2, OnQuickImportRefresh, DrawQuickImportActions); @@ -557,6 +560,9 @@ public partial class ModEditWindow : Window, IDisposable { _communicator.ModPathChanged.Unsubscribe(OnModPathChanged); _editor?.Dispose(); + _materialTab.Dispose(); + _modelTab.Dispose(); + _shaderPackageTab.Dispose(); _left.Dispose(); _right.Dispose(); _center.Dispose();