From ccca2f14340899c8ce3e4eac6323766f970db90d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 19 Aug 2023 03:16:45 +0200 Subject: [PATCH 01/14] Material editor: improve color accuracy --- .../UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs index cd599f11..c03272ef 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs @@ -412,12 +412,13 @@ private bool DrawDyePreview( MtrlFile file, int colorSetIdx, int rowIdx, bool di private static bool ColorPicker( string label, string tooltip, Vector3 input, Action< Vector3 > setter, string letter = "" ) { var ret = false; - var tmp = input; + var inputSqrt = Vector3.SquareRoot( input ); + var tmp = inputSqrt; if( ImGui.ColorEdit3( label, ref tmp, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.DisplayRGB | ImGuiColorEditFlags.InputRGB | ImGuiColorEditFlags.NoTooltip ) - && tmp != input ) + && tmp != inputSqrt ) { - setter( tmp ); + setter( tmp * tmp ); ret = true; } From f64fdd2b26d53bae3a1c24b256637e71ae53ba40 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 19 Aug 2023 05:42:26 +0200 Subject: [PATCH 02/14] Material editor: live-preview changes --- .../Interop/ResourceTree/ResolveContext.cs | 26 +- Penumbra/Interop/Structs/ConstantBuffer.cs | 31 ++ Penumbra/Interop/Structs/Material.cs | 35 +- Penumbra/Interop/Structs/MtrlResource.cs | 22 +- Penumbra/Interop/Structs/ResourceHandle.cs | 14 +- .../Interop/Structs/ShaderPackageUtility.cs | 19 + Penumbra/Interop/Structs/TextureUtility.cs | 36 ++ Penumbra/UI/AdvancedWindow/FileEditor.cs | 41 +- .../ModEditWindow.Materials.ColorSet.cs | 136 +++-- .../ModEditWindow.Materials.LivePreview.cs | 484 ++++++++++++++++++ .../ModEditWindow.Materials.MtrlTab.cs | 269 +++++++++- .../ModEditWindow.Materials.Shpk.cs | 20 +- .../AdvancedWindow/ModEditWindow.Materials.cs | 32 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 12 +- 14 files changed, 1067 insertions(+), 110 deletions(-) create mode 100644 Penumbra/Interop/Structs/ConstantBuffer.cs create mode 100644 Penumbra/Interop/Structs/ShaderPackageUtility.cs create mode 100644 Penumbra/Interop/Structs/TextureUtility.cs create mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs 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 @@ private ResourceNode CreateNodeFromGamePath(ResourceType type, nint sourceAddres 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 @@ 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; + + [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; - public Texture* Texture( int index ) => ( Texture* )Textures[3 * index + 1]; + [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 byte* ShpkString => 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 @@ 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 FileEditor(ModEditWindow owner, IDataManager gameData, Configuration conf _combo = new Combo(config, getFiles); } + ~FileEditor() + { + DoDispose(); + } + public void Draw() { using var tab = ImRaii.TabItem(_tabName); @@ -60,11 +65,23 @@ public void Draw() 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 @@ private void DefaultInput() 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 void Reset() { _currentException = null; _currentPath = null; + (_currentFile as IDisposable)?.Dispose(); _currentFile = null; _changed = false; } @@ -181,10 +201,13 @@ private void UpdateCurrentFile(FileRegistry path) 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 @@ private bool DrawMaterialColorSetChange( MtrlFile file, bool disabled ) 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 @@ private static void ColorSetCopyAllClipboardButton( MtrlFile file, int colorSetI } } - 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 @@ private static unsafe bool ColorSetPasteAllClipboardButton( MtrlFile file, int c 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 @@ private static unsafe bool ColorSetPasteAllClipboardButton( MtrlFile file, int c } } + tab.UpdateColorSetPreview(); + return true; } catch @@ -182,7 +187,7 @@ private static unsafe void ColorSetCopyClipboardButton( MtrlFile.ColorSet.Row ro } } - 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 @@ private static unsafe bool ColorSetPasteFromClipboardButton( MtrlFile file, int 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 @@ private static unsafe bool ColorSetPasteFromClipboardButton( MtrlFile file, int 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 @@ static bool FixFloat( ref float val, float current ) } 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 @@ static bool FixFloat( ref float val, float current ) { 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 @@ static bool FixFloat( ref float val, float current ) 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 @@ static bool FixFloat( ref float val, float current ) { 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 @@ static bool FixFloat( ref float val, float current ) 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 @@ static bool FixFloat( ref float val, float current ) 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 @@ static bool FixFloat( ref float val, float current ) 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 @@ static bool FixFloat( ref float val, float current ) 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 @@ static bool FixFloat( ref float val, float current ) 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 @@ static bool FixFloat( ref float val, float current ) 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 @@ static bool FixFloat( ref float val, float current ) 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 @@ private bool DrawDyePreview( MtrlFile file, int colorSetIdx, int rowIdx, bool di 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.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 @@ private sealed class MtrlTab : IWritable 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 void UpdateSamplers() ? $"#{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 void UpdateSamplers() } } + 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 void Update() 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 @@ private bool DrawPackageNameInput(MtrlTab tab, bool disabled) 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 @@ private static bool DrawMaterialConstantValues(MtrlTab tab, bool disabled, ref i { ret = true; tab.UpdateConstantLabels(); + tab.SetMaterialParameter(constant.Id, valueIdx, values.Slice(valueIdx, 1)); } } } @@ -247,6 +249,7 @@ private static bool DrawMaterialConstantValues(MtrlTab tab, bool disabled, ref i ret = true; tab.UpdateConstantLabels(); + tab.SetMaterialParameter(constant.Id, 0, new float[constant.ByteSize >> 2]); } return ret; @@ -336,7 +339,7 @@ private static bool DrawMaterialConstants(MtrlTab tab, bool disabled) 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 @@ static unsafe bool InputHexUInt16(string label, ref ushort v, ImGuiInputTextFlag { tab.Mtrl.ShaderPackage.Samplers[idx].Flags = (uint)samplerFlags; ret = true; + tab.SetSamplerFlags(samplerCrc, (uint)samplerFlags); } if (!disabled @@ -410,9 +414,10 @@ private static bool DrawMaterialNewSampler(MtrlTab tab) 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 @@ private static bool DrawMaterialNewSampler(MtrlTab tab) }); tab.UpdateSamplers(); tab.UpdateTextureLabels(); + tab.SetSamplerFlags(newSamplerId, 0); return true; } @@ -467,7 +473,7 @@ private bool DrawMaterialShaderResources(MtrlTab tab, bool disabled) 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 @@ private bool DrawMaterialPanel( MtrlTab tab, bool disabled ) 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 @@ private static bool DrawMaterialTextureChange( MtrlTab tab, bool disabled ) 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 @@ private static bool DrawBackFaceAndTransparency( MtrlFile file, bool disabled ) 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 override void OnClose() { _left.Dispose(); _right.Dispose(); + _materialTab.Reset(); + _modelTab.Reset(); + _shaderPackageTab.Reset(); } public override void Draw() @@ -541,12 +544,12 @@ public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialo _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 void Dispose() { _communicator.ModPathChanged.Unsubscribe(OnModPathChanged); _editor?.Dispose(); + _materialTab.Dispose(); + _modelTab.Dispose(); + _shaderPackageTab.Dispose(); _left.Dispose(); _right.Dispose(); _center.Dispose(); From b8d09ab6602d141b99b37e019da6ed50b54734dc Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 24 Aug 2023 05:51:21 +0200 Subject: [PATCH 03/14] Material editor 2099 --- OtterGui | 2 +- Penumbra.GameData | 2 +- .../Interop/ResourceTree/ResolveContext.cs | 2 +- Penumbra/Interop/Structs/CharacterBaseExt.cs | 15 + Penumbra/Interop/Structs/HumanExt.cs | 3 + .../ModEditWindow.Materials.ColorSet.cs | 101 ++- .../ModEditWindow.Materials.ConstantEditor.cs | 235 +++++++ .../ModEditWindow.Materials.LivePreview.cs | 10 +- .../ModEditWindow.Materials.MtrlTab.cs | 610 +++++++++++------ .../ModEditWindow.Materials.Shpk.cs | 614 ++++++++---------- .../AdvancedWindow/ModEditWindow.Materials.cs | 90 ++- .../ModEditWindow.ShaderPackages.cs | 88 ++- .../AdvancedWindow/ModEditWindow.ShpkTab.cs | 12 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 20 + Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 16 +- 15 files changed, 1195 insertions(+), 625 deletions(-) create mode 100644 Penumbra/Interop/Structs/CharacterBaseExt.cs create mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs diff --git a/OtterGui b/OtterGui index 863d08bd..1e172ee9 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 863d08bd83381bb7fe4a8d5c514f0ba55379336f +Subproject commit 1e172ee9a0f5946d67b848a36b2be97f6541453f diff --git a/Penumbra.GameData b/Penumbra.GameData index 97643cad..07c001c5 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 97643cad67b6981c3ee510d1ca12c4321e6a80bf +Subproject commit 07c001c5b2b35b2dba2b8428389d3ed375728616 diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 6dc005ee..d14bd68b 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -190,7 +190,7 @@ public static unsafe FullPath GetResourceHandlePath(ResourceHandle* handle) if (WithNames) { - var name = samplers != null && i < samplers.Count ? samplers[i].Item2?.Name : null; + var name = samplers != null && i < samplers.Length ? samplers[i].ShpkSampler?.Name : null; node.Children.Add(texNode.WithName(name ?? $"Texture #{i}")); } else diff --git a/Penumbra/Interop/Structs/CharacterBaseExt.cs b/Penumbra/Interop/Structs/CharacterBaseExt.cs new file mode 100644 index 00000000..3bbbeca9 --- /dev/null +++ b/Penumbra/Interop/Structs/CharacterBaseExt.cs @@ -0,0 +1,15 @@ +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; + +namespace Penumbra.Interop.Structs; + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct CharacterBaseExt +{ + [FieldOffset( 0x0 )] + public CharacterBase CharacterBase; + + [FieldOffset( 0x258 )] + public Texture** ColorSetTextures; +} \ No newline at end of file diff --git a/Penumbra/Interop/Structs/HumanExt.cs b/Penumbra/Interop/Structs/HumanExt.cs index 7af5cee4..33d83b06 100644 --- a/Penumbra/Interop/Structs/HumanExt.cs +++ b/Penumbra/Interop/Structs/HumanExt.cs @@ -9,6 +9,9 @@ public unsafe struct HumanExt [FieldOffset( 0x0 )] public Human Human; + [FieldOffset( 0x0 )] + public CharacterBaseExt CharacterBase; + [FieldOffset( 0x9E8 )] public ResourceHandle* Decal; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs index 1d6c480a..798939ca 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs @@ -13,22 +13,44 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { + private static readonly float HalfMinValue = (float)Half.MinValue; + private static readonly float HalfMaxValue = (float)Half.MaxValue; + private static readonly float HalfEpsilon = (float)Half.Epsilon; + private bool DrawMaterialColorSetChange( MtrlTab tab, bool disabled ) { - if( !tab.Mtrl.ColorSets.Any( c => c.HasRows ) ) + if( !tab.SamplerIds.Contains( ShpkFile.TableSamplerId ) || !tab.Mtrl.ColorSets.Any( c => c.HasRows ) ) + { + return false; + } + + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + if( !ImGui.CollapsingHeader( "Color Set", ImGuiTreeNodeFlags.DefaultOpen ) ) { return false; } + var hasAnyDye = tab.UseColorDyeSet; + ColorSetCopyAllClipboardButton( tab.Mtrl, 0 ); ImGui.SameLine(); - var ret = ColorSetPasteAllClipboardButton( tab, 0 ); - ImGui.SameLine(); - ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); - ImGui.SameLine(); - ret |= DrawPreviewDye( tab, disabled ); + var ret = ColorSetPasteAllClipboardButton( tab, 0, disabled ); + if( !disabled ) + { + ImGui.SameLine(); + ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); + ImGui.SameLine(); + ret |= ColorSetDyeableCheckbox( tab, ref hasAnyDye ); + } + if( hasAnyDye ) + { + ImGui.SameLine(); + ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); + ImGui.SameLine(); + ret |= DrawPreviewDye( tab, disabled ); + } - using var table = ImRaii.Table( "##ColorSets", 11, + using var table = ImRaii.Table( "##ColorSets", hasAnyDye ? 11 : 9, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV ); if( !table ) { @@ -53,17 +75,20 @@ private bool DrawMaterialColorSetChange( MtrlTab tab, bool disabled ) ImGui.TableHeader( "Repeat" ); ImGui.TableNextColumn(); ImGui.TableHeader( "Skew" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Dye" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Dye Preview" ); + if( hasAnyDye ) + { + ImGui.TableNextColumn(); + ImGui.TableHeader("Dye"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Dye Preview"); + } 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( tab, j, i, disabled ); + ret |= DrawColorSetRow( tab, j, i, disabled, hasAnyDye ); ImGui.TableNextRow(); } } @@ -122,9 +147,9 @@ private bool DrawPreviewDye( MtrlTab tab, bool disabled ) return false; } - private static unsafe bool ColorSetPasteAllClipboardButton( MtrlTab tab, int colorSetIdx ) + private static unsafe bool ColorSetPasteAllClipboardButton( MtrlTab tab, int colorSetIdx, bool disabled ) { - if( !ImGui.Button( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) || tab.Mtrl.ColorSets.Length <= colorSetIdx ) + if( !ImGuiUtil.DrawDisabledButton( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ), string.Empty, disabled ) || tab.Mtrl.ColorSets.Length <= colorSetIdx ) { return false; } @@ -187,6 +212,21 @@ private static unsafe void ColorSetCopyClipboardButton( MtrlFile.ColorSet.Row ro } } + private static bool ColorSetDyeableCheckbox( MtrlTab tab, ref bool dyeable ) + { + var ret = ImGui.Checkbox( "Dyeable", ref dyeable ); + + if( ret ) + { + tab.UseColorDyeSet = dyeable; + if( dyeable ) + tab.Mtrl.FindOrAddColorDyeSet(); + tab.UpdateColorSetPreview(); + } + + return ret; + } + private static unsafe bool ColorSetPasteFromClipboardButton( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled ) { if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, @@ -235,7 +275,7 @@ private static void ColorSetHighlightButton( MtrlTab tab, int rowIdx, bool disab tab.CancelColorSetHighlight(); } - private bool DrawColorSetRow( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled ) + private bool DrawColorSetRow( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled, bool hasAnyDye ) { static bool FixFloat( ref float val, float current ) { @@ -245,7 +285,7 @@ static bool FixFloat( ref float val, float current ) using var id = ImRaii.PushId( rowIdx ); var row = tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ]; - var hasDye = tab.Mtrl.ColorDyeSets.Length > colorSetIdx; + var hasDye = hasAnyDye && 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; @@ -274,7 +314,7 @@ static bool FixFloat( ref float val, float current ) ImGui.SameLine(); var tmpFloat = row.SpecularStrength; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.SpecularStrength ) ) + if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.SpecularStrength ) ) { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat; ret = true; @@ -305,9 +345,9 @@ static bool FixFloat( ref float val, float current ) ImGui.TableNextColumn(); tmpFloat = row.GlossStrength; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.GlossStrength ) ) + if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, Math.Max( 0.1f, tmpFloat * 0.025f ), HalfEpsilon, HalfMaxValue, "%.1f" ) && FixFloat( ref tmpFloat, row.GlossStrength ) ) { - tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = tmpFloat; + tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = Math.Max(tmpFloat, HalfEpsilon); ret = true; tab.UpdateColorSetRowPreview(rowIdx); } @@ -323,9 +363,9 @@ static bool FixFloat( ref float val, float current ) ImGui.TableNextColumn(); int tmpInt = row.TileSet; ImGui.SetNextItemWidth( intSize ); - if( ImGui.InputInt( "##TileSet", ref tmpInt, 0, 0 ) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue ) + if( ImGui.DragInt( "##TileSet", ref tmpInt, 0.25f, 0, 63 ) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue ) { - tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )tmpInt; + tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )Math.Clamp(tmpInt, 0, 63); ret = true; tab.UpdateColorSetRowPreview(rowIdx); } @@ -335,7 +375,7 @@ static bool FixFloat( ref float val, float current ) ImGui.TableNextColumn(); tmpFloat = row.MaterialRepeat.X; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.X ) ) + if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.MaterialRepeat.X ) ) { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; ret = true; @@ -346,7 +386,7 @@ static bool FixFloat( ref float val, float current ) ImGui.SameLine(); tmpFloat = row.MaterialRepeat.Y; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.Y ) ) + if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.MaterialRepeat.Y ) ) { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; ret = true; @@ -358,7 +398,7 @@ static bool FixFloat( ref float val, float current ) ImGui.TableNextColumn(); tmpFloat = row.MaterialSkew.X; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.X ) ) + if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.MaterialSkew.X ) ) { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat }; ret = true; @@ -370,7 +410,7 @@ static bool FixFloat( ref float val, float current ) ImGui.SameLine(); tmpFloat = row.MaterialSkew.Y; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.Y ) ) + if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.MaterialSkew.Y ) ) { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; ret = true; @@ -379,10 +419,10 @@ static bool FixFloat( ref float val, float current ) ImGuiUtil.HoverTooltip( "Skew Y", ImGuiHoveredFlags.AllowWhenDisabled ); - ImGui.TableNextColumn(); if( hasDye ) { - if(_stainService.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + ImGui.TableNextColumn(); + if (_stainService.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) ) { tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = _stainService.TemplateCombo.CurrentSelection; @@ -395,9 +435,10 @@ static bool FixFloat( ref float val, float current ) ImGui.TableNextColumn(); ret |= DrawDyePreview( tab, colorSetIdx, rowIdx, disabled, dye, floatSize ); } - else + else if ( hasAnyDye ) { ImGui.TableNextColumn(); + ImGui.TableNextColumn(); } @@ -431,10 +472,10 @@ private bool DrawDyePreview( MtrlTab tab, int colorSetIdx, int rowIdx, bool disa ImGui.SameLine(); using var dis = ImRaii.Disabled(); ImGui.SetNextItemWidth( floatSize ); - ImGui.DragFloat( "##gloss", ref values.Gloss, 0, 0, 0, "%.2f G" ); + ImGui.DragFloat( "##gloss", ref values.Gloss, 0, values.Gloss, values.Gloss, "%.1f G" ); ImGui.SameLine(); ImGui.SetNextItemWidth( floatSize ); - ImGui.DragFloat( "##specularStrength", ref values.SpecularPower, 0, 0, 0, "%.2f S" ); + ImGui.DragFloat( "##specularStrength", ref values.SpecularPower, 0, values.SpecularPower, values.SpecularPower, "%.2f S" ); return ret; } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs new file mode 100644 index 00000000..5616425c --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui; +using Penumbra.GameData; + +namespace Penumbra.UI.AdvancedWindow; + +public partial class ModEditWindow +{ + private interface IConstantEditor + { + bool Draw(Span values, bool disabled, float editorWidth); + } + + private sealed class FloatConstantEditor : IConstantEditor + { + public static readonly FloatConstantEditor Default = new(null, null, 0.1f, 0.0f, 1.0f, 0.0f, 3, string.Empty); + + private readonly float? _minimum; + private readonly float? _maximum; + private readonly float _speed; + private readonly float _relativeSpeed; + private readonly float _factor; + private readonly float _bias; + private readonly string _format; + + public FloatConstantEditor(float? minimum, float? maximum, float speed, float relativeSpeed, float factor, float bias, byte precision, string unit) + { + _minimum = minimum; + _maximum = maximum; + _speed = speed; + _relativeSpeed = relativeSpeed; + _factor = factor; + _bias = bias; + _format = $"%.{Math.Min(precision, (byte)9)}f"; + if (unit.Length > 0) + _format = $"{_format} {unit.Replace("%", "%%")}"; + } + + public bool Draw(Span values, bool disabled, float editorWidth) + { + var fieldWidth = (editorWidth - (values.Length - 1) * ImGui.GetStyle().ItemSpacing.X) / values.Length; + + var ret = false; + + for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) + { + if (valueIdx > 0) + ImGui.SameLine(); + + ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx)); + + var value = (values[valueIdx] - _bias) / _factor; + if (disabled) + ImGui.DragFloat($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), value, value, _format); + else + { + if (ImGui.DragFloat($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0.0f, _maximum ?? 0.0f, _format)) + { + values[valueIdx] = Clamp(value) * _factor + _bias; + ret = true; + } + } + } + + return ret; + } + + private float Clamp(float value) + => Math.Clamp(value, _minimum ?? float.NegativeInfinity, _maximum ?? float.PositiveInfinity); + } + + private sealed class IntConstantEditor : IConstantEditor + { + private readonly int? _minimum; + private readonly int? _maximum; + private readonly float _speed; + private readonly float _relativeSpeed; + private readonly float _factor; + private readonly float _bias; + private readonly string _format; + + public IntConstantEditor(int? minimum, int? maximum, float speed, float relativeSpeed, float factor, float bias, string unit) + { + _minimum = minimum; + _maximum = maximum; + _speed = speed; + _relativeSpeed = relativeSpeed; + _factor = factor; + _bias = bias; + _format = "%d"; + if (unit.Length > 0) + _format = $"{_format} {unit.Replace("%", "%%")}"; + } + + public bool Draw(Span values, bool disabled, float editorWidth) + { + var fieldWidth = (editorWidth - (values.Length - 1) * ImGui.GetStyle().ItemSpacing.X) / values.Length; + + var ret = false; + + for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) + { + if (valueIdx > 0) + ImGui.SameLine(); + + ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx)); + + var value = (int)Math.Clamp(MathF.Round((values[valueIdx] - _bias) / _factor), int.MinValue, int.MaxValue); + if (disabled) + ImGui.DragInt($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), value, value, _format); + else + { + if (ImGui.DragInt($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0, _maximum ?? 0, _format)) + { + values[valueIdx] = Clamp(value) * _factor + _bias; + ret = true; + } + } + } + + return ret; + } + + private int Clamp(int value) + => Math.Clamp(value, _minimum ?? int.MinValue, _maximum ?? int.MaxValue); + } + + private sealed class ColorConstantEditor : IConstantEditor + { + private readonly bool _squaredRgb; + private readonly bool _clamped; + + public ColorConstantEditor(bool squaredRgb, bool clamped) + { + _squaredRgb = squaredRgb; + _clamped = clamped; + } + + public bool Draw(Span values, bool disabled, float editorWidth) + { + if (values.Length == 3) + { + ImGui.SetNextItemWidth(editorWidth); + var value = new Vector3(values); + if (_squaredRgb) + value = Vector3.SquareRoot(value); + if (ImGui.ColorEdit3("##0", ref value) && !disabled) + { + if (_squaredRgb) + value *= value; + if (_clamped) + value = Vector3.Clamp(value, Vector3.Zero, Vector3.One); + value.CopyTo(values); + return true; + } + + return false; + } + else if (values.Length == 4) + { + ImGui.SetNextItemWidth(editorWidth); + var value = new Vector4(values); + if (_squaredRgb) + value = new Vector4(MathF.Sqrt(value.X), MathF.Sqrt(value.Y), MathF.Sqrt(value.Z), value.W); + if (ImGui.ColorEdit4("##0", ref value) && !disabled) + { + if (_squaredRgb) + value *= new Vector4(value.X, value.Y, value.Z, 1.0f); + if (_clamped) + value = Vector4.Clamp(value, Vector4.Zero, Vector4.One); + value.CopyTo(values); + return true; + } + + return false; + } + else + return FloatConstantEditor.Default.Draw(values, disabled, editorWidth); + } + } + + private sealed class EnumConstantEditor : IConstantEditor + { + private readonly IReadOnlyList<(string Label, float Value, string Description)> _values; + + public EnumConstantEditor(IReadOnlyList<(string Label, float Value, string Description)> values) + { + _values = values; + } + + public bool Draw(Span values, bool disabled, float editorWidth) + { + var fieldWidth = (editorWidth - (values.Length - 1) * ImGui.GetStyle().ItemSpacing.X) / values.Length; + + var ret = false; + + for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) + { + if (valueIdx > 0) + ImGui.SameLine(); + + ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx)); + + var currentValue = values[valueIdx]; + var (currentLabel, _, currentDescription) = _values.FirstOrNull(v => v.Value == currentValue) ?? (currentValue.ToString(), currentValue, string.Empty); + if (disabled) + ImGui.InputText($"##{valueIdx}", ref currentLabel, (uint)currentLabel.Length, ImGuiInputTextFlags.ReadOnly); + else + { + using var c = ImRaii.Combo($"##{valueIdx}", currentLabel); + { + if (c) + foreach (var (valueLabel, value, valueDescription) in _values) + { + if (ImGui.Selectable(valueLabel, value == currentValue)) + { + values[valueIdx] = value; + ret = true; + } + + if (valueDescription.Length > 0) + ImGuiUtil.SelectableHelpMarker(valueDescription); + } + } + } + } + + return ret; + } + } +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs index 376e656f..7d400a71 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs @@ -398,7 +398,7 @@ public LiveColorSetPreviewer(IObjectTable objects, Framework framework, int subA if (mtrlHandle == null) throw new InvalidOperationException("Material doesn't have a resource handle"); - var colorSetTextures = *(Texture***)((nint)DrawObject + 0x258); + var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures; if (colorSetTextures == null) throw new InvalidOperationException("Draw object doesn't have color set textures"); @@ -424,7 +424,8 @@ protected override void Dispose(bool disposing, bool reset) if (reset) { var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)_originalColorSetTexture); - Structs.TextureUtility.DecRef(oldTexture); + if (oldTexture != null) + Structs.TextureUtility.DecRef(oldTexture); } else Structs.TextureUtility.DecRef(_originalColorSetTexture); @@ -460,7 +461,8 @@ private void OnFrameworkUpdate(Framework _) if (success) { var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)newTexture); - Structs.TextureUtility.DecRef(oldTexture); + if (oldTexture != null) + Structs.TextureUtility.DecRef(oldTexture); } else Structs.TextureUtility.DecRef(newTexture); @@ -471,7 +473,7 @@ protected override bool IsStillValid() if (!base.IsStillValid()) return false; - var colorSetTextures = *(Texture***)((nint)DrawObject + 0x258); + var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures; if (colorSetTextures == null) return false; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 9cff681a..17f34b64 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; +using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; @@ -16,6 +17,7 @@ using Penumbra.GameData.Files; using Penumbra.GameData.Structs; using Penumbra.Services; +using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Util; using static Penumbra.GameData.Files.ShpkFile; @@ -26,49 +28,47 @@ public partial class ModEditWindow { private sealed class MtrlTab : IWritable, IDisposable { + private const int ShpkPrefixLength = 16; + + private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true); + private readonly ModEditWindow _edit; public readonly MtrlFile Mtrl; public readonly string FilePath; public readonly bool Writable; - public uint NewKeyId; - public uint NewKeyDefault; - public uint NewConstantId; - public int NewConstantIdx; - public uint NewSamplerId; - public int NewSamplerIdx; + private string[]? _shpkNames; + public string ShaderHeader = "Shader###Shader"; + public FullPath LoadedShpkPath = FullPath.Empty; + public string LoadedShpkPathName = string.Empty; + public string LoadedShpkDevkitPathName = string.Empty; + public string ShaderComment = string.Empty; + public ShpkFile? AssociatedShpk; + public JObject? AssociatedShpkDevkit; - public ShpkFile? AssociatedShpk; - public readonly List< string > TextureLabels = new(4); - public FullPath LoadedShpkPath = FullPath.Empty; - public string LoadedShpkPathName = string.Empty; - public float TextureLabelWidth; + public readonly string LoadedBaseDevkitPathName = string.Empty; + public readonly JObject? AssociatedBaseDevkit; // Shader Key State - public readonly List< string > ShaderKeyLabels = new(16); - public readonly Dictionary< uint, uint > DefinedShaderKeys = new(16); - public readonly List< int > MissingShaderKeyIndices = new(16); - public readonly List< uint > AvailableKeyValues = new(16); - public string VertexShaders = "Vertex Shaders: ???"; - public string PixelShaders = "Pixel Shaders: ???"; + public readonly List< (string Label, int Index, string Description, bool MonoFont, IReadOnlyList< (string Label, uint Value, string Description) > Values) > ShaderKeys = new(16); + + public readonly HashSet< int > VertexShaders = new(16); + public readonly HashSet< int > PixelShaders = new(16); + public bool ShadersKnown = false; + public string VertexShadersString = "Vertex Shaders: ???"; + public string PixelShadersString = "Pixel Shaders: ???"; + + // Textures & Samplers + public readonly List< (string Label, int TextureIndex, int SamplerIndex, string Description, bool MonoFont) > Textures = new(4); + + public readonly HashSet< int > UnfoldedTextures = new(4); + public readonly HashSet< uint > SamplerIds = new(16); + public float TextureLabelWidth; + public bool UseColorDyeSet; // Material Constants - public readonly List< (string Name, bool ComponentOnly, int ParamValueOffset) > MaterialConstants = new(16); - public readonly List< (string Name, uint Id, ushort ByteSize) > MissingMaterialConstants = new(16); - public readonly HashSet< uint > DefinedMaterialConstants = new(16); - - public string MaterialConstantLabel = "Constants###Constants"; - public IndexSet OrphanedMaterialValues = new(0, false); - public int AliasedMaterialValueCount; - public bool HasMalformedMaterialConstants; - - // Samplers - 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; + public readonly List< (string Header, List< (string Label, int ConstantIndex, Range Slice, string Description, bool MonoFont, IConstantEditor Editor) > Constants) > Constants = new(16); // Live-Previewers public readonly List MaterialPreviewers = new(4); @@ -87,8 +87,24 @@ public FullPath FindAssociatedShpk( out string defaultPath, out Utf8GamePath def return _edit.FindBestMatch( defaultGamePath ); } + public string[] GetShpkNames() + { + if (null != _shpkNames) + return _shpkNames; + + var names = new HashSet(StandardShaderPackages); + names.UnionWith(_edit.FindPathsStartingWith(ShpkPrefix).Select(path => path.ToString()[ShpkPrefixLength..])); + + _shpkNames = names.ToArray(); + Array.Sort(_shpkNames); + + return _shpkNames; + } + public void LoadShpk( FullPath path ) { + ShaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader"; + try { LoadedShpkPath = path; @@ -106,180 +122,314 @@ public void LoadShpk( FullPath path ) Penumbra.Chat.NotificationMessage( $"Could not load {LoadedShpkPath.ToPath()}:\n{e}", "Penumbra Advanced Editing", NotificationType.Error ); } + if( LoadedShpkPath.InternalName.IsEmpty ) + { + AssociatedShpkDevkit = null; + LoadedShpkDevkitPathName = string.Empty; + } + else + AssociatedShpkDevkit = TryLoadShpkDevkit( Path.GetFileNameWithoutExtension( Mtrl.ShaderPackage.Name ), out LoadedShpkDevkitPathName ); + + UpdateShaderKeys(); Update(); } - public void UpdateTextureLabels() + private JObject? TryLoadShpkDevkit(string shpkBaseName, out string devkitPathName) { - var samplers = Mtrl.GetSamplersByTexture( AssociatedShpk ); - TextureLabels.Clear(); - TextureLabelWidth = 50f * UiHelpers.Scale; - using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) + try { - for( var i = 0; i < Mtrl.Textures.Length; ++i ) - { - var (sampler, shpkSampler) = samplers[ i ]; - var name = shpkSampler.HasValue ? shpkSampler.Value.Name : sampler.HasValue ? $"0x{sampler.Value.SamplerId:X8}" : $"#{i}"; - TextureLabels.Add( name ); - TextureLabelWidth = Math.Max( TextureLabelWidth, ImGui.CalcTextSize( name ).X ); - } + if (!Utf8GamePath.FromString("penumbra/shpk_devkit/" + shpkBaseName + ".json", out var devkitPath)) + throw new Exception("Could not assemble ShPk dev-kit path."); + + var devkitFullPath = _edit.FindBestMatch(devkitPath); + if (!devkitFullPath.IsRooted) + throw new Exception("Could not resolve ShPk dev-kit path."); + + devkitPathName = devkitFullPath.FullName; + return JObject.Parse(File.ReadAllText(devkitFullPath.FullName)); + } + catch + { + devkitPathName = string.Empty; + return null; } + } - TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4; + private T? TryGetShpkDevkitData(string category, uint? id, bool mayVary) where T : class + { + return TryGetShpkDevkitData(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary) + ?? TryGetShpkDevkitData(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary); } - public void UpdateShaderKeyLabels() + private T? TryGetShpkDevkitData(JObject? devkit, string devkitPathName, string category, uint? id, bool mayVary) where T : class { - ShaderKeyLabels.Clear(); - DefinedShaderKeys.Clear(); - foreach( var (key, idx) in Mtrl.ShaderPackage.ShaderKeys.WithIndex() ) + if (devkit == null) + return null; + + try { - ShaderKeyLabels.Add( $"#{idx}: 0x{key.Category:X8} = 0x{key.Value:X8}###{idx}: 0x{key.Category:X8}" ); - DefinedShaderKeys.Add( key.Category, key.Value ); - } + var data = devkit[category]; + if (id.HasValue) + data = data?[id.Value.ToString()]; + + if (mayVary && (data as JObject)?["Vary"] != null) + { + var selector = BuildSelector(data!["Vary"]! + .Select(key => (uint)key) + .Select(key => Mtrl.GetShaderKey(key)?.Value ?? AssociatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue)); + var index = (int)data["Selectors"]![selector.ToString()]!; + data = data["Items"]![index]; + } - MissingShaderKeyIndices.Clear(); - AvailableKeyValues.Clear(); - var vertexShaders = new IndexSet( AssociatedShpk?.VertexShaders.Length ?? 0, false ); - var pixelShaders = new IndexSet( AssociatedShpk?.PixelShaders.Length ?? 0, false ); - if( AssociatedShpk != null ) + return data?.ToObject(typeof(T)) as T; + } + catch (Exception e) { - MissingShaderKeyIndices.AddRange( AssociatedShpk.MaterialKeys.WithIndex().Where( k => !DefinedShaderKeys.ContainsKey( k.Value.Id ) ).WithoutValue() ); + // Some element in the JSON was undefined or invalid (wrong type, key that doesn't exist in the ShPk, index out of range, …) + Penumbra.Log.Error($"Error while traversing the ShPk dev-kit file at {devkitPathName}: {e}"); + return null; + } + } - if( MissingShaderKeyIndices.Count > 0 && MissingShaderKeyIndices.All( i => AssociatedShpk.MaterialKeys[ i ].Id != NewKeyId ) ) + public void UpdateShaderKeys() + { + ShaderKeys.Clear(); + if (AssociatedShpk != null) + { + foreach (var key in AssociatedShpk.MaterialKeys) { - var key = AssociatedShpk.MaterialKeys[ MissingShaderKeyIndices[ 0 ] ]; - NewKeyId = key.Id; - NewKeyDefault = key.DefaultValue; + var dkData = TryGetShpkDevkitData("ShaderKeys", key.Id, false); + var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); + + var valueSet = new HashSet(key.Values); + if (dkData != null) + valueSet.UnionWith(dkData.Values.Keys); + + var mtrlKeyIndex = Mtrl.FindOrAddShaderKey(key.Id, key.DefaultValue); + var values = valueSet.Select(value => + { + if (dkData != null && dkData.Values.TryGetValue(value, out var dkValue)) + return (dkValue.Label.Length > 0 ? dkValue.Label : $"0x{value:X8}", value, dkValue.Description); + else + return ($"0x{value:X8}", value, string.Empty); + }).ToArray(); + Array.Sort(values, (x, y) => + { + if (x.Value == key.DefaultValue) + return -1; + if (y.Value == key.DefaultValue) + return 1; + return x.Label.CompareTo(y.Label); + }); + ShaderKeys.Add((hasDkLabel ? dkData!.Label : $"0x{key.Id:X8}", mtrlKeyIndex, dkData?.Description ?? string.Empty, !hasDkLabel, values)); } + } + else + { + foreach (var (key, index) in Mtrl.ShaderPackage.ShaderKeys.WithIndex()) + ShaderKeys.Add(($"0x{key.Category:X8}", index, string.Empty, true, Array.Empty<(string, uint, string)>())); + } + } - AvailableKeyValues.AddRange( AssociatedShpk.MaterialKeys.Select( k => DefinedShaderKeys.TryGetValue( k.Id, out var value ) ? value : k.DefaultValue ) ); - foreach( var node in AssociatedShpk.Nodes ) + public void UpdateShaders() + { + VertexShaders.Clear(); + PixelShaders.Clear(); + if (AssociatedShpk == null) + ShadersKnown = false; + else + { + ShadersKnown = true; + var systemKeySelectors = AllSelectors(AssociatedShpk.SystemKeys).ToArray(); + var sceneKeySelectors = AllSelectors(AssociatedShpk.SceneKeys).ToArray(); + var subViewKeySelectors = AllSelectors(AssociatedShpk.SubViewKeys).ToArray(); + var materialKeySelector = BuildSelector(AssociatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value)); + foreach (var systemKeySelector in systemKeySelectors) { - if( node.MaterialKeys.WithIndex().All( key => key.Value == AvailableKeyValues[ key.Index ] ) ) + foreach (var sceneKeySelector in sceneKeySelectors) { - foreach( var pass in node.Passes ) + foreach (var subViewKeySelector in subViewKeySelectors) { - vertexShaders.Add( ( int )pass.VertexShader ); - pixelShaders.Add( ( int )pass.PixelShader ); + var selector = BuildSelector(systemKeySelector, sceneKeySelector, materialKeySelector, subViewKeySelector); + var node = AssociatedShpk.GetNodeBySelector(selector); + if (node.HasValue) + { + foreach (var pass in node.Value.Passes) + { + VertexShaders.Add((int)pass.VertexShader); + PixelShaders.Add((int)pass.PixelShader); + } + } + else + ShadersKnown = false; } } } } - VertexShaders = $"Vertex Shaders: {( vertexShaders.Count > 0 ? string.Join( ", ", vertexShaders.Select( i => $"#{i}" ) ) : "???" )}"; - PixelShaders = $"Pixel Shaders: {( pixelShaders.Count > 0 ? string.Join( ", ", pixelShaders.Select( i => $"#{i}" ) ) : "???" )}"; + var vertexShaders = VertexShaders.OrderBy(i => i).Select(i => $"#{i}"); + var pixelShaders = PixelShaders.OrderBy(i => i).Select(i => $"#{i}"); + + VertexShadersString = $"Vertex Shaders: {string.Join(", ", ShadersKnown ? vertexShaders : vertexShaders.Append("???"))}"; + PixelShadersString = $"Pixel Shaders: {string.Join(", ", ShadersKnown ? pixelShaders : pixelShaders.Append("???"))}"; + + ShaderComment = TryGetShpkDevkitData("Comment", null, true) ?? string.Empty; } - public void UpdateConstantLabels() + public void UpdateTextures() { - var prefix = AssociatedShpk?.GetConstantById( MaterialParamsConstantId )?.Name ?? string.Empty; - MaterialConstantLabel = prefix.Length == 0 ? "Constants###Constants" : prefix + "###Constants"; + Textures.Clear(); + SamplerIds.Clear(); + if (AssociatedShpk == null) + { + SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); + if (Mtrl.ColorSets.Any(c => c.HasRows)) + SamplerIds.Add(TableSamplerId); - DefinedMaterialConstants.Clear(); - MaterialConstants.Clear(); - HasMalformedMaterialConstants = false; - AliasedMaterialValueCount = 0; - OrphanedMaterialValues = new IndexSet( Mtrl.ShaderPackage.ShaderValues.Length, true ); - foreach( var (constant, idx) in Mtrl.ShaderPackage.Constants.WithIndex() ) + foreach (var (sampler, index) in Mtrl.ShaderPackage.Samplers.WithIndex()) + Textures.Add(($"0x{sampler.SamplerId:X8}", sampler.TextureIndex, index, string.Empty, true)); + } + else { - DefinedMaterialConstants.Add( constant.Id ); - var values = Mtrl.GetConstantValues( constant ); - var paramValueOffset = -values.Length; - if( values.Length > 0 ) + foreach (var index in VertexShaders) + SamplerIds.UnionWith(AssociatedShpk.VertexShaders[index].Samplers.Select(sampler => sampler.Id)); + foreach (var index in PixelShaders) + SamplerIds.UnionWith(AssociatedShpk.PixelShaders[index].Samplers.Select(sampler => sampler.Id)); + if (!ShadersKnown) { - var shpkParam = AssociatedShpk?.GetMaterialParamById( constant.Id ); - var paramByteOffset = shpkParam?.ByteOffset ?? -1; - if( ( paramByteOffset & 0x3 ) == 0 ) - { - paramValueOffset = paramByteOffset >> 2; - } - - var unique = OrphanedMaterialValues.RemoveRange( constant.ByteOffset >> 2, values.Length ); - AliasedMaterialValueCount += values.Length - unique; + SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); + if (Mtrl.ColorSets.Any(c => c.HasRows)) + SamplerIds.Add(TableSamplerId); } - else + foreach (var samplerId in SamplerIds) { - HasMalformedMaterialConstants = true; - } + var shpkSampler = AssociatedShpk.GetSamplerById(samplerId); + if (!shpkSampler.HasValue || shpkSampler.Value.Slot != 2) + continue; - var (name, componentOnly) = MaterialParamRangeName( prefix, paramValueOffset, values.Length ); - var label = name == null - ? $"#{idx:D2} (ID: 0x{constant.Id:X8})###{constant.Id}" - : $"#{idx:D2}: {name} (ID: 0x{constant.Id:X8})###{constant.Id}"; + var dkData = TryGetShpkDevkitData("Samplers", samplerId, true); + var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); - MaterialConstants.Add( ( label, componentOnly, paramValueOffset ) ); + var sampler = Mtrl.GetOrAddSampler(samplerId, dkData?.DefaultTexture ?? string.Empty, out var samplerIndex); + Textures.Add((hasDkLabel ? dkData!.Label : shpkSampler.Value.Name, sampler.TextureIndex, samplerIndex, dkData?.Description ?? string.Empty, !hasDkLabel)); + } + if (SamplerIds.Contains(TableSamplerId)) + Mtrl.FindOrAddColorSet(); } + Textures.Sort((x, y) => string.CompareOrdinal(x.Label, y.Label)); - MissingMaterialConstants.Clear(); - if( AssociatedShpk != null ) - { - var setIdx = false; - foreach( var param in AssociatedShpk.MaterialParams.Where( m => !DefinedMaterialConstants.Contains( m.Id ) ) ) - { - var (name, _) = MaterialParamRangeName( prefix, param.ByteOffset >> 2, param.ByteSize >> 2 ); - var label = name == null - ? $"(ID: 0x{param.Id:X8})" - : $"{name} (ID: 0x{param.Id:X8})"; - if( NewConstantId == param.Id ) - { - setIdx = true; - NewConstantIdx = MissingMaterialConstants.Count; - } + TextureLabelWidth = 50f * UiHelpers.Scale; - MissingMaterialConstants.Add( ( label, param.Id, param.ByteSize ) ); - } + float helpWidth; + using (var _ = ImRaii.PushFont(UiBuilder.IconFont)) + helpWidth = ImGui.GetStyle().ItemSpacing.X + ImGui.CalcTextSize(FontAwesomeIcon.InfoCircle.ToIconString()).X; - if( !setIdx && MissingMaterialConstants.Count > 0 ) - { - NewConstantIdx = 0; - NewConstantId = MissingMaterialConstants[ 0 ].Id; - } + foreach (var (label, _, _, description, monoFont) in Textures) + if (!monoFont) + TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); + + using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) + { + foreach (var (label, _, _, description, monoFont) in Textures) + if (monoFont) + TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); } + + TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4; } - public void UpdateSamplers() + public void UpdateConstants() { - Samplers.Clear(); - DefinedSamplers.Clear(); - OrphanedSamplers = new IndexSet( Mtrl.Textures.Length, true ); - foreach( var (sampler, idx) in Mtrl.ShaderPackage.Samplers.WithIndex() ) + static List FindOrAddGroup(List<(string, List)> groups, string name) { - DefinedSamplers.Add( sampler.SamplerId ); - if( !OrphanedSamplers.Remove( sampler.TextureIndex ) ) - { - ++AliasedSamplerCount; - } + foreach (var (groupName, group) in groups) + if (string.Equals(name, groupName, StringComparison.Ordinal)) + return group; - var shpk = AssociatedShpk?.GetSamplerById( sampler.SamplerId ); - var label = shpk.HasValue - ? $"#{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, sampler.SamplerId ) ); + var newGroup = new List(16); + groups.Add((name, newGroup)); + return newGroup; } - MissingSamplers.Clear(); - if( AssociatedShpk != null ) + Constants.Clear(); + if (AssociatedShpk == null) { - var setSampler = false; - foreach( var sampler in AssociatedShpk.Samplers.Where( s => s.Slot == 2 && !DefinedSamplers.Contains( s.Id ) ) ) + var fcGroup = FindOrAddGroup(Constants, "Further Constants"); + foreach (var (constant, index) in Mtrl.ShaderPackage.Constants.WithIndex()) { - if( sampler.Id == NewSamplerId ) + var values = Mtrl.GetConstantValues(constant); + for (var i = 0; i < values.Length; i += 4) + fcGroup.Add(($"0x{constant.Id:X8}", index, i..Math.Min(i + 4, values.Length), string.Empty, true, FloatConstantEditor.Default)); + } + } + else + { + var prefix = AssociatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? string.Empty; + foreach (var shpkConstant in AssociatedShpk.MaterialParams) + { + if ((shpkConstant.ByteSize & 0x3) != 0) + continue; + + var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, shpkConstant.ByteSize >> 2, out var constantIndex); + var values = Mtrl.GetConstantValues(constant); + var handledElements = new IndexSet(values.Length, false); + + var dkData = TryGetShpkDevkitData("Constants", shpkConstant.Id, true); + if (dkData != null) { - setSampler = true; - NewSamplerIdx = MissingSamplers.Count; + foreach (var dkConstant in dkData) + { + var offset = (int)dkConstant.Offset; + var length = values.Length - offset; + if (dkConstant.Length.HasValue) + length = Math.Min(length, (int)dkConstant.Length.Value); + if (length <= 0) + continue; + var editor = dkConstant.CreateEditor(); + if (editor != null) + FindOrAddGroup(Constants, dkConstant.Group.Length > 0 ? dkConstant.Group : "Further Constants") + .Add((dkConstant.Label, constantIndex, offset..(offset + length), dkConstant.Description, false, editor)); + handledElements.AddRange(offset, length); + } } - MissingSamplers.Add( ( sampler.Name, sampler.Id ) ); - } - - if( !setSampler && MissingSamplers.Count > 0 ) - { - NewSamplerIdx = 0; - NewSamplerId = MissingSamplers[ 0 ].Id; + var fcGroup = FindOrAddGroup(Constants, "Further Constants"); + foreach (var (start, end) in handledElements.Ranges(true)) + { + if ((shpkConstant.ByteOffset & 0x3) == 0) + { + var offset = shpkConstant.ByteOffset >> 2; + for (int i = (start & ~0x3) - (offset & 0x3), j = offset >> 2; i < end; i += 4, ++j) + { + var rangeStart = Math.Max(i, start); + var rangeEnd = Math.Min(i + 4, end); + if (rangeEnd > rangeStart) + fcGroup.Add(($"{prefix}[{j:D2}]{VectorSwizzle((offset + rangeStart) & 0x3, (offset + rangeEnd - 1) & 0x3)} (0x{shpkConstant.Id:X8})", constantIndex, rangeStart..rangeEnd, string.Empty, true, FloatConstantEditor.Default)); + } + } + else + { + for (var i = start; i < end; i += 4) + fcGroup.Add(($"0x{shpkConstant.Id:X8}", constantIndex, i..Math.Min(i + 4, end), string.Empty, true, FloatConstantEditor.Default)); + } + } } } + + Constants.RemoveAll(group => group.Constants.Count == 0); + Constants.Sort((x, y) => + { + if (string.Equals(x.Header, "Further Constants", StringComparison.Ordinal)) + return 1; + if (string.Equals(y.Header, "Further Constants", StringComparison.Ordinal)) + return -1; + return string.Compare(x.Header, y.Header, StringComparison.Ordinal); + }); + // HACK the Replace makes w appear after xyz, for the cbuffer-location-based naming scheme + foreach (var (_, group) in Constants) + group.Sort((x, y) => string.CompareOrdinal( + x.MonoFont ? x.Label.Replace("].w", "].{") : x.Label, + y.MonoFont ? y.Label.Replace("].w", "].{") : y.Label)); } public unsafe void BindToMaterialInstances() @@ -329,6 +479,7 @@ public unsafe void BindToMaterialInstances() // Carry on without that previewer. } } + UpdateMaterialPreview(); var colorSet = Mtrl.ColorSets.FirstOrNull(colorSet => colorSet.HasRows); @@ -378,6 +529,19 @@ public void SetSamplerFlags(uint samplerCrc, uint samplerFlags) previewer.SetSamplerFlags(samplerCrc, samplerFlags); } + public void UpdateMaterialPreview() + { + SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); + foreach (var constant in Mtrl.ShaderPackage.Constants) + { + var values = Mtrl.GetConstantValues(constant); + if (values != null) + SetMaterialParameter(constant.Id, 0, values); + } + foreach (var sampler in Mtrl.ShaderPackage.Samplers) + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + public void HighlightColorSetRow(int rowIdx) { var oldRowIdx = HighlightedColorSetRow; @@ -402,7 +566,7 @@ public void CancelColorSetHighlight() UpdateColorSetRowPreview(rowIdx); } - public unsafe void UpdateColorSetRowPreview(int rowIdx) + public void UpdateColorSetRowPreview(int rowIdx) { if (ColorSetPreviewers.Count == 0) return; @@ -415,12 +579,12 @@ public unsafe void UpdateColorSetRowPreview(int rowIdx) var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index); var row = colorSet.Rows[rowIdx]; - if (maybeColorDyeSet.HasValue) + if (maybeColorDyeSet.HasValue && UseColorDyeSet) { 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); + row.ApplyDyeTemplate(dye, dyes); } if (HighlightedColorSetRow == rowIdx) @@ -428,13 +592,12 @@ public unsafe void UpdateColorSetRowPreview(int rowIdx) 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)); + row.AsHalves().CopyTo(previewer.ColorSet.AsSpan().Slice(LiveColorSetPreviewer.TextureWidth * 4 * rowIdx, LiveColorSetPreviewer.TextureWidth * 4)); previewer.ScheduleUpdate(); } } - public unsafe void UpdateColorSetPreview() + public void UpdateColorSetPreview() { if (ColorSetPreviewers.Count == 0) return; @@ -447,7 +610,7 @@ public unsafe void UpdateColorSetPreview() var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index); var rows = colorSet.Rows; - if (maybeColorDyeSet.HasValue) + if (maybeColorDyeSet.HasValue && UseColorDyeSet) { var stm = _edit._stainService.StmFile; var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; @@ -457,7 +620,7 @@ public unsafe void UpdateColorSetPreview() 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); + row.ApplyDyeTemplate(dye, dyes); } } @@ -466,26 +629,11 @@ public unsafe void UpdateColorSetPreview() foreach (var previewer in ColorSetPreviewers) { - fixed (Half* pDest = previewer.ColorSet) - Buffer.MemoryCopy(&rows, pDest, LiveColorSetPreviewer.TextureLength * sizeof(Half), sizeof(MtrlFile.ColorSet.RowArray)); + rows.AsHalves().CopyTo(previewer.ColorSet); 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; @@ -498,18 +646,19 @@ private static void ApplyHighlight(ref MtrlFile.ColorSet.Row row, int time) public void Update() { - UpdateTextureLabels(); - UpdateShaderKeyLabels(); - UpdateConstantLabels(); - UpdateSamplers(); + UpdateShaders(); + UpdateTextures(); + UpdateConstants(); } public MtrlTab( ModEditWindow edit, MtrlFile file, string filePath, bool writable ) { - _edit = edit; - Mtrl = file; - FilePath = filePath; - Writable = writable; + _edit = edit; + Mtrl = file; + FilePath = filePath; + Writable = writable; + UseColorDyeSet = file.ColorDyeSets.Length > 0; + AssociatedBaseDevkit = TryLoadShpkDevkit( "_base", out LoadedBaseDevkitPathName ); LoadShpk( FindAssociatedShpk( out _, out _ ) ); if (writable) BindToMaterialInstances(); @@ -532,9 +681,96 @@ private void DoDispose() } public bool Valid - => Mtrl.Valid; + => ShadersKnown && Mtrl.Valid; public byte[] Write() - => Mtrl.Write(); + { + var output = Mtrl.Clone(); + output.GarbageCollect(AssociatedShpk, SamplerIds, UseColorDyeSet); + + return output.Write(); + } + + private sealed class DevkitShaderKeyValue + { + public string Label = string.Empty; + public string Description = string.Empty; + } + + private sealed class DevkitShaderKey + { + public string Label = string.Empty; + public string Description = string.Empty; + public Dictionary Values = new(); + } + + private sealed class DevkitSampler + { + public string Label = string.Empty; + public string Description = string.Empty; + public string DefaultTexture = string.Empty; + } + + private enum DevkitConstantType + { + Hidden = -1, + Float = 0, + Integer = 1, + Color = 2, + Enum = 3, + } + + private sealed class DevkitConstantValue + { + public string Label = string.Empty; + public string Description = string.Empty; + public float Value = 0.0f; + } + + private sealed class DevkitConstant + { + public uint Offset = 0; + public uint? Length = null; + public string Group = string.Empty; + public string Label = string.Empty; + public string Description = string.Empty; + public DevkitConstantType Type = DevkitConstantType.Float; + + public float? Minimum = null; + public float? Maximum = null; + public float? Speed = null; + public float RelativeSpeed = 0.0f; + public float Factor = 1.0f; + public float Bias = 0.0f; + public byte Precision = 3; + public string Unit = string.Empty; + + public bool SquaredRgb = false; + public bool Clamped = false; + + public DevkitConstantValue[] Values = Array.Empty(); + + public IConstantEditor? CreateEditor() + { + switch (Type) + { + case DevkitConstantType.Hidden: + return null; + case DevkitConstantType.Float: + return new FloatConstantEditor(Minimum, Maximum, Speed ?? 0.1f, RelativeSpeed, Factor, Bias, Precision, Unit); + case DevkitConstantType.Integer: + return new IntConstantEditor(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed, Factor, Bias, Unit); + case DevkitConstantType.Color: + return new ColorConstantEditor(SquaredRgb, Clamped); + case DevkitConstantType.Enum: + return new EnumConstantEditor(Array.ConvertAll(Values, value => (value.Label, value.Value, value.Description))); + default: + return FloatConstantEditor.Default; + } + } + + private int? ToInteger(float? value) + => value.HasValue ? (int)Math.Clamp(MathF.Round(value.Value), int.MinValue, int.MaxValue) : null; + } } -} \ No newline at end of file +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs index 2d1859bd..d3bc826a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; @@ -7,6 +8,7 @@ using Dalamud.Interface.ImGuiFileDialog; using ImGuiNET; using Lumina.Data.Parsing; +using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Raii; using Penumbra.GameData; @@ -19,20 +21,92 @@ public partial class ModEditWindow { private readonly FileDialogService _fileDialog; - private bool DrawPackageNameInput(MtrlTab tab, bool disabled) + // strings path/to/the.exe | grep --fixed-strings '.shpk' | sort -u | sed -e 's#^shader/sm5/shpk/##' + // Apricot shader packages are unlisted because + // 1. they cause performance/memory issues when calculating the effective shader set + // 2. they probably aren't intended for use with materials anyway + private static readonly IReadOnlyList StandardShaderPackages = new string[] { - var ret = false; - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); - if (ImGui.InputText("Shader Package Name", ref tab.Mtrl.ShaderPackage.Name, 63, - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) + "3dui.shpk", + // "apricot_decal_dummy.shpk", + // "apricot_decal_ring.shpk", + // "apricot_decal.shpk", + // "apricot_lightmodel.shpk", + // "apricot_model_dummy.shpk", + // "apricot_model_morph.shpk", + // "apricot_model.shpk", + // "apricot_powder_dummy.shpk", + // "apricot_powder.shpk", + // "apricot_shape_dummy.shpk", + // "apricot_shape.shpk", + "bgcolorchange.shpk", + "bgcrestchange.shpk", + "bgdecal.shpk", + "bg.shpk", + "bguvscroll.shpk", + "channeling.shpk", + "characterglass.shpk", + "character.shpk", + "cloud.shpk", + "createviewposition.shpk", + "crystal.shpk", + "directionallighting.shpk", + "directionalshadow.shpk", + "grass.shpk", + "hair.shpk", + "iris.shpk", + "lightshaft.shpk", + "linelighting.shpk", + "planelighting.shpk", + "pointlighting.shpk", + "river.shpk", + "shadowmask.shpk", + "skin.shpk", + "spotlighting.shpk", + "verticalfog.shpk", + "water.shpk", + "weather.shpk", + }; + + private enum TextureAddressMode : uint + { + Wrap = 0, + Mirror = 1, + Clamp = 2, + Border = 3, + } + + private static readonly IReadOnlyList TextureAddressModeTooltips = new string[] + { + "Tile the texture at every UV integer junction.\n\nFor example, for U values between 0 and 3, the texture is repeated three times.", + "Flip the texture at every UV integer junction.\n\nFor U values between 0 and 1, for example, the texture is addressed normally; between 1 and 2, the texture is mirrored; between 2 and 3, the texture is normal again; and so on.", + "Texture coordinates outside the range [0.0, 1.0] are set to the texture color at 0.0 or 1.0, respectively.", + "Texture coordinates outside the range [0.0, 1.0] are set to the border color (generally black).", + }; + + private static bool DrawPackageNameInput(MtrlTab tab, bool disabled) + { + if (disabled) { - ret = true; - tab.AssociatedShpk = null; - tab.LoadedShpkPath = FullPath.Empty; + ImGui.TextUnformatted("Shader Package: " + tab.Mtrl.ShaderPackage.Name); + return false; } - if (ImGui.IsItemDeactivatedAfterEdit()) - tab.LoadShpk(tab.FindAssociatedShpk(out _, out _)); + var ret = false; + ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); + using var c = ImRaii.Combo("Shader Package", tab.Mtrl.ShaderPackage.Name); + if (c) + foreach (var value in tab.GetShpkNames()) + { + if (ImGui.Selectable(value, value == tab.Mtrl.ShaderPackage.Name)) + { + tab.Mtrl.ShaderPackage.Name = value; + ret = true; + tab.AssociatedShpk = null; + tab.LoadedShpkPath = FullPath.Empty; + tab.LoadShpk(tab.FindAssociatedShpk(out _, out _)); + } + } return ret; } @@ -41,8 +115,8 @@ private static bool DrawShaderFlagsInput(MtrlTab tab, bool disabled) { var ret = false; var shpkFlags = (int)tab.Mtrl.ShaderPackage.Flags; - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); - if (ImGui.InputInt("Shader Package Flags", ref shpkFlags, 0, 0, + ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); + if (ImGui.InputInt("Shader Flags", ref shpkFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) { tab.Mtrl.ShaderPackage.Flags = (uint)shpkFlags; @@ -62,6 +136,12 @@ private void DrawCustomAssociations(MtrlTab tab) var text = tab.AssociatedShpk == null ? "Associated .shpk file: None" : $"Associated .shpk file: {tab.LoadedShpkPathName}"; + var devkitText = tab.AssociatedShpkDevkit == null + ? "Associated dev-kit file: None" + : $"Associated dev-kit file: {tab.LoadedShpkDevkitPathName}"; + var baseDevkitText = tab.AssociatedBaseDevkit == null + ? "Base dev-kit file: None" + : $"Base dev-kit file: {tab.LoadedBaseDevkitPathName}"; ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); @@ -70,6 +150,16 @@ private void DrawCustomAssociations(MtrlTab tab) ImGuiUtil.HoverTooltip("Click to copy file path to clipboard."); + if (ImGui.Selectable(devkitText)) + ImGui.SetClipboardText(tab.LoadedShpkDevkitPathName); + + ImGuiUtil.HoverTooltip("Click to copy file path to clipboard."); + + if (ImGui.Selectable(baseDevkitText)) + ImGui.SetClipboardText(tab.LoadedBaseDevkitPathName); + + ImGuiUtil.HoverTooltip("Click to copy file path to clipboard."); + if (ImGui.Button("Associate Custom .shpk File")) _fileDialog.OpenFilePicker("Associate Custom .shpk File...", ".shpk", (success, name) => { @@ -94,394 +184,247 @@ private void DrawCustomAssociations(MtrlTab tab) ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); } - - private static bool DrawShaderKey(MtrlTab tab, bool disabled, ref int idx) + private static bool DrawMaterialShaderKeys(MtrlTab tab, bool disabled) { - var ret = false; - using var t2 = ImRaii.TreeNode(tab.ShaderKeyLabels[idx], disabled ? ImGuiTreeNodeFlags.Leaf : 0); - if (!t2 || disabled) - return ret; + if (tab.ShaderKeys.Count == 0) + return false; - var key = tab.Mtrl.ShaderPackage.ShaderKeys[idx]; - var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById(key.Category); - if (shpkKey.HasValue) + var ret = false; + foreach (var (label, index, description, monoFont, values) in tab.ShaderKeys) { - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); - using var c = ImRaii.Combo("Value", $"0x{key.Value:X8}"); - if (c) - foreach (var value in shpkKey.Value.Values) + using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); + ref var key = ref tab.Mtrl.ShaderPackage.ShaderKeys[index]; + var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById(key.Category); + var currentValue = key.Value; + var (currentLabel, _, currentDescription) = values.FirstOrNull(v => v.Value == currentValue) ?? ($"0x{currentValue:X8}", currentValue, string.Empty); + if (!disabled && shpkKey.HasValue) + { + ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); + using (var c = ImRaii.Combo($"##{key.Category:X8}", currentLabel)) { - if (ImGui.Selectable($"0x{value:X8}", value == key.Value)) - { - tab.Mtrl.ShaderPackage.ShaderKeys[idx].Value = value; - ret = true; - tab.UpdateShaderKeyLabels(); - } + if (c) + foreach (var (valueLabel, value, valueDescription) in values) + { + if (ImGui.Selectable(valueLabel, value == currentValue)) + { + key.Value = value; + ret = true; + tab.Update(); + } + + if (valueDescription.Length > 0) + ImGuiUtil.SelectableHelpMarker(valueDescription); + } } - } - - if (ImGui.Button("Remove Key")) - { - tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.RemoveItems(idx--); - ret = true; - tab.UpdateShaderKeyLabels(); + ImGui.SameLine(); + if (description.Length > 0) + ImGuiUtil.LabeledHelpMarker(label, description); + else + ImGui.TextUnformatted(label); + } + else if (description.Length > 0 || currentDescription.Length > 0) + ImGuiUtil.LabeledHelpMarker($"{label}: {currentLabel}", + description + ((description.Length > 0 && currentDescription.Length > 0) ? "\n\n" : string.Empty) + currentDescription); + else + ImGui.TextUnformatted($"{label}: {currentLabel}"); } return ret; } - private static bool DrawNewShaderKey(MtrlTab tab) + private static void DrawMaterialShaders(MtrlTab tab) { - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); - var ret = false; - using (var c = ImRaii.Combo("##NewConstantId", $"ID: 0x{tab.NewKeyId:X8}")) - { - if (c) - foreach (var idx in tab.MissingShaderKeyIndices) - { - var key = tab.AssociatedShpk!.MaterialKeys[idx]; + if (tab.AssociatedShpk == null) + return; - if (ImGui.Selectable($"ID: 0x{key.Id:X8}", key.Id == tab.NewKeyId)) - { - tab.NewKeyDefault = key.DefaultValue; - tab.NewKeyId = key.Id; - ret = true; - tab.UpdateShaderKeyLabels(); - } - } - } + ImRaii.TreeNode(tab.VertexShadersString, ImGuiTreeNodeFlags.Leaf).Dispose(); + ImRaii.TreeNode(tab.PixelShadersString, ImGuiTreeNodeFlags.Leaf).Dispose(); - ImGui.SameLine(); - if (ImGui.Button("Add Key")) + if (tab.ShaderComment.Length > 0) { - tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.AddItem(new ShaderKey - { - Category = tab.NewKeyId, - Value = tab.NewKeyDefault, - }); - ret = true; - tab.UpdateShaderKeyLabels(); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ImGui.TextUnformatted(tab.ShaderComment); } - - return ret; } - private static bool DrawMaterialShaderKeys(MtrlTab tab, bool disabled) + private static bool DrawMaterialConstants(MtrlTab tab, bool disabled) { - if (tab.Mtrl.ShaderPackage.ShaderKeys.Length <= 0 - && (disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialKeys.Length <= 0)) + if (tab.Constants.Count == 0) return false; - using var t = ImRaii.TreeNode("Shader Keys"); - if (!t) + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + if (!ImGui.CollapsingHeader("Material Constants")) return false; + using var _ = ImRaii.PushId("MaterialConstants"); + var ret = false; - for (var idx = 0; idx < tab.Mtrl.ShaderPackage.ShaderKeys.Length; ++idx) - ret |= DrawShaderKey(tab, disabled, ref idx); + foreach (var (header, group) in tab.Constants) + { + using var t = ImRaii.TreeNode(header, ImGuiTreeNodeFlags.DefaultOpen); + if (!t) + continue; - if (!disabled && tab.AssociatedShpk != null && tab.MissingShaderKeyIndices.Count != 0) - ret |= DrawNewShaderKey(tab); + foreach (var (label, constantIndex, slice, description, monoFont, editor) in group) + { + var constant = tab.Mtrl.ShaderPackage.Constants[constantIndex]; + var buffer = tab.Mtrl.GetConstantValues(constant); + if (buffer.Length > 0) + { + using var id = ImRaii.PushId($"##{constant.Id:X8}:{slice.Start}"); + if (editor.Draw(buffer[slice], disabled, 250.0f)) + { + ret = true; + tab.SetMaterialParameter(constant.Id, slice.Start, buffer[slice]); + } + ImGui.SameLine(); + using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); + if (description.Length > 0) + ImGuiUtil.LabeledHelpMarker(label, description); + else + ImGui.TextUnformatted(label); + } + } + } return ret; } - private static void DrawMaterialShaders(MtrlTab tab) + private static bool DrawMaterialSampler(MtrlTab tab, bool disabled, int textureIdx, int samplerIdx) { - if (tab.AssociatedShpk == null) - return; - - ImRaii.TreeNode(tab.VertexShaders, ImGuiTreeNodeFlags.Leaf).Dispose(); - ImRaii.TreeNode(tab.PixelShaders, ImGuiTreeNodeFlags.Leaf).Dispose(); - } - + var ret = false; + ref var texture = ref tab.Mtrl.Textures[textureIdx]; + ref var sampler = ref tab.Mtrl.ShaderPackage.Samplers[samplerIdx]; - private static bool DrawMaterialConstantValues(MtrlTab tab, bool disabled, ref int idx) - { - var (name, componentOnly, paramValueOffset) = tab.MaterialConstants[idx]; - using var font = ImRaii.PushFont(UiBuilder.MonoFont); - using var t2 = ImRaii.TreeNode(name); - if (!t2) - return false; - - font.Dispose(); + // FIXME this probably doesn't belong here + static unsafe bool InputHexUInt16(string label, ref ushort v, ImGuiInputTextFlags flags) + { + fixed (ushort* v2 = &v) + { + return ImGui.InputScalar(label, ImGuiDataType.U16, (nint)v2, IntPtr.Zero, IntPtr.Zero, "%04X", flags); + } + } - var constant = tab.Mtrl.ShaderPackage.Constants[idx]; - var ret = false; - var values = tab.Mtrl.GetConstantValues(constant); - if (values.Length > 0) + static bool ComboTextureAddressMode(string label, ref uint samplerFlags, int bitOffset) { - var valueOffset = constant.ByteOffset >> 2; + var current = (TextureAddressMode)((samplerFlags >> bitOffset) & 0x3u); + using var c = ImRaii.Combo(label, current.ToString()); + if (!c) + return false; - for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) + var ret = false; + foreach (var value in Enum.GetValues()) { - var paramName = MaterialParamName(componentOnly, paramValueOffset + valueIdx) ?? $"#{valueIdx}"; - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); - if (ImGui.InputFloat($"{paramName} (at 0x{(valueOffset + valueIdx) << 2:X4})", ref values[valueIdx], 0.0f, 0.0f, "%.3f", - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) + if (ImGui.Selectable(value.ToString(), value == current)) { - ret = true; - tab.UpdateConstantLabels(); - tab.SetMaterialParameter(constant.Id, valueIdx, values.Slice(valueIdx, 1)); + samplerFlags = (samplerFlags & ~(0x3u << bitOffset)) | ((uint)value << bitOffset); + ret = true; } + + ImGuiUtil.SelectableHelpMarker(TextureAddressModeTooltips[(int)value]); } + return ret; } - else + + var dx11 = texture.DX11; + if (ImGui.Checkbox("Prepend -- to the file name on DirectX 11", ref dx11)) { - ImRaii.TreeNode($"Offset: 0x{constant.ByteOffset:X4}", ImGuiTreeNodeFlags.Leaf).Dispose(); - ImRaii.TreeNode($"Size: 0x{constant.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf).Dispose(); + texture.DX11 = dx11; + ret = true; } - if (!disabled - && !tab.HasMalformedMaterialConstants - && tab.OrphanedMaterialValues.Count == 0 - && tab.AliasedMaterialValueCount == 0 - && ImGui.Button("Remove Constant")) + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ComboTextureAddressMode("##UAddressMode", ref sampler.Flags, 2)) { - tab.Mtrl.ShaderPackage.ShaderValues = - tab.Mtrl.ShaderPackage.ShaderValues.RemoveItems(constant.ByteOffset >> 2, constant.ByteSize >> 2); - tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.RemoveItems(idx--); - for (var i = 0; i < tab.Mtrl.ShaderPackage.Constants.Length; ++i) - { - if (tab.Mtrl.ShaderPackage.Constants[i].ByteOffset >= constant.ByteOffset) - tab.Mtrl.ShaderPackage.Constants[i].ByteOffset -= constant.ByteSize; - } - ret = true; - tab.UpdateConstantLabels(); - tab.SetMaterialParameter(constant.Id, 0, new float[constant.ByteSize >> 2]); + tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); } + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("U Address Mode", "Method to use for resolving a U texture coordinate that is outside the 0 to 1 range."); - return ret; - } - - private static bool DrawMaterialOrphans(MtrlTab tab, bool disabled) - { - using var t2 = ImRaii.TreeNode($"Orphan Values ({tab.OrphanedMaterialValues.Count})"); - if (!t2) - return false; - - var ret = false; - foreach (var idx in tab.OrphanedMaterialValues) + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ComboTextureAddressMode("##VAddressMode", ref sampler.Flags, 0)) { - ImGui.SetNextItemWidth(ImGui.GetFontSize() * 10.0f); - if (ImGui.InputFloat($"#{idx} (at 0x{idx << 2:X4})", - ref tab.Mtrl.ShaderPackage.ShaderValues[idx], 0.0f, 0.0f, "%.3f", - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) - { - ret = true; - tab.UpdateConstantLabels(); - } + ret = true; + tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); } + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("V Address Mode", "Method to use for resolving a V texture coordinate that is outside the 0 to 1 range."); - return ret; - } - - private static bool DrawNewMaterialParam(MtrlTab tab) - { - ImGui.SetNextItemWidth(UiHelpers.Scale * 450.0f); - using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) + var lodBias = ((int)(sampler.Flags << 12) >> 22) / 64.0f; + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ImGui.DragFloat("##LoDBias", ref lodBias, 0.1f, -8.0f, 7.984375f)) { - using var c = ImRaii.Combo("##NewConstantId", tab.MissingMaterialConstants[tab.NewConstantIdx].Name); - if (c) - foreach (var (constant, idx) in tab.MissingMaterialConstants.WithIndex()) - { - if (ImGui.Selectable(constant.Name, constant.Id == tab.NewConstantId)) - { - tab.NewConstantIdx = idx; - tab.NewConstantId = constant.Id; - } - } + sampler.Flags = (uint)((sampler.Flags & ~0x000FFC00) | (uint)((int)Math.Round(Math.Clamp(lodBias, -8.0f, 7.984375f) * 64.0f) & 0x3FF) << 10); + ret = true; + tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); } - ImGui.SameLine(); - if (ImGui.Button("Add Constant")) + ImGuiUtil.LabeledHelpMarker("Level of Detail Bias", "Offset from the calculated mipmap level.\n\nHigher means that the texture will start to lose detail nearer.\nLower means that the texture will keep its detail until farther."); + + var minLod = (int)((sampler.Flags >> 20) & 0xF); + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ImGui.DragInt("##MinLoD", ref minLod, 0.1f, 0, 15)) { - var (_, _, byteSize) = tab.MissingMaterialConstants[tab.NewConstantIdx]; - tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.AddItem(new MtrlFile.Constant - { - Id = tab.NewConstantId, - ByteOffset = (ushort)(tab.Mtrl.ShaderPackage.ShaderValues.Length << 2), - ByteSize = byteSize, - }); - tab.Mtrl.ShaderPackage.ShaderValues = tab.Mtrl.ShaderPackage.ShaderValues.AddItem(0.0f, byteSize >> 2); - tab.UpdateConstantLabels(); - return true; + sampler.Flags = (uint)((sampler.Flags & ~0x00F00000) | ((uint)Math.Clamp(minLod, 0, 15) << 20)); + ret = true; + tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); } + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("Minimum Level of Detail", "Most detailed mipmap level to use.\n\n0 is the full-sized texture, 1 is the half-sized texture, 2 is the quarter-sized texture, and so on.\n15 will forcibly reduce the texture to its smallest mipmap."); - return false; - } - - private static bool DrawMaterialConstants(MtrlTab tab, bool disabled) - { - if (tab.Mtrl.ShaderPackage.Constants.Length == 0 - && tab.Mtrl.ShaderPackage.ShaderValues.Length == 0 - && (disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialParams.Length == 0)) - return false; - - using var font = ImRaii.PushFont(UiBuilder.MonoFont); - using var t = ImRaii.TreeNode(tab.MaterialConstantLabel); + using var t = ImRaii.TreeNode("Advanced Settings"); if (!t) - return false; - - font.Dispose(); - var ret = false; - for (var idx = 0; idx < tab.Mtrl.ShaderPackage.Constants.Length; ++idx) - ret |= DrawMaterialConstantValues(tab, disabled, ref idx); - - if (tab.OrphanedMaterialValues.Count > 0) - ret |= DrawMaterialOrphans(tab, disabled); - else if (!disabled && !tab.HasMalformedMaterialConstants && tab.MissingMaterialConstants.Count > 0) - ret |= DrawNewMaterialParam(tab); - - return ret; - } - - private static bool DrawMaterialSampler(MtrlTab tab, bool disabled, ref int idx) - { - var (label, filename, samplerCrc) = tab.Samplers[idx]; - using var tree = ImRaii.TreeNode(label); - if (!tree) - return false; - - ImRaii.TreeNode(filename, ImGuiTreeNodeFlags.Leaf).Dispose(); - var ret = false; - var sampler = tab.Mtrl.ShaderPackage.Samplers[idx]; - - // FIXME this probably doesn't belong here - static unsafe bool InputHexUInt16(string label, ref ushort v, ImGuiInputTextFlags flags) - { - fixed (ushort* v2 = &v) - { - return ImGui.InputScalar(label, ImGuiDataType.U16, (nint)v2, IntPtr.Zero, IntPtr.Zero, "%04X", flags); - } - } + return ret; - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); - if (InputHexUInt16("Texture Flags", ref tab.Mtrl.Textures[sampler.TextureIndex].Flags, + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (InputHexUInt16("Texture Flags", ref texture.Flags, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) ret = true; var samplerFlags = (int)sampler.Flags; - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); if (ImGui.InputInt("Sampler Flags", ref samplerFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) { - tab.Mtrl.ShaderPackage.Samplers[idx].Flags = (uint)samplerFlags; - ret = true; - tab.SetSamplerFlags(samplerCrc, (uint)samplerFlags); - } - - if (!disabled - && tab.OrphanedSamplers.Count == 0 - && tab.AliasedSamplerCount == 0 - && ImGui.Button("Remove Sampler")) - { - tab.Mtrl.Textures = tab.Mtrl.Textures.RemoveItems(sampler.TextureIndex); - tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.RemoveItems(idx--); - for (var i = 0; i < tab.Mtrl.ShaderPackage.Samplers.Length; ++i) - { - if (tab.Mtrl.ShaderPackage.Samplers[i].TextureIndex >= sampler.TextureIndex) - --tab.Mtrl.ShaderPackage.Samplers[i].TextureIndex; - } - - ret = true; - tab.UpdateSamplers(); - tab.UpdateTextureLabels(); + sampler.Flags = (uint)samplerFlags; + ret = true; + tab.SetSamplerFlags(sampler.SamplerId, (uint)samplerFlags); } return ret; } - private static bool DrawMaterialNewSampler(MtrlTab tab) + private bool DrawMaterialShader(MtrlTab tab, bool disabled) { - var (name, id) = tab.MissingSamplers[tab.NewSamplerIdx]; - ImGui.SetNextItemWidth(UiHelpers.Scale * 450.0f); - using (var c = ImRaii.Combo("##NewSamplerId", $"{name} (ID: 0x{id:X8})")) + var ret = false; + if (ImGui.CollapsingHeader(tab.ShaderHeader)) { - if (c) - foreach (var (sampler, idx) in tab.MissingSamplers.WithIndex()) - { - if (ImGui.Selectable($"{sampler.Name} (ID: 0x{sampler.Id:X8})", sampler.Id == tab.NewSamplerId)) - { - tab.NewSamplerIdx = idx; - tab.NewSamplerId = sampler.Id; - } - } + ret |= DrawPackageNameInput(tab, disabled); + ret |= DrawShaderFlagsInput(tab, disabled); + DrawCustomAssociations(tab); + ret |= DrawMaterialShaderKeys(tab, disabled); + DrawMaterialShaders(tab); } - ImGui.SameLine(); - if (!ImGui.Button("Add Sampler")) - return false; - - var newSamplerId = tab.NewSamplerId; - tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.AddItem(new Sampler - { - SamplerId = newSamplerId, - TextureIndex = (byte)tab.Mtrl.Textures.Length, - Flags = 0, - }); - tab.Mtrl.Textures = tab.Mtrl.Textures.AddItem(new MtrlFile.Texture + if (tab.AssociatedShpkDevkit == null) { - Path = string.Empty, - Flags = 0, - }); - tab.UpdateSamplers(); - tab.UpdateTextureLabels(); - tab.SetSamplerFlags(newSamplerId, 0); - return true; - } + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + GC.KeepAlive(tab); - private static bool DrawMaterialSamplers(MtrlTab tab, bool disabled) - { - if (tab.Mtrl.ShaderPackage.Samplers.Length == 0 - && tab.Mtrl.Textures.Length == 0 - && (disabled || (tab.AssociatedShpk?.Samplers.All(sampler => sampler.Slot != 2) ?? false))) - return false; - - using var t = ImRaii.TreeNode("Samplers"); - if (!t) - return false; + var textColor = ImGui.GetColorU32(ImGuiCol.Text); + var textColorWarning = (textColor & 0xFF000000u) | ((textColor & 0x00FEFEFE) >> 1) | (tab.AssociatedShpk == null ? 0x80u : 0x8080u); // Half red or yellow - var ret = false; - for (var idx = 0; idx < tab.Mtrl.ShaderPackage.Samplers.Length; ++idx) - ret |= DrawMaterialSampler(tab, disabled, ref idx); + using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning); - if (tab.OrphanedSamplers.Count > 0) - { - using var t2 = ImRaii.TreeNode($"Orphan Textures ({tab.OrphanedSamplers.Count})"); - if (t2) - foreach (var idx in tab.OrphanedSamplers) - { - ImRaii.TreeNode($"#{idx}: {Path.GetFileName(tab.Mtrl.Textures[idx].Path)} - {tab.Mtrl.Textures[idx].Flags:X4}", - ImGuiTreeNodeFlags.Leaf) - .Dispose(); - } - } - else if (!disabled && tab.MissingSamplers.Count > 0 && tab.AliasedSamplerCount == 0 && tab.Mtrl.Textures.Length < 255) - { - ret |= DrawMaterialNewSampler(tab); + ImGui.TextUnformatted(tab.AssociatedShpk == null + ? "Unable to find a suitable .shpk file for cross-references. Some functionality will be missing." + : "No dev-kit file found for this material's shaders. Please install one for optimal editing experience, such as actual constant names instead of hexadecimal identifiers."); } return ret; } - private bool DrawMaterialShaderResources(MtrlTab tab, bool disabled) - { - var ret = false; - if (!ImGui.CollapsingHeader("Advanced Shader Resources")) - return ret; - - ret |= DrawPackageNameInput(tab, disabled); - ret |= DrawShaderFlagsInput(tab, disabled); - DrawCustomAssociations(tab); - ret |= DrawMaterialShaderKeys(tab, disabled); - DrawMaterialShaders(tab); - ret |= DrawMaterialConstants(tab, disabled); - ret |= DrawMaterialSamplers(tab, disabled); - return ret; - } - private static string? MaterialParamName(bool componentOnly, int offset) { if (offset < 0) @@ -500,26 +443,25 @@ private bool DrawMaterialShaderResources(MtrlTab tab, bool disabled) _ => null, }; } + private static string VectorSwizzle(int firstComponent, int lastComponent) + => (firstComponent, lastComponent) switch + { + (0, 4) => " ", + (0, 0) => ".x ", + (0, 1) => ".xy ", + (0, 2) => ".xyz ", + (0, 3) => " ", + (1, 1) => ".y ", + (1, 2) => ".yz ", + (1, 3) => ".yzw ", + (2, 2) => ".z ", + (2, 3) => ".zw ", + (3, 3) => ".w ", + _ => string.Empty, + }; private static (string? Name, bool ComponentOnly) MaterialParamRangeName(string prefix, int valueOffset, int valueLength) { - static string VectorSwizzle(int firstComponent, int lastComponent) - => (firstComponent, lastComponent) switch - { - (0, 4) => " ", - (0, 0) => ".x ", - (0, 1) => ".xy ", - (0, 2) => ".xyz ", - (0, 3) => " ", - (1, 1) => ".y ", - (1, 2) => ".yz ", - (1, 3) => ".yzw ", - (2, 2) => ".z ", - (2, 3) => ".zw ", - (3, 3) => ".w ", - _ => string.Empty, - }; - if (valueLength == 0 || valueOffset < 0) return (null, false); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index e4de66a8..b89bab01 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -18,16 +18,14 @@ private bool DrawMaterialPanel( MtrlTab tab, bool disabled ) DrawMaterialLivePreviewRebind( tab, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - var ret = DrawMaterialTextureChange( tab, disabled ); + var ret = DrawBackFaceAndTransparency( tab, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawBackFaceAndTransparency( tab, disabled ); + ret |= DrawMaterialShader( tab, disabled ); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + ret |= DrawMaterialTextureChange( tab, disabled ); ret |= DrawMaterialColorSetChange( tab, disabled ); - - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawMaterialShaderResources( tab, disabled ); + ret |= DrawMaterialConstants( tab, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); DrawOtherMaterialDetails( tab.Mtrl, disabled ); @@ -40,35 +38,87 @@ private static void DrawMaterialLivePreviewRebind( MtrlTab tab, bool disabled ) if (disabled) return; - if (ImGui.Button("Reload live-preview")) + if (ImGui.Button("Reload live preview")) tab.BindToMaterialInstances(); + + if (tab.MaterialPreviewers.Count == 0 && tab.ColorSetPreviewers.Count == 0) + { + ImGui.SameLine(); + + var textColor = ImGui.GetColorU32(ImGuiCol.Text); + var textColorWarning = (textColor & 0xFF000000u) | ((textColor & 0x00FEFEFE) >> 1) | 0x80u; // Half red + + using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning); + + ImGui.TextUnformatted("The current material has not been found on your character. Please check the Import from Screen tab for more information."); + } } private static bool DrawMaterialTextureChange( MtrlTab tab, bool disabled ) { - var ret = false; - using var table = ImRaii.Table( "##Textures", 2 ); - ImGui.TableSetupColumn( "Path", ImGuiTableColumnFlags.WidthStretch ); - ImGui.TableSetupColumn( "Name", ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * UiHelpers.Scale ); - for( var i = 0; i < tab.Mtrl.Textures.Length; ++i ) + if( tab.Textures.Count == 0 ) { - using var _ = ImRaii.PushId( i ); - var tmp = tab.Mtrl.Textures[ i ].Path; + return false; + } + + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + if( !ImGui.CollapsingHeader( "Textures and Samplers", ImGuiTreeNodeFlags.DefaultOpen ) ) + { + return false; + } + + var frameHeight = ImGui.GetFrameHeight(); + var ret = false; + using var table = ImRaii.Table( "##Textures", 3 ); + + ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, frameHeight ); + ImGui.TableSetupColumn( "Path" , ImGuiTableColumnFlags.WidthStretch ); + ImGui.TableSetupColumn( "Name" , ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * UiHelpers.Scale ); + for( var i = 0; i < tab.Textures.Count; ++i ) + { + var (label, textureI, samplerI, description, monoFont) = tab.Textures[i]; + + using var _ = ImRaii.PushId( samplerI ); + var tmp = tab.Mtrl.Textures[ textureI ].Path; + var unfolded = tab.UnfoldedTextures.Contains( samplerI ); + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( ( unfolded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight ).ToIconString(), new Vector2( frameHeight ), + "Settings for this texture and the associated sampler", false, true ) ) + { + unfolded = !unfolded; + if( unfolded ) + tab.UnfoldedTextures.Add( samplerI ); + else + tab.UnfoldedTextures.Remove( samplerI ); + } ImGui.TableNextColumn(); ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) && tmp.Length > 0 - && tmp != tab.Mtrl.Textures[ i ].Path ) + && tmp != tab.Mtrl.Textures[ textureI ].Path ) { - ret = true; - tab.Mtrl.Textures[ i ].Path = tmp; + ret = true; + tab.Mtrl.Textures[ textureI ].Path = tmp; } ImGui.TableNextColumn(); - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( tab.TextureLabels[ i ] ); + using( var font = ImRaii.PushFont( UiBuilder.MonoFont, monoFont ) ) + { + ImGui.AlignTextToFramePadding(); + if( description.Length > 0 ) + ImGuiUtil.LabeledHelpMarker( label, description ); + else + ImGui.TextUnformatted( label ); + } + + if( unfolded ) + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ret |= DrawMaterialSampler( tab, disabled, textureI, samplerI ); + ImGui.TableNextColumn(); + } } return ret; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index 4e8a4f45..1b159efc 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -14,7 +14,6 @@ using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.String; -using Penumbra.UI.AdvancedWindow; using static Penumbra.GameData.Files.ShpkFile; namespace Penumbra.UI.AdvancedWindow; @@ -40,7 +39,13 @@ private static bool DrawShaderPackagePanel( ShpkTab file, bool disabled ) ret |= DrawShaderPackageMaterialParamLayout( file, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawOtherShaderPackageDetails( file, disabled ); + ret |= DrawShaderPackageResources( file, disabled ); + + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + DrawShaderPackageSelection( file ); + + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + DrawOtherShaderPackageDetails( file ); file.FileDialog.Draw(); @@ -50,7 +55,18 @@ private static bool DrawShaderPackagePanel( ShpkTab file, bool disabled ) } private static void DrawShaderPackageSummary( ShpkTab tab ) - => ImGui.TextUnformatted( tab.Header ); + { + ImGui.TextUnformatted( tab.Header ); + if( !tab.Shpk.Disassembled ) + { + var textColor = ImGui.GetColorU32( ImGuiCol.Text ); + var textColorWarning = ( textColor & 0xFF000000u ) | ( ( textColor & 0x00FEFEFE ) >> 1 ) | 0x80u; // Half red + + using var c = ImRaii.PushColor( ImGuiCol.Text, textColorWarning ); + + ImGui.TextUnformatted( "Your system doesn't support disassembling shaders. Some functionality will be missing." ); + } + } private static void DrawShaderExportButton( ShpkTab tab, string objectName, Shader shader, int idx ) { @@ -163,7 +179,7 @@ private static bool DrawShaderPackageShaderArray( ShpkTab tab, string objectName } DrawShaderExportButton( tab, objectName, shader, idx ); - if( !disabled ) + if( !disabled && tab.Shpk.Disassembled ) { ImGui.SameLine(); DrawShaderImportButton( tab, objectName, shaders, idx ); @@ -182,7 +198,8 @@ private static bool DrawShaderPackageShaderArray( ShpkTab tab, string objectName } } - DrawRawDisassembly( shader ); + if( tab.Shpk.Disassembled ) + DrawRawDisassembly( shader ); } return ret; @@ -276,7 +293,9 @@ private static bool DrawMaterialParamLayoutBufferSize( ShpkFile file, Resource? private static bool DrawShaderPackageMaterialMatrix( ShpkTab tab, bool disabled ) { - ImGui.TextUnformatted( "Parameter positions (continuations are grayed out, unused values are red):" ); + ImGui.TextUnformatted( tab.Shpk.Disassembled + ? "Parameter positions (continuations are grayed out, unused values are red):" + : "Parameter positions (continuations are grayed out):" ); using var table = ImRaii.Table( "##MaterialParamLayout", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); @@ -471,6 +490,22 @@ private static bool DrawShaderPackageMaterialParamLayout( ShpkTab tab, bool disa return ret; } + private static bool DrawShaderPackageResources( ShpkTab tab, bool disabled ) + { + var ret = false; + + if( !ImGui.CollapsingHeader( "Shader Resources" ) ) + { + return false; + } + + ret |= DrawShaderPackageResourceArray( "Constant Buffers", "type", true, tab.Shpk.Constants, disabled ); + ret |= DrawShaderPackageResourceArray( "Samplers", "type", false, tab.Shpk.Samplers, disabled ); + ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "type", false, tab.Shpk.Uavs, disabled ); + + return ret; + } + private static void DrawKeyArray( string arrayName, bool withId, IReadOnlyCollection< Key > keys ) { if( keys.Count == 0 ) @@ -513,7 +548,7 @@ private static void DrawShaderPackageNodes( ShpkTab tab ) foreach( var (node, idx) in tab.Shpk.Nodes.WithIndex() ) { using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - using var t2 = ImRaii.TreeNode( $"#{idx:D4}: ID: 0x{node.Id:X8}" ); + using var t2 = ImRaii.TreeNode( $"#{idx:D4}: Selector: 0x{node.Selector:X8}" ); if( !t2 ) { continue; @@ -549,39 +584,38 @@ private static void DrawShaderPackageNodes( ShpkTab tab ) } } - private static bool DrawOtherShaderPackageDetails( ShpkTab tab, bool disabled ) + private static void DrawShaderPackageSelection( ShpkTab tab ) { - var ret = false; - - if( !ImGui.CollapsingHeader( "Further Content" ) ) + if( !ImGui.CollapsingHeader( "Shader Selection" ) ) { - return false; + return; } - ImRaii.TreeNode( $"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - - ret |= DrawShaderPackageResourceArray( "Constant Buffers", "type", true, tab.Shpk.Constants, disabled ); - ret |= DrawShaderPackageResourceArray( "Samplers", "type", false, tab.Shpk.Samplers, disabled ); - ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "type", false, tab.Shpk.Uavs, disabled ); - DrawKeyArray( "System Keys", true, tab.Shpk.SystemKeys ); DrawKeyArray( "Scene Keys", true, tab.Shpk.SceneKeys ); DrawKeyArray( "Material Keys", true, tab.Shpk.MaterialKeys ); DrawKeyArray( "Sub-View Keys", false, tab.Shpk.SubViewKeys ); DrawShaderPackageNodes( tab ); - if( tab.Shpk.Items.Length > 0 ) + using var t = ImRaii.TreeNode( $"Node Selectors ({tab.Shpk.NodeSelectors.Count})###NodeSelectors" ); + if( t ) { - using var t = ImRaii.TreeNode( $"Items ({tab.Shpk.Items.Length})###Items" ); - if( t ) + using var font = ImRaii.PushFont( UiBuilder.MonoFont ); + foreach( var selector in tab.Shpk.NodeSelectors ) { - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - foreach( var (item, idx) in tab.Shpk.Items.WithIndex() ) - { - ImRaii.TreeNode( $"#{idx:D4}: ID: 0x{item.Id:X8}, node: {item.Node}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + ImRaii.TreeNode( $"#{selector.Value:D4}: Selector: 0x{selector.Key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); } } + } + + private static void DrawOtherShaderPackageDetails( ShpkTab tab ) + { + if( !ImGui.CollapsingHeader( "Further Content" ) ) + { + return; + } + + ImRaii.TreeNode( $"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); if( tab.Shpk.AdditionalData.Length > 0 ) { @@ -591,8 +625,6 @@ private static bool DrawOtherShaderPackageDetails( ShpkTab tab, bool disabled ) ImGuiUtil.TextWrapped( string.Join( ' ', tab.Shpk.AdditionalData.Select( c => $"{c:X2}" ) ) ); } } - - return ret; } private static string UsedComponentString( bool withSize, in Resource resource ) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs index 1720ec8c..2df52130 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs @@ -27,8 +27,16 @@ private class ShpkTab : IWritable public ShpkTab(FileDialogService fileDialog, byte[] bytes) { FileDialog = fileDialog; - Shpk = new ShpkFile(bytes, true); - Header = $"Shader Package for DirectX {(int)Shpk.DirectXVersion}"; + try + { + Shpk = new ShpkFile(bytes, true); + } + catch (NotImplementedException) + { + Shpk = new ShpkFile(bytes, false); + } + + Header = $"Shader Package for DirectX {(int)Shpk.DirectXVersion}"; Extension = Shpk.DirectXVersion switch { ShpkFile.DxVersion.DirectX9 => ".cso", diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index a37363e3..f1c78bf7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; @@ -21,6 +23,7 @@ using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; +using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI.Classes; using Penumbra.Util; @@ -523,6 +526,23 @@ private FullPath FindBestMatch(Utf8GamePath path) return new FullPath(path); } + private HashSet FindPathsStartingWith(ByteString prefix) + { + var ret = new HashSet(); + + foreach (var path in _activeCollections.Current.ResolvedFiles.Keys) + if (path.Path.StartsWith(prefix)) + ret.Add(path); + + if (_mod != null) + foreach (var option in _mod.Groups.SelectMany(g => g).Append(_mod.Default)) + foreach (var path in option.Files.Keys) + if (path.Path.StartsWith(prefix)) + ret.Add(path); + + return ret; + } + public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, StainService stainService, ActiveCollections activeCollections, DalamudServices dalamud, ModMergeTab modMergeTab, diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 0378d620..ad0f2e40 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -195,21 +195,7 @@ private void DrawSingleGroupCombo(IModGroup group, int groupIdx) _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, (uint)idx2); if (option.Description.Length > 0) - { - var hovered = ImGui.IsItemHovered(); - ImGui.SameLine(); - using (var _ = ImRaii.PushFont(UiBuilder.IconFont)) - { - using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled)); - ImGuiUtil.RightAlign(FontAwesomeIcon.InfoCircle.ToIconString(), ImGui.GetStyle().ItemSpacing.X); - } - - if (hovered) - { - using var tt = ImRaii.Tooltip(); - ImGui.TextUnformatted(option.Description); - } - } + ImGuiUtil.SelectableHelpMarker(option.Description); id.Pop(); } From 9364ecccd22673bd54b3c63574992bdb81dba88b Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 25 Aug 2023 06:17:54 +0200 Subject: [PATCH 04/14] Material editor: better color constants --- OtterGui | 2 +- .../AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/OtterGui b/OtterGui index 1e172ee9..c8394607 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 1e172ee9a0f5946d67b848a36b2be97f6541453f +Subproject commit c8394607addd29cb7f8ae3257f635a4486c40a63 diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs index 5616425c..f7ea317d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs @@ -148,7 +148,7 @@ public bool Draw(Span values, bool disabled, float editorWidth) var value = new Vector3(values); if (_squaredRgb) value = Vector3.SquareRoot(value); - if (ImGui.ColorEdit3("##0", ref value) && !disabled) + if (ImGui.ColorEdit3("##0", ref value, ImGuiColorEditFlags.Float | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) && !disabled) { if (_squaredRgb) value *= value; @@ -166,7 +166,7 @@ public bool Draw(Span values, bool disabled, float editorWidth) var value = new Vector4(values); if (_squaredRgb) value = new Vector4(MathF.Sqrt(value.X), MathF.Sqrt(value.Y), MathF.Sqrt(value.Z), value.W); - if (ImGui.ColorEdit4("##0", ref value) && !disabled) + if (ImGui.ColorEdit4("##0", ref value, ImGuiColorEditFlags.Float | ImGuiColorEditFlags.AlphaPreviewHalf | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) && !disabled) { if (_squaredRgb) value *= new Vector4(value.X, value.Y, value.Z, 1.0f); From afd7aab37dcb3a2b0e8b4991367c26ad38890ce3 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 25 Aug 2023 17:46:53 +0200 Subject: [PATCH 05/14] Update GameData --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 07c001c5..1c68fd5e 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 07c001c5b2b35b2dba2b8428389d3ed375728616 +Subproject commit 1c68fd5efb23798d13154c1de0ad010db319abe2 From ffb8f0e8d3530e24a4406ebb44e459bb07cfc56e Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 28 Aug 2023 03:06:18 +0200 Subject: [PATCH 06/14] =?UTF-8?q?Material=20editor:=20Allow=20negatives=20?= =?UTF-8?q?again=20with=20R=C2=B2G=C2=B2B=C2=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There seems to be people using it. --- .../ModEditWindow.Materials.ColorSet.cs | 26 ++++++++++++++++--- .../ModEditWindow.Materials.ConstantEditor.cs | 8 +++--- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs index 798939ca..daca1098 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs @@ -483,13 +483,13 @@ private bool DrawDyePreview( MtrlTab tab, int colorSetIdx, int rowIdx, bool disa private static bool ColorPicker( string label, string tooltip, Vector3 input, Action< Vector3 > setter, string letter = "" ) { var ret = false; - var inputSqrt = Vector3.SquareRoot( input ); + var inputSqrt = PseudoSqrtRgb( input ); var tmp = inputSqrt; if( ImGui.ColorEdit3( label, ref tmp, - ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.DisplayRGB | ImGuiColorEditFlags.InputRGB | ImGuiColorEditFlags.NoTooltip ) + ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.DisplayRGB | ImGuiColorEditFlags.InputRGB | ImGuiColorEditFlags.NoTooltip | ImGuiColorEditFlags.HDR ) && tmp != inputSqrt ) { - setter( tmp * tmp ); + setter( PseudoSquareRgb( tmp ) ); ret = true; } @@ -505,4 +505,24 @@ private static bool ColorPicker( string label, string tooltip, Vector3 input, Ac return ret; } + + // Functions to deal with squared RGB values without making negatives useless. + + private static float PseudoSquareRgb(float x) + => x < 0.0f ? -(x * x) : (x * x); + + private static Vector3 PseudoSquareRgb(Vector3 vec) + => new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z)); + + private static Vector4 PseudoSquareRgb(Vector4 vec) + => new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z), vec.W); + + private static float PseudoSqrtRgb(float x) + => x < 0.0f ? -MathF.Sqrt(-x) : MathF.Sqrt(x); + + private static Vector3 PseudoSqrtRgb(Vector3 vec) + => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z)); + + private static Vector4 PseudoSqrtRgb(Vector4 vec) + => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z), vec.W); } \ No newline at end of file diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs index f7ea317d..8a104145 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs @@ -147,11 +147,11 @@ public bool Draw(Span values, bool disabled, float editorWidth) ImGui.SetNextItemWidth(editorWidth); var value = new Vector3(values); if (_squaredRgb) - value = Vector3.SquareRoot(value); + value = PseudoSqrtRgb(value); if (ImGui.ColorEdit3("##0", ref value, ImGuiColorEditFlags.Float | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) && !disabled) { if (_squaredRgb) - value *= value; + value = PseudoSquareRgb(value); if (_clamped) value = Vector3.Clamp(value, Vector3.Zero, Vector3.One); value.CopyTo(values); @@ -165,11 +165,11 @@ public bool Draw(Span values, bool disabled, float editorWidth) ImGui.SetNextItemWidth(editorWidth); var value = new Vector4(values); if (_squaredRgb) - value = new Vector4(MathF.Sqrt(value.X), MathF.Sqrt(value.Y), MathF.Sqrt(value.Z), value.W); + value = PseudoSqrtRgb(value); if (ImGui.ColorEdit4("##0", ref value, ImGuiColorEditFlags.Float | ImGuiColorEditFlags.AlphaPreviewHalf | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) && !disabled) { if (_squaredRgb) - value *= new Vector4(value.X, value.Y, value.Z, 1.0f); + value = PseudoSquareRgb(value); if (_clamped) value = Vector4.Clamp(value, Vector4.Zero, Vector4.One); value.CopyTo(values); From bb8d9441f49ff69e190ae4a0708b37097aefe330 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 30 Aug 2023 01:14:20 +0200 Subject: [PATCH 07/14] Material editor: tweak colorset highlighting Make the frequency framerate-independent, set it to 1 Hz, and decrease the dynamic range. Thanks @StoiaCode for feedback! --- .../ModEditWindow.Materials.MtrlTab.cs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 17f34b64..9061cc98 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Numerics; @@ -74,7 +75,7 @@ private sealed class MtrlTab : IWritable, IDisposable public readonly List MaterialPreviewers = new(4); public readonly List ColorSetPreviewers = new(4); public int HighlightedColorSetRow = -1; - public int HighlightTime = -1; + public readonly Stopwatch HighlightTime = new(); public FullPath FindAssociatedShpk( out string defaultPath, out Utf8GamePath defaultGamePath ) { @@ -546,8 +547,11 @@ public void HighlightColorSetRow(int rowIdx) { var oldRowIdx = HighlightedColorSetRow; - HighlightedColorSetRow = rowIdx; - HighlightTime = (HighlightTime + 1) % 32; + if (HighlightedColorSetRow != rowIdx) + { + HighlightedColorSetRow = rowIdx; + HighlightTime.Restart(); + } if (oldRowIdx >= 0) UpdateColorSetRowPreview(oldRowIdx); @@ -560,7 +564,7 @@ public void CancelColorSetHighlight() var rowIdx = HighlightedColorSetRow; HighlightedColorSetRow = -1; - HighlightTime = -1; + HighlightTime.Reset(); if (rowIdx >= 0) UpdateColorSetRowPreview(rowIdx); @@ -588,7 +592,7 @@ public void UpdateColorSetRowPreview(int rowIdx) } if (HighlightedColorSetRow == rowIdx) - ApplyHighlight(ref row, HighlightTime); + ApplyHighlight(ref row, (float)HighlightTime.Elapsed.TotalSeconds); foreach (var previewer in ColorSetPreviewers) { @@ -625,7 +629,7 @@ public void UpdateColorSetPreview() } if (HighlightedColorSetRow >= 0) - ApplyHighlight(ref rows[HighlightedColorSetRow], HighlightTime); + ApplyHighlight(ref rows[HighlightedColorSetRow], (float)HighlightTime.Elapsed.TotalSeconds); foreach (var previewer in ColorSetPreviewers) { @@ -634,9 +638,9 @@ public void UpdateColorSetPreview() } } - private static void ApplyHighlight(ref MtrlFile.ColorSet.Row row, int time) + private static void ApplyHighlight(ref MtrlFile.ColorSet.Row row, float time) { - var level = Math.Sin(time * Math.PI / 16) * 0.5 + 0.5; + var level = Math.Sin(time * 2.0 * Math.PI) * 0.25 + 0.5; var levelSq = (float)(level * level); row.Diffuse = Vector3.Zero; From 5346abaadf34171d81a0525f16e44d1437eb2a9b Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 30 Aug 2023 01:51:43 +0200 Subject: [PATCH 08/14] Material editor: tear down previewers bound to a CharacterBase that goes away --- .../ModEditWindow.Materials.LivePreview.cs | 2 +- .../ModEditWindow.Materials.MtrlTab.cs | 28 +++++++++++++++++++ Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 5 +++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs index 7d400a71..76ac8915 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs @@ -154,7 +154,7 @@ private abstract unsafe class LiveMaterialPreviewerBase : IDisposable protected readonly int ModelSlot; protected readonly int MaterialSlot; - protected readonly CharacterBase* DrawObject; + public readonly CharacterBase* DrawObject; protected readonly Material* Material; protected bool Valid; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 9061cc98..6677db5b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -512,6 +512,29 @@ public void UnbindFromMaterialInstances() ColorSetPreviewers.Clear(); } + public unsafe void UnbindFromDrawObjectMaterialInstances(nint characterBase) + { + for (var i = MaterialPreviewers.Count; i-- > 0; ) + { + var previewer = MaterialPreviewers[i]; + if ((nint)previewer.DrawObject != characterBase) + continue; + + previewer.Dispose(); + MaterialPreviewers.RemoveAt(i); + } + + for (var i = ColorSetPreviewers.Count; i-- > 0;) + { + var previewer = ColorSetPreviewers[i]; + if ((nint)previewer.DrawObject != characterBase) + continue; + + previewer.Dispose(); + ColorSetPreviewers.RemoveAt(i); + } + } + public void SetShaderPackageFlags(uint shPkFlags) { foreach (var previewer in MaterialPreviewers) @@ -665,7 +688,10 @@ public MtrlTab( ModEditWindow edit, MtrlFile file, string filePath, bool writabl AssociatedBaseDevkit = TryLoadShpkDevkit( "_base", out LoadedBaseDevkitPathName ); LoadShpk( FindAssociatedShpk( out _, out _ ) ); if (writable) + { + _edit._gameEvents.CharacterBaseDestructor += UnbindFromDrawObjectMaterialInstances; BindToMaterialInstances(); + } } ~MtrlTab() @@ -682,6 +708,8 @@ public void Dispose() private void DoDispose() { UnbindFromMaterialInstances(); + if (Writable) + _edit._gameEvents.CharacterBaseDestructor -= UnbindFromDrawObjectMaterialInstances; } public bool Valid diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index f1c78bf7..e40a7915 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -19,6 +19,7 @@ using Penumbra.GameData.Files; using Penumbra.Import.Textures; using Penumbra.Interop.ResourceTree; +using Penumbra.Interop.Services; using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Manager; @@ -45,6 +46,7 @@ public partial class ModEditWindow : Window, IDisposable private readonly ModMergeTab _modMergeTab; private readonly CommunicatorService _communicator; private readonly IDragDropManager _dragDropManager; + private readonly GameEventManager _gameEvents; private Mod? _mod; private Vector2 _iconSize = Vector2.Zero; @@ -546,7 +548,7 @@ private HashSet FindPathsStartingWith(ByteString prefix) public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, StainService stainService, ActiveCollections activeCollections, DalamudServices dalamud, ModMergeTab modMergeTab, - CommunicatorService communicator, TextureManager textures, IDragDropManager dragDropManager) + CommunicatorService communicator, TextureManager textures, IDragDropManager dragDropManager, GameEventManager gameEvents) : base(WindowBaseLabel) { _performance = performance; @@ -562,6 +564,7 @@ public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialo _dragDropManager = dragDropManager; _textures = textures; _fileDialog = fileDialog; + _gameEvents = gameEvents; _materialTab = new FileEditor(this, gameData, config, _fileDialog, "Materials", ".mtrl", () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); From 2ac997610d9b63366d5e964bfe971cd349b15ac3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 31 Aug 2023 01:00:46 +0200 Subject: [PATCH 09/14] Remove Finalize from FileEditor. --- Penumbra/UI/AdvancedWindow/FileEditor.cs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index ac873ce2..d0e9504c 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -39,11 +39,6 @@ public FileEditor(ModEditWindow owner, IDataManager gameData, Configuration conf _combo = new Combo(config, getFiles); } - ~FileEditor() - { - DoDispose(); - } - public void Draw() { using var tab = ImRaii.TabItem(_tabName); @@ -66,15 +61,11 @@ public void Draw() } public void Dispose() - { - DoDispose(); - GC.SuppressFinalize(this); - } - - private void DoDispose() { (_currentFile as IDisposable)?.Dispose(); - _currentFile = null; + _currentFile = null; + (_defaultFile as IDisposable)?.Dispose(); + _defaultFile = null; } private readonly string _tabName; From 5023fafc19366ea5192f77831d00c05db61bee3c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 25 Aug 2023 18:45:13 +0200 Subject: [PATCH 10/14] Some formatting in Materials.Shpk. --- OtterGui | 2 +- .../ModEditWindow.Materials.Shpk.cs | 71 +++++++++++-------- 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/OtterGui b/OtterGui index c8394607..728dd8c3 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit c8394607addd29cb7f8ae3257f635a4486c40a63 +Subproject commit 728dd8c33f8b43f7a2725ac7c8886fe7cb3f04a9 diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs index d3bc826a..0f13f47e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs @@ -101,9 +101,9 @@ private static bool DrawPackageNameInput(MtrlTab tab, bool disabled) if (ImGui.Selectable(value, value == tab.Mtrl.ShaderPackage.Name)) { tab.Mtrl.ShaderPackage.Name = value; - ret = true; - tab.AssociatedShpk = null; - tab.LoadedShpkPath = FullPath.Empty; + ret = true; + tab.AssociatedShpk = null; + tab.LoadedShpkPath = FullPath.Empty; tab.LoadShpk(tab.FindAssociatedShpk(out _, out _)); } } @@ -133,6 +133,7 @@ private static bool DrawShaderFlagsInput(MtrlTab tab, bool disabled) /// private void DrawCustomAssociations(MtrlTab tab) { + const string tooltip = "Click to copy file path to clipboard."; var text = tab.AssociatedShpk == null ? "Associated .shpk file: None" : $"Associated .shpk file: {tab.LoadedShpkPathName}"; @@ -145,20 +146,9 @@ private void DrawCustomAssociations(MtrlTab tab) ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if (ImGui.Selectable(text)) - ImGui.SetClipboardText(tab.LoadedShpkPathName); - - ImGuiUtil.HoverTooltip("Click to copy file path to clipboard."); - - if (ImGui.Selectable(devkitText)) - ImGui.SetClipboardText(tab.LoadedShpkDevkitPathName); - - ImGuiUtil.HoverTooltip("Click to copy file path to clipboard."); - - if (ImGui.Selectable(baseDevkitText)) - ImGui.SetClipboardText(tab.LoadedBaseDevkitPathName); - - ImGuiUtil.HoverTooltip("Click to copy file path to clipboard."); + ImGuiUtil.CopyOnClickSelectable(text, tab.LoadedShpkPathName, tooltip); + ImGuiUtil.CopyOnClickSelectable(devkitText, tab.LoadedShpkDevkitPathName, tooltip); + ImGuiUtil.CopyOnClickSelectable(baseDevkitText, tab.LoadedBaseDevkitPathName, tooltip); if (ImGui.Button("Associate Custom .shpk File")) _fileDialog.OpenFilePicker("Associate Custom .shpk File...", ".shpk", (success, name) => @@ -192,11 +182,12 @@ private static bool DrawMaterialShaderKeys(MtrlTab tab, bool disabled) var ret = false; foreach (var (label, index, description, monoFont, values) in tab.ShaderKeys) { - using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); - ref var key = ref tab.Mtrl.ShaderPackage.ShaderKeys[index]; - var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById(key.Category); - var currentValue = key.Value; - var (currentLabel, _, currentDescription) = values.FirstOrNull(v => v.Value == currentValue) ?? ($"0x{currentValue:X8}", currentValue, string.Empty); + using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); + ref var key = ref tab.Mtrl.ShaderPackage.ShaderKeys[index]; + var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById(key.Category); + var currentValue = key.Value; + var (currentLabel, _, currentDescription) = + values.FirstOrNull(v => v.Value == currentValue) ?? ($"0x{currentValue:X8}", currentValue, string.Empty); if (!disabled && shpkKey.HasValue) { ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); @@ -216,6 +207,7 @@ private static bool DrawMaterialShaderKeys(MtrlTab tab, bool disabled) ImGuiUtil.SelectableHelpMarker(valueDescription); } } + ImGui.SameLine(); if (description.Length > 0) ImGuiUtil.LabeledHelpMarker(label, description); @@ -223,10 +215,14 @@ private static bool DrawMaterialShaderKeys(MtrlTab tab, bool disabled) ImGui.TextUnformatted(label); } else if (description.Length > 0 || currentDescription.Length > 0) + { ImGuiUtil.LabeledHelpMarker($"{label}: {currentLabel}", - description + ((description.Length > 0 && currentDescription.Length > 0) ? "\n\n" : string.Empty) + currentDescription); + description + (description.Length > 0 && currentDescription.Length > 0 ? "\n\n" : string.Empty) + currentDescription); + } else + { ImGui.TextUnformatted($"{label}: {currentLabel}"); + } } return ret; @@ -268,7 +264,7 @@ private static bool DrawMaterialConstants(MtrlTab tab, bool disabled) foreach (var (label, constantIndex, slice, description, monoFont, editor) in group) { var constant = tab.Mtrl.ShaderPackage.Constants[constantIndex]; - var buffer = tab.Mtrl.GetConstantValues(constant); + var buffer = tab.Mtrl.GetConstantValues(constant); if (buffer.Length > 0) { using var id = ImRaii.PushId($"##{constant.Id:X8}:{slice.Start}"); @@ -277,6 +273,7 @@ private static bool DrawMaterialConstants(MtrlTab tab, bool disabled) ret = true; tab.SetMaterialParameter(constant.Id, slice.Start, buffer[slice]); } + ImGui.SameLine(); using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); if (description.Length > 0) @@ -307,8 +304,8 @@ static unsafe bool InputHexUInt16(string label, ref ushort v, ImGuiInputTextFlag static bool ComboTextureAddressMode(string label, ref uint samplerFlags, int bitOffset) { - var current = (TextureAddressMode)((samplerFlags >> bitOffset) & 0x3u); - using var c = ImRaii.Combo(label, current.ToString()); + var current = (TextureAddressMode)((samplerFlags >> bitOffset) & 0x3u); + using var c = ImRaii.Combo(label, current.ToString()); if (!c) return false; @@ -323,6 +320,7 @@ static bool ComboTextureAddressMode(string label, ref uint samplerFlags, int bit ImGuiUtil.SelectableHelpMarker(TextureAddressModeTooltips[(int)value]); } + return ret; } @@ -339,6 +337,7 @@ static bool ComboTextureAddressMode(string label, ref uint samplerFlags, int bit ret = true; tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); } + ImGui.SameLine(); ImGuiUtil.LabeledHelpMarker("U Address Mode", "Method to use for resolving a U texture coordinate that is outside the 0 to 1 range."); @@ -348,6 +347,7 @@ static bool ComboTextureAddressMode(string label, ref uint samplerFlags, int bit ret = true; tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); } + ImGui.SameLine(); ImGuiUtil.LabeledHelpMarker("V Address Mode", "Method to use for resolving a V texture coordinate that is outside the 0 to 1 range."); @@ -355,12 +355,15 @@ static bool ComboTextureAddressMode(string label, ref uint samplerFlags, int bit ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); if (ImGui.DragFloat("##LoDBias", ref lodBias, 0.1f, -8.0f, 7.984375f)) { - sampler.Flags = (uint)((sampler.Flags & ~0x000FFC00) | (uint)((int)Math.Round(Math.Clamp(lodBias, -8.0f, 7.984375f) * 64.0f) & 0x3FF) << 10); - ret = true; + sampler.Flags = (uint)((sampler.Flags & ~0x000FFC00) + | ((uint)((int)Math.Round(Math.Clamp(lodBias, -8.0f, 7.984375f) * 64.0f) & 0x3FF) << 10)); + ret = true; tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); } + ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker("Level of Detail Bias", "Offset from the calculated mipmap level.\n\nHigher means that the texture will start to lose detail nearer.\nLower means that the texture will keep its detail until farther."); + ImGuiUtil.LabeledHelpMarker("Level of Detail Bias", + "Offset from the calculated mipmap level.\n\nHigher means that the texture will start to lose detail nearer.\nLower means that the texture will keep its detail until farther."); var minLod = (int)((sampler.Flags >> 20) & 0xF); ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); @@ -370,8 +373,10 @@ static bool ComboTextureAddressMode(string label, ref uint samplerFlags, int bit ret = true; tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); } + ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker("Minimum Level of Detail", "Most detailed mipmap level to use.\n\n0 is the full-sized texture, 1 is the half-sized texture, 2 is the quarter-sized texture, and so on.\n15 will forcibly reduce the texture to its smallest mipmap."); + ImGuiUtil.LabeledHelpMarker("Minimum Level of Detail", + "Most detailed mipmap level to use.\n\n0 is the full-sized texture, 1 is the half-sized texture, 2 is the quarter-sized texture, and so on.\n15 will forcibly reduce the texture to its smallest mipmap."); using var t = ImRaii.TreeNode("Advanced Settings"); if (!t) @@ -413,7 +418,10 @@ private bool DrawMaterialShader(MtrlTab tab, bool disabled) GC.KeepAlive(tab); var textColor = ImGui.GetColorU32(ImGuiCol.Text); - var textColorWarning = (textColor & 0xFF000000u) | ((textColor & 0x00FEFEFE) >> 1) | (tab.AssociatedShpk == null ? 0x80u : 0x8080u); // Half red or yellow + var textColorWarning = + (textColor & 0xFF000000u) + | ((textColor & 0x00FEFEFE) >> 1) + | (tab.AssociatedShpk == null ? 0x80u : 0x8080u); // Half red or yellow using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning); @@ -443,6 +451,7 @@ private bool DrawMaterialShader(MtrlTab tab, bool disabled) _ => null, }; } + private static string VectorSwizzle(int firstComponent, int lastComponent) => (firstComponent, lastComponent) switch { From ff012768691892e06d9eda774a3306ba07d9145a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 31 Aug 2023 01:11:57 +0200 Subject: [PATCH 11/14] Small cleanup in ResolveContext. --- .../Interop/ResourceTree/ResolveContext.cs | 46 +++++++++---------- .../ModEditWindow.Materials.LivePreview.cs | 17 +++---- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index d14bd68b..0cb854f3 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -65,31 +65,10 @@ 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); - public static unsafe FullPath GetResourceHandlePath(ResourceHandle* handle) - { - var name = handle->FileName(); - if (name.IsEmpty) - return FullPath.Empty; - - if (name[0] == (byte)'|') - { - var pos = name.IndexOf((byte)'|', 1); - if (pos < 0) - return FullPath.Empty; - - name = name.Substring(pos + 1); - } - - 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); + { + var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(handle), out var p) ? new FullPath(p) : FullPath.Empty; if (fullPath.InternalName.IsEmpty) return null; @@ -294,4 +273,25 @@ private List Filter(List gamePaths) var i = index.GetOffset(array.Length); return i >= 0 && i < array.Length ? array[i] : null; } + + internal static unsafe ByteString GetResourceHandlePath(ResourceHandle* handle) + { + if (handle == null) + return ByteString.Empty; + + var name = handle->FileName(); + if (name.IsEmpty) + return ByteString.Empty; + + if (name[0] == (byte)'|') + { + var pos = name.IndexOf((byte)'|', 1); + if (pos < 0) + return ByteString.Empty; + + name = name.Substring(pos + 1); + } + + return name; + } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs index 76ac8915..d2ce8796 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs @@ -9,6 +9,7 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.GameData.Files; using Penumbra.Interop.ResourceTree; +using Penumbra.String; using Structs = Penumbra.Interop.Structs; namespace Penumbra.UI.AdvancedWindow; @@ -52,8 +53,8 @@ public partial class ModEditWindow 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) - { + static void CollectMaterials(List<(int, int, int, int)> result, int subActorType, int childObjectIndex, CharacterBase* drawObject, ByteString materialPath) + { for (var i = 0; i < drawObject->SlotCount; ++i) { var model = drawObject->Models[i]; @@ -67,11 +68,8 @@ static void CollectMaterials(List<(int, int, int, int)> result, int subActorType continue; var mtrlHandle = material->MaterialResourceHandle; - if (mtrlHandle == null) - continue; - var path = ResolveContext.GetResourceHandlePath((Structs.ResourceHandle*)mtrlHandle); - if (path.ToString() == materialPath) + if (path == materialPath) result.Add((subActorType, childObjectIndex, i, j)); } } @@ -82,9 +80,8 @@ static void CollectMaterials(List<(int, int, int, int)> result, int subActorType if (drawObject == null) return result; - materialPath = materialPath.Replace('/', '\\').ToLowerInvariant(); - - CollectMaterials(result, subActorType, -1, drawObject, materialPath); + var path = ByteString.FromString(materialPath.Replace('/', '\\'), out var m, true) ? m : ByteString.Empty; + CollectMaterials(result, subActorType, -1, drawObject, path); var firstChildObject = (CharacterBase*)drawObject->DrawObject.Object.ChildObject; if (firstChildObject != null) @@ -93,7 +90,7 @@ static void CollectMaterials(List<(int, int, int, int)> result, int subActorType var childObjectIndex = 0; do { - CollectMaterials(result, subActorType, childObjectIndex, childObject, materialPath); + CollectMaterials(result, subActorType, childObjectIndex, childObject, path); childObject = (CharacterBase*)childObject->DrawObject.Object.NextSiblingObject; ++childObjectIndex; From e5e555b981a6afc2149c60cd581e1f8d41b68a93 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 31 Aug 2023 17:12:39 +0200 Subject: [PATCH 12/14] Auto-formatting and some cleanup. --- .../ModEditWindow.Materials.ColorSet.cs | 478 ++++++------ .../ModEditWindow.Materials.ConstantEditor.cs | 103 +-- .../ModEditWindow.Materials.MtrlTab.cs | 294 ++++---- .../ModEditWindow.Materials.Shpk.cs | 23 +- .../AdvancedWindow/ModEditWindow.Materials.cs | 226 +++--- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 1 - .../ModEditWindow.ShaderPackages.cs | 690 ++++++++---------- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 6 + 8 files changed, 869 insertions(+), 952 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs index daca1098..e1ba045d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs @@ -17,65 +17,60 @@ public partial class ModEditWindow private static readonly float HalfMaxValue = (float)Half.MaxValue; private static readonly float HalfEpsilon = (float)Half.Epsilon; - private bool DrawMaterialColorSetChange( MtrlTab tab, bool disabled ) + private bool DrawMaterialColorSetChange(MtrlTab tab, bool disabled) { - if( !tab.SamplerIds.Contains( ShpkFile.TableSamplerId ) || !tab.Mtrl.ColorSets.Any( c => c.HasRows ) ) - { + if (!tab.SamplerIds.Contains(ShpkFile.TableSamplerId) || !tab.Mtrl.ColorSets.Any(c => c.HasRows)) return false; - } - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - if( !ImGui.CollapsingHeader( "Color Set", ImGuiTreeNodeFlags.DefaultOpen ) ) - { + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + if (!ImGui.CollapsingHeader("Color Set", ImGuiTreeNodeFlags.DefaultOpen)) return false; - } var hasAnyDye = tab.UseColorDyeSet; - ColorSetCopyAllClipboardButton( tab.Mtrl, 0 ); + ColorSetCopyAllClipboardButton(tab.Mtrl, 0); ImGui.SameLine(); - var ret = ColorSetPasteAllClipboardButton( tab, 0, disabled ); - if( !disabled ) + var ret = ColorSetPasteAllClipboardButton(tab, 0, disabled); + if (!disabled) { ImGui.SameLine(); - ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); ImGui.SameLine(); - ret |= ColorSetDyeableCheckbox( tab, ref hasAnyDye ); + ret |= ColorSetDyeableCheckbox(tab, ref hasAnyDye); } - if( hasAnyDye ) + + if (hasAnyDye) { ImGui.SameLine(); - ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); ImGui.SameLine(); - ret |= DrawPreviewDye( tab, disabled ); + ret |= DrawPreviewDye(tab, disabled); } - using var table = ImRaii.Table( "##ColorSets", hasAnyDye ? 11 : 9, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV ); - if( !table ) - { + using var table = ImRaii.Table("##ColorSets", hasAnyDye ? 11 : 9, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV); + if (!table) return false; - } ImGui.TableNextColumn(); - ImGui.TableHeader( string.Empty ); + ImGui.TableHeader(string.Empty); ImGui.TableNextColumn(); - ImGui.TableHeader( "Row" ); + ImGui.TableHeader("Row"); ImGui.TableNextColumn(); - ImGui.TableHeader( "Diffuse" ); + ImGui.TableHeader("Diffuse"); ImGui.TableNextColumn(); - ImGui.TableHeader( "Specular" ); + ImGui.TableHeader("Specular"); ImGui.TableNextColumn(); - ImGui.TableHeader( "Emissive" ); + ImGui.TableHeader("Emissive"); ImGui.TableNextColumn(); - ImGui.TableHeader( "Gloss" ); + ImGui.TableHeader("Gloss"); ImGui.TableNextColumn(); - ImGui.TableHeader( "Tile" ); + ImGui.TableHeader("Tile"); ImGui.TableNextColumn(); - ImGui.TableHeader( "Repeat" ); + ImGui.TableHeader("Repeat"); ImGui.TableNextColumn(); - ImGui.TableHeader( "Skew" ); - if( hasAnyDye ) + ImGui.TableHeader("Skew"); + if (hasAnyDye) { ImGui.TableNextColumn(); ImGui.TableHeader("Dye"); @@ -83,12 +78,12 @@ private bool DrawMaterialColorSetChange( MtrlTab tab, bool disabled ) ImGui.TableHeader("Dye Preview"); } - for( var j = 0; j < tab.Mtrl.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 ) + using var _ = ImRaii.PushId(j); + for (var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i) { - ret |= DrawColorSetRow( tab, j, i, disabled, hasAnyDye ); + ret |= DrawColorSetRow(tab, j, i, disabled, hasAnyDye); ImGui.TableNextRow(); } } @@ -97,22 +92,20 @@ private bool DrawMaterialColorSetChange( MtrlTab tab, bool disabled ) } - private static void ColorSetCopyAllClipboardButton( MtrlFile file, int colorSetIdx ) + private static void ColorSetCopyAllClipboardButton(MtrlFile file, int colorSetIdx) { - if( !ImGui.Button( "Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) ) - { + if (!ImGui.Button("Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2(200, 0))) return; - } try { - var data1 = file.ColorSets[ colorSetIdx ].Rows.AsBytes(); - var data2 = file.ColorDyeSets.Length > colorSetIdx ? file.ColorDyeSets[ colorSetIdx ].Rows.AsBytes() : ReadOnlySpan< byte >.Empty; + var data1 = file.ColorSets[colorSetIdx].Rows.AsBytes(); + var data2 = file.ColorDyeSets.Length > colorSetIdx ? file.ColorDyeSets[colorSetIdx].Rows.AsBytes() : ReadOnlySpan.Empty; var array = new byte[data1.Length + data2.Length]; - data1.TryCopyTo( array ); - data2.TryCopyTo( array.AsSpan( data1.Length ) ); - var text = Convert.ToBase64String( array ); - ImGui.SetClipboardText( text ); + data1.TryCopyTo(array); + data2.TryCopyTo(array.AsSpan(data1.Length)); + var text = Convert.ToBase64String(array); + ImGui.SetClipboardText(text); } catch { @@ -120,19 +113,19 @@ private static void ColorSetCopyAllClipboardButton( MtrlFile file, int colorSetI } } - private bool DrawPreviewDye( MtrlTab tab, 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 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 < tab.Mtrl.ColorDyeSets.Length; ++j ) + for (var j = 0; j < tab.Mtrl.ColorDyeSets.Length; ++j) { - for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) - { - ret |= tab.Mtrl.ApplyDyeTemplate( _stainService.StmFile, j, i, dyeId ); - } + for (var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i) + ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, j, i, dyeId); } tab.UpdateColorSetPreview(); @@ -147,33 +140,31 @@ private bool DrawPreviewDye( MtrlTab tab, bool disabled ) return false; } - private static unsafe bool ColorSetPasteAllClipboardButton( MtrlTab tab, int colorSetIdx, bool disabled ) + private static unsafe bool ColorSetPasteAllClipboardButton(MtrlTab tab, int colorSetIdx, bool disabled) { - if( !ImGuiUtil.DrawDisabledButton( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ), string.Empty, disabled ) || tab.Mtrl.ColorSets.Length <= colorSetIdx ) - { + if (!ImGuiUtil.DrawDisabledButton("Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2(200, 0), string.Empty, disabled) + || tab.Mtrl.ColorSets.Length <= colorSetIdx) return false; - } try { var text = ImGui.GetClipboardText(); - var data = Convert.FromBase64String( text ); - if( data.Length < Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() ) - { + var data = Convert.FromBase64String(text); + if (data.Length < Marshal.SizeOf()) return false; - } - ref var rows = ref tab.Mtrl.ColorSets[ colorSetIdx ].Rows; - fixed( void* ptr = data, output = &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 >() - && tab.Mtrl.ColorDyeSets.Length > colorSetIdx ) + MemoryUtility.MemCpyUnchecked(output, ptr, Marshal.SizeOf()); + if (data.Length >= Marshal.SizeOf() + Marshal.SizeOf() + && tab.Mtrl.ColorDyeSets.Length > colorSetIdx) { - ref var dyeRows = ref tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows; - fixed( void* output2 = &dyeRows ) + 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 >() ); + MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf(), + Marshal.SizeOf()); } } } @@ -188,38 +179,38 @@ private static unsafe bool ColorSetPasteAllClipboardButton( MtrlTab tab, int col } } - private static unsafe void ColorSetCopyClipboardButton( MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye ) + private static unsafe void ColorSetCopyClipboardButton(MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye) { - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Export this row to your clipboard.", false, true ) ) - { - try - { - var data = new byte[MtrlFile.ColorSet.Row.Size + 2]; - fixed( byte* ptr = data ) - { - MemoryUtility.MemCpyUnchecked( ptr, &row, MtrlFile.ColorSet.Row.Size ); - MemoryUtility.MemCpyUnchecked( ptr + MtrlFile.ColorSet.Row.Size, &dye, 2 ); - } + if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Export this row to your clipboard.", false, true)) + return; - var text = Convert.ToBase64String( data ); - ImGui.SetClipboardText( text ); - } - catch + try + { + var data = new byte[MtrlFile.ColorSet.Row.Size + 2]; + fixed (byte* ptr = data) { - // ignored + MemoryUtility.MemCpyUnchecked(ptr, &row, MtrlFile.ColorSet.Row.Size); + MemoryUtility.MemCpyUnchecked(ptr + MtrlFile.ColorSet.Row.Size, &dye, 2); } + + var text = Convert.ToBase64String(data); + ImGui.SetClipboardText(text); + } + catch + { + // ignored } } - private static bool ColorSetDyeableCheckbox( MtrlTab tab, ref bool dyeable ) + private static bool ColorSetDyeableCheckbox(MtrlTab tab, ref bool dyeable) { - var ret = ImGui.Checkbox( "Dyeable", ref dyeable ); + var ret = ImGui.Checkbox("Dyeable", ref dyeable); - if( ret ) + if (ret) { tab.UseColorDyeSet = dyeable; - if( dyeable ) + if (dyeable) tab.Mtrl.FindOrAddColorDyeSet(); tab.UpdateColorSetPreview(); } @@ -227,215 +218,244 @@ private static bool ColorSetDyeableCheckbox( MtrlTab tab, ref bool dyeable ) return ret; } - private static unsafe bool ColorSetPasteFromClipboardButton( MtrlTab tab, 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 ) ) - { - try - { - var text = ImGui.GetClipboardText(); - var data = Convert.FromBase64String( text ); - if( data.Length != MtrlFile.ColorSet.Row.Size + 2 - || tab.Mtrl.ColorSets.Length <= colorSetIdx ) - { - return false; - } - - fixed( byte* ptr = data ) - { - tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorSet.Row* )ptr; - if( colorSetIdx < tab.Mtrl.ColorDyeSets.Length ) - { - tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorDyeSet.Row* )( ptr + MtrlFile.ColorSet.Row.Size ); - } - } + if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Import an exported row from your clipboard onto this row.", disabled, true)) + return false; - tab.UpdateColorSetRowPreview(rowIdx); + try + { + var text = ImGui.GetClipboardText(); + var data = Convert.FromBase64String(text); + if (data.Length != MtrlFile.ColorSet.Row.Size + 2 + || tab.Mtrl.ColorSets.Length <= colorSetIdx) + return false; - return true; - } - catch + fixed (byte* ptr = data) { - // ignored + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx] = *(MtrlFile.ColorSet.Row*)ptr; + if (colorSetIdx < tab.Mtrl.ColorDyeSets.Length) + tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx] = *(MtrlFile.ColorDyeSet.Row*)(ptr + MtrlFile.ColorSet.Row.Size); } - } - return false; + tab.UpdateColorSetRowPreview(rowIdx); + + return true; + } + catch + { + return false; + } } - private static void ColorSetHighlightButton( MtrlTab tab, 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 ); + 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 ) + if (ImGui.IsItemHovered()) + tab.HighlightColorSetRow(rowIdx); + else if (tab.HighlightedColorSetRow == rowIdx) tab.CancelColorSetHighlight(); } - private bool DrawColorSetRow( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled, bool hasAnyDye ) + private bool DrawColorSetRow(MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled, bool hasAnyDye) { - static bool FixFloat( ref float val, float current ) + static bool FixFloat(ref float val, float current) { - val = ( float )( Half )val; + val = (float)(Half)val; return val != current; } - using var id = ImRaii.PushId( rowIdx ); - var row = tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ]; + using var id = ImRaii.PushId(rowIdx); + var row = tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx]; var hasDye = hasAnyDye && tab.Mtrl.ColorDyeSets.Length > colorSetIdx; - var dye = hasDye ? tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] : new MtrlFile.ColorDyeSet.Row(); + 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 ); + ColorSetCopyClipboardButton(row, dye); ImGui.SameLine(); - var ret = ColorSetPasteFromClipboardButton( tab, colorSetIdx, rowIdx, disabled ); + var ret = ColorSetPasteFromClipboardButton(tab, colorSetIdx, rowIdx, disabled); ImGui.SameLine(); - ColorSetHighlightButton( tab, rowIdx, disabled ); + ColorSetHighlightButton(tab, rowIdx, disabled); ImGui.TableNextColumn(); - ImGui.TextUnformatted( $"#{rowIdx + 1:D2}" ); + ImGui.TextUnformatted($"#{rowIdx + 1:D2}"); ImGui.TableNextColumn(); - using var dis = ImRaii.Disabled( disabled ); - ret |= ColorPicker( "##Diffuse", "Diffuse Color", row.Diffuse, c => { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = c; tab.UpdateColorSetRowPreview(rowIdx); } ); - if( hasDye ) + using var dis = ImRaii.Disabled(disabled); + 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 => { tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = b; tab.UpdateColorSetRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled ); + ret |= ImGuiUtil.Checkbox("##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse, + b => + { + tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].Diffuse = b; + tab.UpdateColorSetRowPreview(rowIdx); + }, ImGuiHoveredFlags.AllowWhenDisabled); } ImGui.TableNextColumn(); - ret |= ColorPicker( "##Specular", "Specular Color", row.Specular, c => { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Specular = c; tab.UpdateColorSetRowPreview(rowIdx); } ); + 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, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.SpecularStrength ) ) + ImGui.SetNextItemWidth(floatSize); + if (ImGui.DragFloat("##SpecularStrength", ref tmpFloat, 0.1f, 0f, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.SpecularStrength)) { - tab.Mtrl.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 ); + ImGuiUtil.HoverTooltip("Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled); - if( hasDye ) + if (hasDye) { ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox( "##dyeSpecular", "Apply Specular Color on Dye", dye.Specular, - b => { tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Specular = b; tab.UpdateColorSetRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled ); + ret |= ImGuiUtil.Checkbox("##dyeSpecular", "Apply Specular Color on Dye", dye.Specular, + 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 => { tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = b; tab.UpdateColorSetRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled ); + ret |= ImGuiUtil.Checkbox("##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength, + b => + { + tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].SpecularStrength = b; + tab.UpdateColorSetRowPreview(rowIdx); + }, ImGuiHoveredFlags.AllowWhenDisabled); } ImGui.TableNextColumn(); - ret |= ColorPicker( "##Emissive", "Emissive Color", row.Emissive, c => { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = c; tab.UpdateColorSetRowPreview(rowIdx); } ); - if( hasDye ) + 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 => { tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = b; tab.UpdateColorSetRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled ); + ret |= ImGuiUtil.Checkbox("##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive, + b => + { + tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].Emissive = b; + tab.UpdateColorSetRowPreview(rowIdx); + }, ImGuiHoveredFlags.AllowWhenDisabled); } ImGui.TableNextColumn(); tmpFloat = row.GlossStrength; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, Math.Max( 0.1f, tmpFloat * 0.025f ), HalfEpsilon, HalfMaxValue, "%.1f" ) && FixFloat( ref tmpFloat, row.GlossStrength ) ) + ImGui.SetNextItemWidth(floatSize); + if (ImGui.DragFloat("##GlossStrength", ref tmpFloat, Math.Max(0.1f, tmpFloat * 0.025f), HalfEpsilon, HalfMaxValue, "%.1f") + && FixFloat(ref tmpFloat, row.GlossStrength)) { - tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = Math.Max(tmpFloat, HalfEpsilon); - ret = true; + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].GlossStrength = Math.Max(tmpFloat, HalfEpsilon); + ret = true; tab.UpdateColorSetRowPreview(rowIdx); } - ImGuiUtil.HoverTooltip( "Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled ); - if( hasDye ) + ImGuiUtil.HoverTooltip("Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled); + if (hasDye) { ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox( "##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss, - b => { tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Gloss = b; tab.UpdateColorSetRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled ); + ret |= ImGuiUtil.Checkbox("##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss, + b => + { + tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].Gloss = b; + tab.UpdateColorSetRowPreview(rowIdx); + }, ImGuiHoveredFlags.AllowWhenDisabled); } ImGui.TableNextColumn(); int tmpInt = row.TileSet; - ImGui.SetNextItemWidth( intSize ); - if( ImGui.DragInt( "##TileSet", ref tmpInt, 0.25f, 0, 63 ) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue ) + ImGui.SetNextItemWidth(intSize); + if (ImGui.DragInt("##TileSet", ref tmpInt, 0.25f, 0, 63) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue) { - tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )Math.Clamp(tmpInt, 0, 63); - ret = true; + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].TileSet = (ushort)Math.Clamp(tmpInt, 0, 63); + ret = true; tab.UpdateColorSetRowPreview(rowIdx); } - ImGuiUtil.HoverTooltip( "Tile Set", ImGuiHoveredFlags.AllowWhenDisabled ); + ImGuiUtil.HoverTooltip("Tile Set", ImGuiHoveredFlags.AllowWhenDisabled); ImGui.TableNextColumn(); tmpFloat = row.MaterialRepeat.X; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.MaterialRepeat.X ) ) + ImGui.SetNextItemWidth(floatSize); + if (ImGui.DragFloat("##RepeatX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") + && FixFloat(ref tmpFloat, row.MaterialRepeat.X)) { - tab.Mtrl.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 ); + ImGuiUtil.HoverTooltip("Repeat X", ImGuiHoveredFlags.AllowWhenDisabled); ImGui.SameLine(); tmpFloat = row.MaterialRepeat.Y; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.MaterialRepeat.Y ) ) + ImGui.SetNextItemWidth(floatSize); + if (ImGui.DragFloat("##RepeatY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") + && FixFloat(ref tmpFloat, row.MaterialRepeat.Y)) { - tab.Mtrl.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 ); + ImGuiUtil.HoverTooltip("Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled); ImGui.TableNextColumn(); tmpFloat = row.MaterialSkew.X; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.MaterialSkew.X ) ) + ImGui.SetNextItemWidth(floatSize); + if (ImGui.DragFloat("##SkewX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialSkew.X)) { - tab.Mtrl.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 ); + ImGuiUtil.HoverTooltip("Skew X", ImGuiHoveredFlags.AllowWhenDisabled); ImGui.SameLine(); tmpFloat = row.MaterialSkew.Y; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.MaterialSkew.Y ) ) + ImGui.SetNextItemWidth(floatSize); + if (ImGui.DragFloat("##SkewY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialSkew.Y)) { - tab.Mtrl.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 ); + ImGuiUtil.HoverTooltip("Skew Y", ImGuiHoveredFlags.AllowWhenDisabled); - if( hasDye ) + if (hasDye) { ImGui.TableNextColumn(); - if (_stainService.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize - + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) ) + if (_stainService.TemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) { - tab.Mtrl.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 ); + ImGuiUtil.HoverTooltip("Dye Template", ImGuiHoveredFlags.AllowWhenDisabled); ImGui.TableNextColumn(); - ret |= DrawDyePreview( tab, colorSetIdx, rowIdx, disabled, dye, floatSize ); + ret |= DrawDyePreview(tab, colorSetIdx, rowIdx, disabled, dye, floatSize); } - else if ( hasAnyDye ) + else if (hasAnyDye) { ImGui.TableNextColumn(); ImGui.TableNextColumn(); @@ -445,63 +465,65 @@ static bool FixFloat( ref float val, float current ) return ret; } - private bool DrawDyePreview( MtrlTab tab, 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 ) ) - { + if (stain == 0 || !_stainService.StmFile.Entries.TryGetValue(dye.Template, out var entry)) return false; - } - var values = entry[ ( int )stain ]; - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2 ); + var values = entry[(int)stain]; + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2); - var ret = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), - "Apply the selected dye to this row.", disabled, true ); + var ret = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2(ImGui.GetFrameHeight()), + "Apply the selected dye to this row.", disabled, true); - ret = ret && tab.Mtrl.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" ); + ColorPicker("##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D"); ImGui.SameLine(); - ColorPicker( "##specularPreview", string.Empty, values.Specular, _ => { }, "S" ); + ColorPicker("##specularPreview", string.Empty, values.Specular, _ => { }, "S"); ImGui.SameLine(); - ColorPicker( "##emissivePreview", string.Empty, values.Emissive, _ => { }, "E" ); + ColorPicker("##emissivePreview", string.Empty, values.Emissive, _ => { }, "E"); ImGui.SameLine(); using var dis = ImRaii.Disabled(); - ImGui.SetNextItemWidth( floatSize ); - ImGui.DragFloat( "##gloss", ref values.Gloss, 0, values.Gloss, values.Gloss, "%.1f G" ); + ImGui.SetNextItemWidth(floatSize); + ImGui.DragFloat("##gloss", ref values.Gloss, 0, values.Gloss, values.Gloss, "%.1f G"); ImGui.SameLine(); - ImGui.SetNextItemWidth( floatSize ); - ImGui.DragFloat( "##specularStrength", ref values.SpecularPower, 0, values.SpecularPower, values.SpecularPower, "%.2f S" ); + ImGui.SetNextItemWidth(floatSize); + ImGui.DragFloat("##specularStrength", ref values.SpecularPower, 0, values.SpecularPower, values.SpecularPower, "%.2f S"); return ret; } - private static bool ColorPicker( string label, string tooltip, Vector3 input, Action< Vector3 > setter, string letter = "" ) + private static bool ColorPicker(string label, string tooltip, Vector3 input, Action setter, string letter = "") { - var ret = false; - var inputSqrt = PseudoSqrtRgb( input ); - var tmp = inputSqrt; - if( ImGui.ColorEdit3( label, ref tmp, - ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.DisplayRGB | ImGuiColorEditFlags.InputRGB | ImGuiColorEditFlags.NoTooltip | ImGuiColorEditFlags.HDR ) - && tmp != inputSqrt ) - { - setter( PseudoSquareRgb( tmp ) ); + var ret = false; + var inputSqrt = PseudoSqrtRgb(input); + var tmp = inputSqrt; + if (ImGui.ColorEdit3(label, ref tmp, + ImGuiColorEditFlags.NoInputs + | ImGuiColorEditFlags.DisplayRGB + | ImGuiColorEditFlags.InputRGB + | ImGuiColorEditFlags.NoTooltip + | ImGuiColorEditFlags.HDR) + && tmp != inputSqrt) + { + setter(PseudoSquareRgb(tmp)); ret = true; } - if( letter.Length > 0 && ImGui.IsItemVisible() ) + if (letter.Length > 0 && ImGui.IsItemVisible()) { - var textSize = ImGui.CalcTextSize( letter ); - var center = ImGui.GetItemRectMin() + ( ImGui.GetItemRectSize() - textSize ) / 2; + var textSize = ImGui.CalcTextSize(letter); + var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2; var textColor = input.LengthSquared() < 0.25f ? 0x80FFFFFFu : 0x80000000u; - ImGui.GetWindowDrawList().AddText( center, textColor, letter ); + ImGui.GetWindowDrawList().AddText(center, textColor, letter); } - ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled ); + ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled); return ret; } @@ -509,7 +531,7 @@ private static bool ColorPicker( string label, string tooltip, Vector3 input, Ac // Functions to deal with squared RGB values without making negatives useless. private static float PseudoSquareRgb(float x) - => x < 0.0f ? -(x * x) : (x * x); + => x < 0.0f ? -(x * x) : x * x; private static Vector3 PseudoSquareRgb(Vector3 vec) => new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z)); @@ -525,4 +547,4 @@ private static Vector3 PseudoSqrtRgb(Vector3 vec) private static Vector4 PseudoSqrtRgb(Vector4 vec) => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z), vec.W); -} \ No newline at end of file +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs index 8a104145..e5b16a47 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Numerics; using ImGuiNET; using OtterGui.Raii; @@ -27,7 +28,8 @@ private sealed class FloatConstantEditor : IConstantEditor private readonly float _bias; private readonly string _format; - public FloatConstantEditor(float? minimum, float? maximum, float speed, float relativeSpeed, float factor, float bias, byte precision, string unit) + public FloatConstantEditor(float? minimum, float? maximum, float speed, float relativeSpeed, float factor, float bias, byte precision, + string unit) { _minimum = minimum; _maximum = maximum; @@ -55,10 +57,13 @@ public bool Draw(Span values, bool disabled, float editorWidth) var value = (values[valueIdx] - _bias) / _factor; if (disabled) + { ImGui.DragFloat($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), value, value, _format); + } else { - if (ImGui.DragFloat($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0.0f, _maximum ?? 0.0f, _format)) + if (ImGui.DragFloat($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0.0f, + _maximum ?? 0.0f, _format)) { values[valueIdx] = Clamp(value) * _factor + _bias; ret = true; @@ -111,10 +116,13 @@ public bool Draw(Span values, bool disabled, float editorWidth) var value = (int)Math.Clamp(MathF.Round((values[valueIdx] - _bias) / _factor), int.MinValue, int.MaxValue); if (disabled) + { ImGui.DragInt($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), value, value, _format); + } else { - if (ImGui.DragInt($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0, _maximum ?? 0, _format)) + if (ImGui.DragInt($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0, _maximum ?? 0, + _format)) { values[valueIdx] = Clamp(value) * _factor + _bias; ret = true; @@ -142,14 +150,17 @@ public ColorConstantEditor(bool squaredRgb, bool clamped) public bool Draw(Span values, bool disabled, float editorWidth) { - if (values.Length == 3) + switch (values.Length) { - ImGui.SetNextItemWidth(editorWidth); - var value = new Vector3(values); - if (_squaredRgb) - value = PseudoSqrtRgb(value); - if (ImGui.ColorEdit3("##0", ref value, ImGuiColorEditFlags.Float | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) && !disabled) + case 3: { + ImGui.SetNextItemWidth(editorWidth); + var value = new Vector3(values); + if (_squaredRgb) + value = PseudoSqrtRgb(value); + if (!ImGui.ColorEdit3("##0", ref value, ImGuiColorEditFlags.Float | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) || disabled) + return false; + if (_squaredRgb) value = PseudoSquareRgb(value); if (_clamped) @@ -157,17 +168,17 @@ public bool Draw(Span values, bool disabled, float editorWidth) value.CopyTo(values); return true; } - - return false; - } - else if (values.Length == 4) - { - ImGui.SetNextItemWidth(editorWidth); - var value = new Vector4(values); - if (_squaredRgb) - value = PseudoSqrtRgb(value); - if (ImGui.ColorEdit4("##0", ref value, ImGuiColorEditFlags.Float | ImGuiColorEditFlags.AlphaPreviewHalf | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) && !disabled) + case 4: { + ImGui.SetNextItemWidth(editorWidth); + var value = new Vector4(values); + if (_squaredRgb) + value = PseudoSqrtRgb(value); + if (!ImGui.ColorEdit4("##0", ref value, + ImGuiColorEditFlags.Float | ImGuiColorEditFlags.AlphaPreviewHalf | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) + || disabled) + return false; + if (_squaredRgb) value = PseudoSquareRgb(value); if (_clamped) @@ -175,11 +186,8 @@ public bool Draw(Span values, bool disabled, float editorWidth) value.CopyTo(values); return true; } - - return false; + default: return FloatConstantEditor.Default.Draw(values, disabled, editorWidth); } - else - return FloatConstantEditor.Default.Draw(values, disabled, editorWidth); } } @@ -188,9 +196,7 @@ private sealed class EnumConstantEditor : IConstantEditor private readonly IReadOnlyList<(string Label, float Value, string Description)> _values; public EnumConstantEditor(IReadOnlyList<(string Label, float Value, string Description)> values) - { - _values = values; - } + => _values = values; public bool Draw(Span values, bool disabled, float editorWidth) { @@ -200,33 +206,40 @@ public bool Draw(Span values, bool disabled, float editorWidth) for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) { + using var id = ImRaii.PushId(valueIdx); if (valueIdx > 0) ImGui.SameLine(); ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx)); var currentValue = values[valueIdx]; - var (currentLabel, _, currentDescription) = _values.FirstOrNull(v => v.Value == currentValue) ?? (currentValue.ToString(), currentValue, string.Empty); - if (disabled) - ImGui.InputText($"##{valueIdx}", ref currentLabel, (uint)currentLabel.Length, ImGuiInputTextFlags.ReadOnly); - else + var currentLabel = _values.FirstOrNull(v => v.Value == currentValue)?.Label + ?? currentValue.ToString(CultureInfo.CurrentCulture); + ret = disabled + ? ImGui.InputText(string.Empty, ref currentLabel, (uint)currentLabel.Length, ImGuiInputTextFlags.ReadOnly) + : DrawCombo(currentLabel, ref values[valueIdx]); + } + + return ret; + } + + private bool DrawCombo(string label, ref float currentValue) + { + using var c = ImRaii.Combo(string.Empty, label); + if (!c) + return false; + + var ret = false; + foreach (var (valueLabel, value, valueDescription) in _values) + { + if (ImGui.Selectable(valueLabel, value == currentValue)) { - using var c = ImRaii.Combo($"##{valueIdx}", currentLabel); - { - if (c) - foreach (var (valueLabel, value, valueDescription) in _values) - { - if (ImGui.Selectable(valueLabel, value == currentValue)) - { - values[valueIdx] = value; - ret = true; - } - - if (valueDescription.Length > 0) - ImGuiUtil.SelectableHelpMarker(valueDescription); - } - } + currentValue = value; + ret = true; } + + if (valueDescription.Length > 0) + ImGuiUtil.SelectableHelpMarker(valueDescription); } return ret; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 6677db5b..12f7acd7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -6,7 +6,6 @@ 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 Newtonsoft.Json.Linq; @@ -17,10 +16,8 @@ using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.GameData.Structs; -using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; -using Penumbra.Util; using static Penumbra.GameData.Files.ShpkFile; namespace Penumbra.UI.AdvancedWindow; @@ -48,28 +45,32 @@ private sealed class MtrlTab : IWritable, IDisposable public ShpkFile? AssociatedShpk; public JObject? AssociatedShpkDevkit; - public readonly string LoadedBaseDevkitPathName = string.Empty; + public readonly string LoadedBaseDevkitPathName; public readonly JObject? AssociatedBaseDevkit; // Shader Key State - public readonly List< (string Label, int Index, string Description, bool MonoFont, IReadOnlyList< (string Label, uint Value, string Description) > Values) > ShaderKeys = new(16); + public readonly + List<(string Label, int Index, string Description, bool MonoFont, IReadOnlyList<(string Label, uint Value, string Description)> + Values)> ShaderKeys = new(16); - public readonly HashSet< int > VertexShaders = new(16); - public readonly HashSet< int > PixelShaders = new(16); - public bool ShadersKnown = false; - public string VertexShadersString = "Vertex Shaders: ???"; - public string PixelShadersString = "Pixel Shaders: ???"; + public readonly HashSet VertexShaders = new(16); + public readonly HashSet PixelShaders = new(16); + public bool ShadersKnown; + public string VertexShadersString = "Vertex Shaders: ???"; + public string PixelShadersString = "Pixel Shaders: ???"; // Textures & Samplers - public readonly List< (string Label, int TextureIndex, int SamplerIndex, string Description, bool MonoFont) > Textures = new(4); + public readonly List<(string Label, int TextureIndex, int SamplerIndex, string Description, bool MonoFont)> Textures = new(4); - public readonly HashSet< int > UnfoldedTextures = new(4); - public readonly HashSet< uint > SamplerIds = new(16); - public float TextureLabelWidth; - public bool UseColorDyeSet; + public readonly HashSet UnfoldedTextures = new(4); + public readonly HashSet SamplerIds = new(16); + public float TextureLabelWidth; + public bool UseColorDyeSet; // Material Constants - public readonly List< (string Header, List< (string Label, int ConstantIndex, Range Slice, string Description, bool MonoFont, IConstantEditor Editor) > Constants) > Constants = new(16); + public readonly + List<(string Header, List<(string Label, int ConstantIndex, Range Slice, string Description, bool MonoFont, IConstantEditor Editor)> + Constants)> Constants = new(16); // Live-Previewers public readonly List MaterialPreviewers = new(4); @@ -77,15 +78,13 @@ private sealed class MtrlTab : IWritable, IDisposable public int HighlightedColorSetRow = -1; public readonly Stopwatch HighlightTime = new(); - public FullPath FindAssociatedShpk( out string defaultPath, out Utf8GamePath defaultGamePath ) + public FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath) { - defaultPath = GamePaths.Shader.ShpkPath( Mtrl.ShaderPackage.Name ); - if( !Utf8GamePath.FromString( defaultPath, out defaultGamePath, true ) ) - { + defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name); + if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath, true)) return FullPath.Empty; - } - return _edit.FindBestMatch( defaultGamePath ); + return _edit.FindBestMatch(defaultGamePath); } public string[] GetShpkNames() @@ -102,7 +101,7 @@ public string[] GetShpkNames() return _shpkNames; } - public void LoadShpk( FullPath path ) + public void LoadShpk(FullPath path) { ShaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader"; @@ -110,26 +109,30 @@ public void LoadShpk( FullPath path ) { LoadedShpkPath = path; var data = LoadedShpkPath.IsRooted - ? File.ReadAllBytes( LoadedShpkPath.FullName ) - : _edit._dalamud.GameData.GetFile( LoadedShpkPath.InternalName.ToString() )?.Data; - AssociatedShpk = data?.Length > 0 ? new ShpkFile( data ) : throw new Exception( "Failure to load file data." ); + ? File.ReadAllBytes(LoadedShpkPath.FullName) + : _edit._dalamud.GameData.GetFile(LoadedShpkPath.InternalName.ToString())?.Data; + AssociatedShpk = data?.Length > 0 ? new ShpkFile(data) : throw new Exception("Failure to load file data."); LoadedShpkPathName = path.ToPath(); } - catch( Exception e ) + catch (Exception e) { LoadedShpkPath = FullPath.Empty; LoadedShpkPathName = string.Empty; AssociatedShpk = null; - Penumbra.Chat.NotificationMessage( $"Could not load {LoadedShpkPath.ToPath()}:\n{e}", "Penumbra Advanced Editing", NotificationType.Error ); + Penumbra.Chat.NotificationMessage($"Could not load {LoadedShpkPath.ToPath()}:\n{e}", "Penumbra Advanced Editing", + NotificationType.Error); } - if( LoadedShpkPath.InternalName.IsEmpty ) + if (LoadedShpkPath.InternalName.IsEmpty) { AssociatedShpkDevkit = null; LoadedShpkDevkitPathName = string.Empty; } else - AssociatedShpkDevkit = TryLoadShpkDevkit( Path.GetFileNameWithoutExtension( Mtrl.ShaderPackage.Name ), out LoadedShpkDevkitPathName ); + { + AssociatedShpkDevkit = + TryLoadShpkDevkit(Path.GetFileNameWithoutExtension(Mtrl.ShaderPackage.Name), out LoadedShpkDevkitPathName); + } UpdateShaderKeys(); Update(); @@ -157,10 +160,8 @@ public void LoadShpk( FullPath path ) } private T? TryGetShpkDevkitData(string category, uint? id, bool mayVary) where T : class - { - return TryGetShpkDevkitData(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary) - ?? TryGetShpkDevkitData(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary); - } + => TryGetShpkDevkitData(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary) + ?? TryGetShpkDevkitData(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary); private T? TryGetShpkDevkitData(JObject? devkit, string devkitPathName, string category, uint? id, bool mayVary) where T : class { @@ -175,7 +176,7 @@ public void LoadShpk( FullPath path ) if (mayVary && (data as JObject)?["Vary"] != null) { - var selector = BuildSelector(data!["Vary"]! + var selector = BuildSelector(data["Vary"]! .Select(key => (uint)key) .Select(key => Mtrl.GetShaderKey(key)?.Value ?? AssociatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue)); var index = (int)data["Selectors"]![selector.ToString()]!; @@ -192,14 +193,13 @@ public void LoadShpk( FullPath path ) } } - public void UpdateShaderKeys() + private void UpdateShaderKeys() { ShaderKeys.Clear(); if (AssociatedShpk != null) - { foreach (var key in AssociatedShpk.MaterialKeys) { - var dkData = TryGetShpkDevkitData("ShaderKeys", key.Id, false); + var dkData = TryGetShpkDevkitData("ShaderKeys", key.Id, false); var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); var valueSet = new HashSet(key.Values); @@ -211,8 +211,8 @@ public void UpdateShaderKeys() { if (dkData != null && dkData.Values.TryGetValue(value, out var dkValue)) return (dkValue.Label.Length > 0 ? dkValue.Label : $"0x{value:X8}", value, dkValue.Description); - else - return ($"0x{value:X8}", value, string.Empty); + + return ($"0x{value:X8}", value, string.Empty); }).ToArray(); Array.Sort(values, (x, y) => { @@ -220,31 +220,33 @@ public void UpdateShaderKeys() return -1; if (y.Value == key.DefaultValue) return 1; - return x.Label.CompareTo(y.Label); + + return string.Compare(x.Label, y.Label, StringComparison.Ordinal); }); - ShaderKeys.Add((hasDkLabel ? dkData!.Label : $"0x{key.Id:X8}", mtrlKeyIndex, dkData?.Description ?? string.Empty, !hasDkLabel, values)); + ShaderKeys.Add((hasDkLabel ? dkData!.Label : $"0x{key.Id:X8}", mtrlKeyIndex, dkData?.Description ?? string.Empty, + !hasDkLabel, values)); } - } else - { foreach (var (key, index) in Mtrl.ShaderPackage.ShaderKeys.WithIndex()) ShaderKeys.Add(($"0x{key.Category:X8}", index, string.Empty, true, Array.Empty<(string, uint, string)>())); - } } - public void UpdateShaders() + private void UpdateShaders() { VertexShaders.Clear(); PixelShaders.Clear(); if (AssociatedShpk == null) + { ShadersKnown = false; + } else { ShadersKnown = true; var systemKeySelectors = AllSelectors(AssociatedShpk.SystemKeys).ToArray(); var sceneKeySelectors = AllSelectors(AssociatedShpk.SceneKeys).ToArray(); var subViewKeySelectors = AllSelectors(AssociatedShpk.SubViewKeys).ToArray(); - var materialKeySelector = BuildSelector(AssociatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value)); + var materialKeySelector = + BuildSelector(AssociatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value)); foreach (var systemKeySelector in systemKeySelectors) { foreach (var sceneKeySelector in sceneKeySelectors) @@ -252,15 +254,13 @@ public void UpdateShaders() foreach (var subViewKeySelector in subViewKeySelectors) { var selector = BuildSelector(systemKeySelector, sceneKeySelector, materialKeySelector, subViewKeySelector); - var node = AssociatedShpk.GetNodeBySelector(selector); + var node = AssociatedShpk.GetNodeBySelector(selector); if (node.HasValue) - { foreach (var pass in node.Value.Passes) { VertexShaders.Add((int)pass.VertexShader); PixelShaders.Add((int)pass.PixelShader); } - } else ShadersKnown = false; } @@ -272,12 +272,12 @@ public void UpdateShaders() var pixelShaders = PixelShaders.OrderBy(i => i).Select(i => $"#{i}"); VertexShadersString = $"Vertex Shaders: {string.Join(", ", ShadersKnown ? vertexShaders : vertexShaders.Append("???"))}"; - PixelShadersString = $"Pixel Shaders: {string.Join(", ", ShadersKnown ? pixelShaders : pixelShaders.Append("???"))}"; + PixelShadersString = $"Pixel Shaders: {string.Join(", ", ShadersKnown ? pixelShaders : pixelShaders.Append("???"))}"; ShaderComment = TryGetShpkDevkitData("Comment", null, true) ?? string.Empty; } - public void UpdateTextures() + private void UpdateTextures() { Textures.Clear(); SamplerIds.Clear(); @@ -302,50 +302,63 @@ public void UpdateTextures() if (Mtrl.ColorSets.Any(c => c.HasRows)) SamplerIds.Add(TableSamplerId); } + foreach (var samplerId in SamplerIds) { var shpkSampler = AssociatedShpk.GetSamplerById(samplerId); - if (!shpkSampler.HasValue || shpkSampler.Value.Slot != 2) + if (shpkSampler is not { Slot: 2 }) continue; - var dkData = TryGetShpkDevkitData("Samplers", samplerId, true); + var dkData = TryGetShpkDevkitData("Samplers", samplerId, true); var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); var sampler = Mtrl.GetOrAddSampler(samplerId, dkData?.DefaultTexture ?? string.Empty, out var samplerIndex); - Textures.Add((hasDkLabel ? dkData!.Label : shpkSampler.Value.Name, sampler.TextureIndex, samplerIndex, dkData?.Description ?? string.Empty, !hasDkLabel)); + Textures.Add((hasDkLabel ? dkData!.Label : shpkSampler.Value.Name, sampler.TextureIndex, samplerIndex, + dkData?.Description ?? string.Empty, !hasDkLabel)); } + if (SamplerIds.Contains(TableSamplerId)) Mtrl.FindOrAddColorSet(); } + Textures.Sort((x, y) => string.CompareOrdinal(x.Label, y.Label)); TextureLabelWidth = 50f * UiHelpers.Scale; float helpWidth; using (var _ = ImRaii.PushFont(UiBuilder.IconFont)) + { helpWidth = ImGui.GetStyle().ItemSpacing.X + ImGui.CalcTextSize(FontAwesomeIcon.InfoCircle.ToIconString()).X; + } foreach (var (label, _, _, description, monoFont) in Textures) + { if (!monoFont) TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); + } using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) { foreach (var (label, _, _, description, monoFont) in Textures) + { if (monoFont) - TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); + TextureLabelWidth = Math.Max(TextureLabelWidth, + ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); + } } TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4; } - public void UpdateConstants() + private void UpdateConstants() { static List FindOrAddGroup(List<(string, List)> groups, string name) { foreach (var (groupName, group) in groups) + { if (string.Equals(name, groupName, StringComparison.Ordinal)) return group; + } var newGroup = new List(16); groups.Add((name, newGroup)); @@ -360,7 +373,10 @@ static List FindOrAddGroup(List<(string, List)> groups, string name) { var values = Mtrl.GetConstantValues(constant); for (var i = 0; i < values.Length; i += 4) - fcGroup.Add(($"0x{constant.Id:X8}", index, i..Math.Min(i + 4, values.Length), string.Empty, true, FloatConstantEditor.Default)); + { + fcGroup.Add(($"0x{constant.Id:X8}", index, i..Math.Min(i + 4, values.Length), string.Empty, true, + FloatConstantEditor.Default)); + } } } else @@ -371,13 +387,12 @@ static List FindOrAddGroup(List<(string, List)> groups, string name) if ((shpkConstant.ByteSize & 0x3) != 0) continue; - var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, shpkConstant.ByteSize >> 2, out var constantIndex); - var values = Mtrl.GetConstantValues(constant); + var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, shpkConstant.ByteSize >> 2, out var constantIndex); + var values = Mtrl.GetConstantValues(constant); var handledElements = new IndexSet(values.Length, false); var dkData = TryGetShpkDevkitData("Constants", shpkConstant.Id, true); if (dkData != null) - { foreach (var dkConstant in dkData) { var offset = (int)dkConstant.Offset; @@ -386,13 +401,13 @@ static List FindOrAddGroup(List<(string, List)> groups, string name) length = Math.Min(length, (int)dkConstant.Length.Value); if (length <= 0) continue; + var editor = dkConstant.CreateEditor(); if (editor != null) FindOrAddGroup(Constants, dkConstant.Group.Length > 0 ? dkConstant.Group : "Further Constants") .Add((dkConstant.Label, constantIndex, offset..(offset + length), dkConstant.Description, false, editor)); handledElements.AddRange(offset, length); } - } var fcGroup = FindOrAddGroup(Constants, "Further Constants"); foreach (var (start, end) in handledElements.Ranges(true)) @@ -403,15 +418,20 @@ static List FindOrAddGroup(List<(string, List)> groups, string name) for (int i = (start & ~0x3) - (offset & 0x3), j = offset >> 2; i < end; i += 4, ++j) { var rangeStart = Math.Max(i, start); - var rangeEnd = Math.Min(i + 4, end); + var rangeEnd = Math.Min(i + 4, end); if (rangeEnd > rangeStart) - fcGroup.Add(($"{prefix}[{j:D2}]{VectorSwizzle((offset + rangeStart) & 0x3, (offset + rangeEnd - 1) & 0x3)} (0x{shpkConstant.Id:X8})", constantIndex, rangeStart..rangeEnd, string.Empty, true, FloatConstantEditor.Default)); + fcGroup.Add(( + $"{prefix}[{j:D2}]{VectorSwizzle((offset + rangeStart) & 0x3, (offset + rangeEnd - 1) & 0x3)} (0x{shpkConstant.Id:X8})", + constantIndex, rangeStart..rangeEnd, string.Empty, true, FloatConstantEditor.Default)); } } else { for (var i = start; i < end; i += 4) - fcGroup.Add(($"0x{shpkConstant.Id:X8}", constantIndex, i..Math.Min(i + 4, end), string.Empty, true, FloatConstantEditor.Default)); + { + fcGroup.Add(($"0x{shpkConstant.Id:X8}", constantIndex, i..Math.Min(i + 4, end), string.Empty, true, + FloatConstantEditor.Default)); + } } } } @@ -424,20 +444,23 @@ static List FindOrAddGroup(List<(string, List)> groups, string name) return 1; if (string.Equals(y.Header, "Further Constants", StringComparison.Ordinal)) return -1; + return string.Compare(x.Header, y.Header, StringComparison.Ordinal); }); // HACK the Replace makes w appear after xyz, for the cbuffer-location-based naming scheme foreach (var (_, group) in Constants) + { group.Sort((x, y) => string.CompareOrdinal( x.MonoFont ? x.Label.Replace("].w", "].{") : x.Label, y.MonoFont ? y.Label.Replace("].w", "].{") : y.Label)); + } } public unsafe void BindToMaterialInstances() { UnbindFromMaterialInstances(); - var localPlayer = FindLocalPlayer(_edit._dalamud.Objects); + var localPlayer = LocalPlayer(_edit._dalamud.Objects); if (null == localPlayer) return; @@ -449,7 +472,9 @@ public unsafe void BindToMaterialInstances() var drawObjects = stackalloc CharacterBase*[4]; drawObjects[0] = drawObject; - + drawObjects[1] = *((CharacterBase**)&localPlayer->DrawData.MainHand + 1); + drawObjects[2] = *((CharacterBase**)&localPlayer->DrawData.OffHand + 1); + drawObjects[3] = *((CharacterBase**)&localPlayer->DrawData.UnkF0 + 1); for (var i = 0; i < 3; ++i) { var subActor = FindSubActor(localPlayer, i); @@ -470,9 +495,11 @@ public unsafe void BindToMaterialInstances() 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)); + MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._dalamud.Objects, subActorType, childObjectIndex, modelSlot, + materialSlot)); foundMaterials.Add((nint)material); } catch (InvalidOperationException) @@ -480,28 +507,31 @@ public unsafe void BindToMaterialInstances() // Carry on without that previewer. } } + UpdateMaterialPreview(); var colorSet = Mtrl.ColorSets.FirstOrNull(colorSet => colorSet.HasRows); - if (colorSet.HasValue) + if (!colorSet.HasValue) + return; + + foreach (var (subActorType, childObjectIndex, modelSlot, materialSlot) in instances) { - foreach (var (subActorType, childObjectIndex, modelSlot, materialSlot) in instances) + try { - try - { - ColorSetPreviewers.Add(new LiveColorSetPreviewer(_edit._dalamud.Objects, _edit._dalamud.Framework, subActorType, childObjectIndex, modelSlot, materialSlot)); - } - catch (InvalidOperationException) - { - // Carry on without that previewer. - } + ColorSetPreviewers.Add(new LiveColorSetPreviewer(_edit._dalamud.Objects, _edit._dalamud.Framework, subActorType, + childObjectIndex, modelSlot, materialSlot)); + } + catch (InvalidOperationException) + { + // Carry on without that previewer. } - UpdateColorSetPreview(); } + + UpdateColorSetPreview(); } - public void UnbindFromMaterialInstances() + private void UnbindFromMaterialInstances() { foreach (var previewer in MaterialPreviewers) previewer.Dispose(); @@ -512,9 +542,9 @@ public void UnbindFromMaterialInstances() ColorSetPreviewers.Clear(); } - public unsafe void UnbindFromDrawObjectMaterialInstances(nint characterBase) + private unsafe void UnbindFromDrawObjectMaterialInstances(nint characterBase) { - for (var i = MaterialPreviewers.Count; i-- > 0; ) + for (var i = MaterialPreviewers.Count; i-- > 0;) { var previewer = MaterialPreviewers[i]; if ((nint)previewer.DrawObject != characterBase) @@ -553,7 +583,7 @@ public void SetSamplerFlags(uint samplerCrc, uint samplerFlags) previewer.SetSamplerFlags(samplerCrc, samplerFlags); } - public void UpdateMaterialPreview() + private void UpdateMaterialPreview() { SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); foreach (var constant in Mtrl.ShaderPackage.Constants) @@ -562,6 +592,7 @@ public void UpdateMaterialPreview() if (values != null) SetMaterialParameter(constant.Id, 0, values); } + foreach (var sampler in Mtrl.ShaderPackage.Samplers) SetSamplerFlags(sampler.SamplerId, sampler.Flags); } @@ -602,7 +633,7 @@ public void UpdateColorSetRowPreview(int rowIdx) if (!maybeColorSet.HasValue) return; - var colorSet = maybeColorSet.Value; + var colorSet = maybeColorSet.Value; var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index); var row = colorSet.Rows[rowIdx]; @@ -610,7 +641,7 @@ public void UpdateColorSetRowPreview(int rowIdx) { 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)) + if (stm.TryGetValue(dye.Template, _edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) row.ApplyDyeTemplate(dye, dyes); } @@ -619,7 +650,8 @@ public void UpdateColorSetRowPreview(int rowIdx) foreach (var previewer in ColorSetPreviewers) { - row.AsHalves().CopyTo(previewer.ColorSet.AsSpan().Slice(LiveColorSetPreviewer.TextureWidth * 4 * rowIdx, LiveColorSetPreviewer.TextureWidth * 4)); + row.AsHalves().CopyTo(previewer.ColorSet.AsSpan() + .Slice(LiveColorSetPreviewer.TextureWidth * 4 * rowIdx, LiveColorSetPreviewer.TextureWidth * 4)); previewer.ScheduleUpdate(); } } @@ -633,19 +665,19 @@ public void UpdateColorSetPreview() if (!maybeColorSet.HasValue) return; - var colorSet = maybeColorSet.Value; + var colorSet = maybeColorSet.Value; var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index); var rows = colorSet.Rows; if (maybeColorDyeSet.HasValue && UseColorDyeSet) { - var stm = _edit._stainService.StmFile; - var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; + 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]; + var dye = colorDyeSet.Rows[i]; if (stm.TryGetValue(dye.Template, stainId, out var dyes)) row.ApplyDyeTemplate(dye, dyes); } @@ -663,10 +695,10 @@ public void UpdateColorSetPreview() private static void ApplyHighlight(ref MtrlFile.ColorSet.Row row, float time) { - var level = Math.Sin(time * 2.0 * Math.PI) * 0.25 + 0.5; + var level = Math.Sin(time * 2.0 * Math.PI) * 0.25 + 0.5; var levelSq = (float)(level * level); - row.Diffuse = Vector3.Zero; + row.Diffuse = Vector3.Zero; row.Specular = Vector3.Zero; row.Emissive = new Vector3(levelSq); } @@ -678,15 +710,15 @@ public void Update() UpdateConstants(); } - public MtrlTab( ModEditWindow edit, MtrlFile file, string filePath, bool writable ) + public MtrlTab(ModEditWindow edit, MtrlFile file, string filePath, bool writable) { - _edit = edit; - Mtrl = file; - FilePath = filePath; - Writable = writable; - UseColorDyeSet = file.ColorDyeSets.Length > 0; - AssociatedBaseDevkit = TryLoadShpkDevkit( "_base", out LoadedBaseDevkitPathName ); - LoadShpk( FindAssociatedShpk( out _, out _ ) ); + _edit = edit; + Mtrl = file; + FilePath = filePath; + Writable = writable; + UseColorDyeSet = file.ColorDyeSets.Length > 0; + AssociatedBaseDevkit = TryLoadShpkDevkit("_base", out LoadedBaseDevkitPathName); + LoadShpk(FindAssociatedShpk(out _, out _)); if (writable) { _edit._gameEvents.CharacterBaseDestructor += UnbindFromDrawObjectMaterialInstances; @@ -694,18 +726,7 @@ public MtrlTab( ModEditWindow edit, MtrlFile file, string filePath, bool writabl } } - ~MtrlTab() - { - DoDispose(); - } - public void Dispose() - { - DoDispose(); - GC.SuppressFinalize(this); - } - - private void DoDispose() { UnbindFromMaterialInstances(); if (Writable) @@ -723,11 +744,7 @@ public byte[] Write() return output.Write(); } - private sealed class DevkitShaderKeyValue - { - public string Label = string.Empty; - public string Description = string.Empty; - } + private sealed record DevkitShaderKeyValue(string Label = "", string Description = ""); private sealed class DevkitShaderKey { @@ -736,12 +753,7 @@ private sealed class DevkitShaderKey public Dictionary Values = new(); } - private sealed class DevkitSampler - { - public string Label = string.Empty; - public string Description = string.Empty; - public string DefaultTexture = string.Empty; - } + private sealed record DevkitSampler(string Label = "", string Description = "", string DefaultTexture = ""); private enum DevkitConstantType { @@ -752,12 +764,7 @@ private enum DevkitConstantType Enum = 3, } - private sealed class DevkitConstantValue - { - public string Label = string.Empty; - public string Description = string.Empty; - public float Value = 0.0f; - } + private sealed record DevkitConstantValue(string Label = "", string Description = "", float Value = 0); private sealed class DevkitConstant { @@ -783,25 +790,20 @@ private sealed class DevkitConstant public DevkitConstantValue[] Values = Array.Empty(); public IConstantEditor? CreateEditor() - { - switch (Type) + => Type switch { - case DevkitConstantType.Hidden: - return null; - case DevkitConstantType.Float: - return new FloatConstantEditor(Minimum, Maximum, Speed ?? 0.1f, RelativeSpeed, Factor, Bias, Precision, Unit); - case DevkitConstantType.Integer: - return new IntConstantEditor(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed, Factor, Bias, Unit); - case DevkitConstantType.Color: - return new ColorConstantEditor(SquaredRgb, Clamped); - case DevkitConstantType.Enum: - return new EnumConstantEditor(Array.ConvertAll(Values, value => (value.Label, value.Value, value.Description))); - default: - return FloatConstantEditor.Default; - } - } - - private int? ToInteger(float? value) + DevkitConstantType.Hidden => null, + DevkitConstantType.Float => new FloatConstantEditor(Minimum, Maximum, Speed ?? 0.1f, RelativeSpeed, Factor, Bias, Precision, + Unit), + DevkitConstantType.Integer => new IntConstantEditor(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed, + Factor, Bias, Unit), + DevkitConstantType.Color => new ColorConstantEditor(SquaredRgb, Clamped), + DevkitConstantType.Enum => new EnumConstantEditor(Array.ConvertAll(Values, + value => (value.Label, value.Value, value.Description))), + _ => FloatConstantEditor.Default, + }; + + private static int? ToInteger(float? value) => value.HasValue ? (int)Math.Clamp(MathF.Round(value.Value), int.MinValue, int.MaxValue) : null; } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs index 0f13f47e..8fca8aa6 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs @@ -1,18 +1,12 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Numerics; using System.Text; using Dalamud.Interface; -using Dalamud.Interface.ImGuiFileDialog; using ImGuiNET; -using Lumina.Data.Parsing; -using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Raii; using Penumbra.GameData; -using Penumbra.GameData.Files; using Penumbra.String.Classes; namespace Penumbra.UI.AdvancedWindow; @@ -25,7 +19,7 @@ public partial class ModEditWindow // Apricot shader packages are unlisted because // 1. they cause performance/memory issues when calculating the effective shader set // 2. they probably aren't intended for use with materials anyway - private static readonly IReadOnlyList StandardShaderPackages = new string[] + private static readonly IReadOnlyList StandardShaderPackages = new[] { "3dui.shpk", // "apricot_decal_dummy.shpk", @@ -76,7 +70,7 @@ private enum TextureAddressMode : uint Border = 3, } - private static readonly IReadOnlyList TextureAddressModeTooltips = new string[] + private static readonly IReadOnlyList TextureAddressModeTooltips = new[] { "Tile the texture at every UV integer junction.\n\nFor example, for U values between 0 and 3, the texture is repeated three times.", "Flip the texture at every UV integer junction.\n\nFor U values between 0 and 1, for example, the texture is addressed normally; between 1 and 2, the texture is mirrored; between 2 and 3, the texture is normal again; and so on.", @@ -113,18 +107,15 @@ private static bool DrawPackageNameInput(MtrlTab tab, bool disabled) private static bool DrawShaderFlagsInput(MtrlTab tab, bool disabled) { - var ret = false; var shpkFlags = (int)tab.Mtrl.ShaderPackage.Flags; ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); - if (ImGui.InputInt("Shader Flags", ref shpkFlags, 0, 0, + if (!ImGui.InputInt("Shader Flags", ref shpkFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) - { - tab.Mtrl.ShaderPackage.Flags = (uint)shpkFlags; - ret = true; - tab.SetShaderPackageFlags((uint)shpkFlags); - } + return false; - return ret; + tab.Mtrl.ShaderPackage.Flags = (uint)shpkFlags; + tab.SetShaderPackageFlags((uint)shpkFlags); + return true; } /// diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index b89bab01..102a6778 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -6,34 +6,35 @@ using OtterGui.Raii; using Penumbra.GameData.Files; using Penumbra.String.Classes; +using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private readonly FileEditor< MtrlTab > _materialTab; + private readonly FileEditor _materialTab; - private bool DrawMaterialPanel( MtrlTab tab, bool disabled ) + private bool DrawMaterialPanel(MtrlTab tab, bool disabled) { - DrawMaterialLivePreviewRebind( tab, disabled ); + DrawMaterialLivePreviewRebind(tab, disabled); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - var ret = DrawBackFaceAndTransparency( tab, disabled ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + var ret = DrawBackFaceAndTransparency(tab, disabled); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawMaterialShader( tab, disabled ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ret |= DrawMaterialShader(tab, disabled); - ret |= DrawMaterialTextureChange( tab, disabled ); - ret |= DrawMaterialColorSetChange( tab, disabled ); - ret |= DrawMaterialConstants( tab, disabled ); + ret |= DrawMaterialTextureChange(tab, disabled); + ret |= DrawMaterialColorSetChange(tab, disabled); + ret |= DrawMaterialConstants(tab, disabled); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - DrawOtherMaterialDetails( tab.Mtrl, disabled ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + DrawOtherMaterialDetails(tab.Mtrl, disabled); return !disabled && ret; } - private static void DrawMaterialLivePreviewRebind( MtrlTab tab, bool disabled ) + private static void DrawMaterialLivePreviewRebind(MtrlTab tab, bool disabled) { if (disabled) return; @@ -41,82 +42,74 @@ private static void DrawMaterialLivePreviewRebind( MtrlTab tab, bool disabled ) if (ImGui.Button("Reload live preview")) tab.BindToMaterialInstances(); - if (tab.MaterialPreviewers.Count == 0 && tab.ColorSetPreviewers.Count == 0) - { - ImGui.SameLine(); - - var textColor = ImGui.GetColorU32(ImGuiCol.Text); - var textColorWarning = (textColor & 0xFF000000u) | ((textColor & 0x00FEFEFE) >> 1) | 0x80u; // Half red - - using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning); + if (tab.MaterialPreviewers.Count != 0 || tab.ColorSetPreviewers.Count != 0) + return; - ImGui.TextUnformatted("The current material has not been found on your character. Please check the Import from Screen tab for more information."); - } + ImGui.SameLine(); + using var c = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); + ImGui.TextUnformatted( + "The current material has not been found on your character. Please check the Import from Screen tab for more information."); } - private static bool DrawMaterialTextureChange( MtrlTab tab, bool disabled ) + private static bool DrawMaterialTextureChange(MtrlTab tab, bool disabled) { - if( tab.Textures.Count == 0 ) - { + if (tab.Textures.Count == 0) return false; - } - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - if( !ImGui.CollapsingHeader( "Textures and Samplers", ImGuiTreeNodeFlags.DefaultOpen ) ) - { + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + if (!ImGui.CollapsingHeader("Textures and Samplers", ImGuiTreeNodeFlags.DefaultOpen)) return false; - } var frameHeight = ImGui.GetFrameHeight(); var ret = false; - using var table = ImRaii.Table( "##Textures", 3 ); + using var table = ImRaii.Table("##Textures", 3); - ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, frameHeight ); - ImGui.TableSetupColumn( "Path" , ImGuiTableColumnFlags.WidthStretch ); - ImGui.TableSetupColumn( "Name" , ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * UiHelpers.Scale ); - for( var i = 0; i < tab.Textures.Count; ++i ) + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, frameHeight); + ImGui.TableSetupColumn("Path", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * UiHelpers.Scale); + foreach (var (label, textureI, samplerI, description, monoFont) in tab.Textures) { - var (label, textureI, samplerI, description, monoFont) = tab.Textures[i]; - - using var _ = ImRaii.PushId( samplerI ); - var tmp = tab.Mtrl.Textures[ textureI ].Path; - var unfolded = tab.UnfoldedTextures.Contains( samplerI ); + using var _ = ImRaii.PushId(samplerI); + var tmp = tab.Mtrl.Textures[textureI].Path; + var unfolded = tab.UnfoldedTextures.Contains(samplerI); ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( ( unfolded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight ).ToIconString(), new Vector2( frameHeight ), - "Settings for this texture and the associated sampler", false, true ) ) + if (ImGuiUtil.DrawDisabledButton((unfolded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight).ToIconString(), + new Vector2(frameHeight), + "Settings for this texture and the associated sampler", false, true)) { unfolded = !unfolded; - if( unfolded ) - tab.UnfoldedTextures.Add( samplerI ); + if (unfolded) + tab.UnfoldedTextures.Add(samplerI); else - tab.UnfoldedTextures.Remove( samplerI ); + tab.UnfoldedTextures.Remove(samplerI); } + ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); - if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) - && tmp.Length > 0 - && tmp != tab.Mtrl.Textures[ textureI ].Path ) + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.InputText(string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None) + && tmp.Length > 0 + && tmp != tab.Mtrl.Textures[textureI].Path) { - ret = true; - tab.Mtrl.Textures[ textureI ].Path = tmp; + ret = true; + tab.Mtrl.Textures[textureI].Path = tmp; } ImGui.TableNextColumn(); - using( var font = ImRaii.PushFont( UiBuilder.MonoFont, monoFont ) ) + using (var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont)) { ImGui.AlignTextToFramePadding(); - if( description.Length > 0 ) - ImGuiUtil.LabeledHelpMarker( label, description ); + if (description.Length > 0) + ImGuiUtil.LabeledHelpMarker(label, description); else - ImGui.TextUnformatted( label ); + ImGui.TextUnformatted(label); } - if( unfolded ) + if (unfolded) { ImGui.TableNextColumn(); ImGui.TableNextColumn(); - ret |= DrawMaterialSampler( tab, disabled, textureI, samplerI ); + ret |= DrawMaterialSampler(tab, disabled, textureI, samplerI); ImGui.TableNextColumn(); } } @@ -124,26 +117,27 @@ private static bool DrawMaterialTextureChange( MtrlTab tab, bool disabled ) return ret; } - private static bool DrawBackFaceAndTransparency( MtrlTab tab, bool disabled ) + private static bool DrawBackFaceAndTransparency(MtrlTab tab, bool disabled) { const uint transparencyBit = 0x10; const uint backfaceBit = 0x01; var ret = false; - using var dis = ImRaii.Disabled( disabled ); + using var dis = ImRaii.Disabled(disabled); - var tmp = ( tab.Mtrl.ShaderPackage.Flags & transparencyBit ) != 0; - if( ImGui.Checkbox( "Enable Transparency", ref tmp ) ) + var tmp = (tab.Mtrl.ShaderPackage.Flags & transparencyBit) != 0; + if (ImGui.Checkbox("Enable Transparency", ref tmp)) { - tab.Mtrl.ShaderPackage.Flags = tmp ? tab.Mtrl.ShaderPackage.Flags | transparencyBit : tab.Mtrl.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 = ( tab.Mtrl.ShaderPackage.Flags & backfaceBit ) != 0; - if( ImGui.Checkbox( "Hide Backfaces", ref tmp ) ) + ImGui.SameLine(200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X); + tmp = (tab.Mtrl.ShaderPackage.Flags & backfaceBit) != 0; + if (ImGui.Checkbox("Hide Backfaces", ref tmp)) { tab.Mtrl.ShaderPackage.Flags = tmp ? tab.Mtrl.ShaderPackage.Flags | backfaceBit : tab.Mtrl.ShaderPackage.Flags & ~backfaceBit; ret = true; @@ -153,106 +147,80 @@ private static bool DrawBackFaceAndTransparency( MtrlTab tab, bool disabled ) return ret; } - private static void DrawOtherMaterialDetails( MtrlFile file, bool _ ) + private static void DrawOtherMaterialDetails(MtrlFile file, bool _) { - if( !ImGui.CollapsingHeader( "Further Content" ) ) - { + if (!ImGui.CollapsingHeader("Further Content")) return; - } - using( var sets = ImRaii.TreeNode( "UV Sets", ImGuiTreeNodeFlags.DefaultOpen ) ) + using (var sets = ImRaii.TreeNode("UV Sets", ImGuiTreeNodeFlags.DefaultOpen)) { - if( sets ) - { - foreach( var set in file.UvSets ) - { - ImRaii.TreeNode( $"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - } + if (sets) + foreach (var set in file.UvSets) + ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); } - if( file.AdditionalData.Length <= 0 ) - { + if (file.AdditionalData.Length <= 0) return; - } - using var t = ImRaii.TreeNode( $"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData" ); - if( t ) - { - ImGuiUtil.TextWrapped( string.Join( ' ', file.AdditionalData.Select( c => $"{c:X2}" ) ) ); - } + using var t = ImRaii.TreeNode($"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData"); + if (t) + ImGuiUtil.TextWrapped(string.Join(' ', file.AdditionalData.Select(c => $"{c:X2}"))); } private void DrawMaterialReassignmentTab() { - if( _editor.Files.Mdl.Count == 0 ) - { + if (_editor.Files.Mdl.Count == 0) return; - } - using var tab = ImRaii.TabItem( "Material Reassignment" ); - if( !tab ) - { + using var tab = ImRaii.TabItem("Material Reassignment"); + if (!tab) return; - } ImGui.NewLine(); - MaterialSuffix.Draw( _editor, ImGuiHelpers.ScaledVector2( 175, 0 ) ); + MaterialSuffix.Draw(_editor, ImGuiHelpers.ScaledVector2(175, 0)); ImGui.NewLine(); - using var child = ImRaii.Child( "##mdlFiles", -Vector2.One, true ); - if( !child ) - { + using var child = ImRaii.Child("##mdlFiles", -Vector2.One, true); + if (!child) return; - } - using var table = ImRaii.Table( "##files", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One ); - if( !table ) - { + using var table = ImRaii.Table("##files", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One); + if (!table) return; - } var iconSize = ImGui.GetFrameHeight() * Vector2.One; - foreach( var (info, idx) in _editor.MdlMaterialEditor.ModelFiles.WithIndex() ) + foreach (var (info, idx) in _editor.MdlMaterialEditor.ModelFiles.WithIndex()) { - using var id = ImRaii.PushId( idx ); + using var id = ImRaii.PushId(idx); ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Save.ToIconString(), iconSize, - "Save the changed mdl file.\nUse at own risk!", !info.Changed, true ) ) - { + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), iconSize, + "Save the changed mdl file.\nUse at own risk!", !info.Changed, true)) info.Save(); - } ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Recycle.ToIconString(), iconSize, - "Restore current changes to default.", !info.Changed, true ) ) - { + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), iconSize, + "Restore current changes to default.", !info.Changed, true)) info.Restore(); - } ImGui.TableNextColumn(); - ImGui.TextUnformatted( info.Path.FullName[ ( _mod!.ModPath.FullName.Length + 1 ).. ] ); + ImGui.TextUnformatted(info.Path.FullName[(_mod!.ModPath.FullName.Length + 1)..]); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( 400 * UiHelpers.Scale ); - var tmp = info.CurrentMaterials[ 0 ]; - if( ImGui.InputText( "##0", ref tmp, 64 ) ) - { - info.SetMaterial( tmp, 0 ); - } + ImGui.SetNextItemWidth(400 * UiHelpers.Scale); + var tmp = info.CurrentMaterials[0]; + if (ImGui.InputText("##0", ref tmp, 64)) + info.SetMaterial(tmp, 0); - for( var i = 1; i < info.Count; ++i ) + for (var i = 1; i < info.Count; ++i) { ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( 400 * UiHelpers.Scale ); - tmp = info.CurrentMaterials[ i ]; - if( ImGui.InputText( $"##{i}", ref tmp, 64 ) ) - { - info.SetMaterial( tmp, i ); - } + ImGui.SetNextItemWidth(400 * UiHelpers.Scale); + tmp = info.CurrentMaterials[i]; + if (ImGui.InputText($"##{i}", ref tmp, 64)) + info.SetMaterial(tmp, i); } } } -} \ No newline at end of file +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index b212e791..518566f5 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -5,7 +5,6 @@ using Penumbra.String.Classes; using System.Globalization; using System.Linq; -using Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index 1b159efc..c0868b71 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -20,32 +20,32 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private static readonly ByteString DisassemblyLabel = ByteString.FromSpanUnsafe( "##disassembly"u8, true, true, true ); + private static readonly ByteString DisassemblyLabel = ByteString.FromSpanUnsafe("##disassembly"u8, true, true, true); - private readonly FileEditor< ShpkTab > _shaderPackageTab; + private readonly FileEditor _shaderPackageTab; - private static bool DrawShaderPackagePanel( ShpkTab file, bool disabled ) + private static bool DrawShaderPackagePanel(ShpkTab file, bool disabled) { - DrawShaderPackageSummary( file ); + DrawShaderPackageSummary(file); var ret = false; - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawShaderPackageShaderArray( file, "Vertex Shader", file.Shpk.VertexShaders, disabled ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ret |= DrawShaderPackageShaderArray(file, "Vertex Shader", file.Shpk.VertexShaders, disabled); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawShaderPackageShaderArray( file, "Pixel Shader", file.Shpk.PixelShaders, disabled ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ret |= DrawShaderPackageShaderArray(file, "Pixel Shader", file.Shpk.PixelShaders, disabled); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawShaderPackageMaterialParamLayout( file, disabled ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ret |= DrawShaderPackageMaterialParamLayout(file, disabled); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawShaderPackageResources( file, disabled ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ret |= DrawShaderPackageResources(file, disabled); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - DrawShaderPackageSelection( file ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + DrawShaderPackageSelection(file); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - DrawOtherShaderPackageDetails( file ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + DrawOtherShaderPackageDetails(file); file.FileDialog.Draw(); @@ -54,28 +54,26 @@ private static bool DrawShaderPackagePanel( ShpkTab file, bool disabled ) return !disabled && ret; } - private static void DrawShaderPackageSummary( ShpkTab tab ) + private static void DrawShaderPackageSummary(ShpkTab tab) { - ImGui.TextUnformatted( tab.Header ); - if( !tab.Shpk.Disassembled ) + ImGui.TextUnformatted(tab.Header); + if (!tab.Shpk.Disassembled) { - var textColor = ImGui.GetColorU32( ImGuiCol.Text ); - var textColorWarning = ( textColor & 0xFF000000u ) | ( ( textColor & 0x00FEFEFE ) >> 1 ) | 0x80u; // Half red + var textColor = ImGui.GetColorU32(ImGuiCol.Text); + var textColorWarning = (textColor & 0xFF000000u) | ((textColor & 0x00FEFEFE) >> 1) | 0x80u; // Half red - using var c = ImRaii.PushColor( ImGuiCol.Text, textColorWarning ); + using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning); - ImGui.TextUnformatted( "Your system doesn't support disassembling shaders. Some functionality will be missing." ); + ImGui.TextUnformatted("Your system doesn't support disassembling shaders. Some functionality will be missing."); } } - private static void DrawShaderExportButton( ShpkTab tab, string objectName, Shader shader, int idx ) + private static void DrawShaderExportButton(ShpkTab tab, string objectName, Shader shader, int idx) { - if( !ImGui.Button( $"Export Shader Program Blob ({shader.Blob.Length} bytes)" ) ) - { + if (!ImGui.Button($"Export Shader Program Blob ({shader.Blob.Length} bytes)")) return; - } - var defaultName = objectName[ 0 ] switch + var defaultName = objectName[0] switch { 'V' => $"vs{idx}", 'P' => $"ps{idx}", @@ -83,247 +81,225 @@ private static void DrawShaderExportButton( ShpkTab tab, string objectName, Shad }; var blob = shader.Blob; - tab.FileDialog.OpenSavePicker( $"Export {objectName} #{idx} Program Blob to...", tab.Extension, defaultName, tab.Extension, ( success, name ) => - { - if( !success ) + tab.FileDialog.OpenSavePicker($"Export {objectName} #{idx} Program Blob to...", tab.Extension, defaultName, tab.Extension, + (success, name) => { - return; - } + if (!success) + return; - try - { - File.WriteAllBytes( name, blob ); - } - catch( Exception e ) - { - Penumbra.Chat.NotificationMessage( $"Could not export {defaultName}{tab.Extension} to {name}:\n{e.Message}", "Penumbra Advanced Editing", - NotificationType.Error ); - return; - } + try + { + File.WriteAllBytes(name, blob); + } + catch (Exception e) + { + Penumbra.Chat.NotificationMessage($"Could not export {defaultName}{tab.Extension} to {name}:\n{e.Message}", + "Penumbra Advanced Editing", + NotificationType.Error); + return; + } - Penumbra.Chat.NotificationMessage( $"Shader Program Blob {defaultName}{tab.Extension} exported successfully to {Path.GetFileName( name )}", - "Penumbra Advanced Editing", NotificationType.Success ); - }, null, false ); + Penumbra.Chat.NotificationMessage( + $"Shader Program Blob {defaultName}{tab.Extension} exported successfully to {Path.GetFileName(name)}", + "Penumbra Advanced Editing", NotificationType.Success); + }, null, false); } - private static void DrawShaderImportButton( ShpkTab tab, string objectName, Shader[] shaders, int idx ) + private static void DrawShaderImportButton(ShpkTab tab, string objectName, Shader[] shaders, int idx) { - if( !ImGui.Button( "Replace Shader Program Blob" ) ) - { + if (!ImGui.Button("Replace Shader Program Blob")) return; - } - tab.FileDialog.OpenFilePicker( $"Replace {objectName} #{idx} Program Blob...", "Shader Program Blobs{.o,.cso,.dxbc,.dxil}", ( success, name ) => - { - if( !success ) + tab.FileDialog.OpenFilePicker($"Replace {objectName} #{idx} Program Blob...", "Shader Program Blobs{.o,.cso,.dxbc,.dxil}", + (success, name) => { - return; - } + if (!success) + return; - try - { - shaders[ idx ].Blob = File.ReadAllBytes(name[0] ); - } - catch( Exception e ) - { - Penumbra.Chat.NotificationMessage( $"Could not import {name}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); - return; - } + try + { + shaders[idx].Blob = File.ReadAllBytes(name[0]); + } + catch (Exception e) + { + Penumbra.Chat.NotificationMessage($"Could not import {name}:\n{e.Message}", "Penumbra Advanced Editing", + NotificationType.Error); + return; + } - try - { - shaders[ idx ].UpdateResources( tab.Shpk ); - tab.Shpk.UpdateResources(); - } - catch( Exception e ) - { - tab.Shpk.SetInvalid(); - Penumbra.Chat.NotificationMessage( $"Failed to update resources after importing {name}:\n{e.Message}", "Penumbra Advanced Editing", - NotificationType.Error ); - return; - } + try + { + shaders[idx].UpdateResources(tab.Shpk); + tab.Shpk.UpdateResources(); + } + catch (Exception e) + { + tab.Shpk.SetInvalid(); + Penumbra.Chat.NotificationMessage($"Failed to update resources after importing {name}:\n{e.Message}", + "Penumbra Advanced Editing", + NotificationType.Error); + return; + } - tab.Shpk.SetChanged(); - }, 1, null, false ); + tab.Shpk.SetChanged(); + }, 1, null, false); } - private static unsafe void DrawRawDisassembly( Shader shader ) + private static unsafe void DrawRawDisassembly(Shader shader) { - using var t2 = ImRaii.TreeNode( "Raw Program Disassembly" ); - if( !t2 ) - { + using var t2 = ImRaii.TreeNode("Raw Program Disassembly"); + if (!t2) return; - } - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - var size = new Vector2( ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeight() * 20 ); - ImGuiNative.igInputTextMultiline( DisassemblyLabel.Path, shader.Disassembly!.RawDisassembly.Path, ( uint )shader.Disassembly!.RawDisassembly.Length + 1, size, - ImGuiInputTextFlags.ReadOnly, null, null ); + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + var size = new Vector2(ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeight() * 20); + ImGuiNative.igInputTextMultiline(DisassemblyLabel.Path, shader.Disassembly!.RawDisassembly.Path, + (uint)shader.Disassembly!.RawDisassembly.Length + 1, size, + ImGuiInputTextFlags.ReadOnly, null, null); } - private static bool DrawShaderPackageShaderArray( ShpkTab tab, string objectName, Shader[] shaders, bool disabled ) + private static bool DrawShaderPackageShaderArray(ShpkTab tab, string objectName, Shader[] shaders, bool disabled) { - if( shaders.Length == 0 || !ImGui.CollapsingHeader( $"{objectName}s" ) ) - { + if (shaders.Length == 0 || !ImGui.CollapsingHeader($"{objectName}s")) return false; - } var ret = false; - for( var idx = 0; idx < shaders.Length; ++idx ) + for (var idx = 0; idx < shaders.Length; ++idx) { - var shader = shaders[ idx ]; - using var t = ImRaii.TreeNode( $"{objectName} #{idx}" ); - if( !t ) - { + var shader = shaders[idx]; + using var t = ImRaii.TreeNode($"{objectName} #{idx}"); + if (!t) continue; - } - DrawShaderExportButton( tab, objectName, shader, idx ); - if( !disabled && tab.Shpk.Disassembled ) + DrawShaderExportButton(tab, objectName, shader, idx); + if (!disabled && tab.Shpk.Disassembled) { ImGui.SameLine(); - DrawShaderImportButton( tab, objectName, shaders, idx ); + DrawShaderImportButton(tab, objectName, shaders, idx); } - ret |= DrawShaderPackageResourceArray( "Constant Buffers", "slot", true, shader.Constants, true ); - ret |= DrawShaderPackageResourceArray( "Samplers", "slot", false, shader.Samplers, true ); - ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "slot", true, shader.Uavs, true ); + ret |= DrawShaderPackageResourceArray("Constant Buffers", "slot", true, shader.Constants, true); + ret |= DrawShaderPackageResourceArray("Samplers", "slot", false, shader.Samplers, true); + ret |= DrawShaderPackageResourceArray("Unordered Access Views", "slot", true, shader.Uavs, true); - if( shader.AdditionalHeader.Length > 0 ) + if (shader.AdditionalHeader.Length > 0) { - using var t2 = ImRaii.TreeNode( $"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader" ); - if( t2 ) - { - ImGuiUtil.TextWrapped( string.Join( ' ', shader.AdditionalHeader.Select( c => $"{c:X2}" ) ) ); - } + using var t2 = ImRaii.TreeNode($"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader"); + if (t2) + ImGuiUtil.TextWrapped(string.Join(' ', shader.AdditionalHeader.Select(c => $"{c:X2}"))); } - if( tab.Shpk.Disassembled ) - DrawRawDisassembly( shader ); + if (tab.Shpk.Disassembled) + DrawRawDisassembly(shader); } return ret; } - private static bool DrawShaderPackageResource( string slotLabel, bool withSize, ref Resource resource, bool disabled ) + private static bool DrawShaderPackageResource(string slotLabel, bool withSize, ref Resource resource, bool disabled) { var ret = false; - if( !disabled ) + if (!disabled) { - ImGui.SetNextItemWidth( UiHelpers.Scale * 150.0f ); - if( ImGuiUtil.InputUInt16( $"{char.ToUpper( slotLabel[ 0 ] )}{slotLabel[ 1.. ].ToLower()}", ref resource.Slot, ImGuiInputTextFlags.None ) ) - { + ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); + if (ImGuiUtil.InputUInt16($"{char.ToUpper(slotLabel[0])}{slotLabel[1..].ToLower()}", ref resource.Slot, ImGuiInputTextFlags.None)) ret = true; - } } - if( resource.Used == null ) - { + if (resource.Used == null) return ret; - } - var usedString = UsedComponentString( withSize, resource ); - if( usedString.Length > 0 ) - { - ImRaii.TreeNode( $"Used: {usedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + var usedString = UsedComponentString(withSize, resource); + if (usedString.Length > 0) + ImRaii.TreeNode($"Used: {usedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); else - { - ImRaii.TreeNode( "Unused", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + ImRaii.TreeNode("Unused", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); return ret; } - private static bool DrawShaderPackageResourceArray( string arrayName, string slotLabel, bool withSize, Resource[] resources, bool disabled ) + private static bool DrawShaderPackageResourceArray(string arrayName, string slotLabel, bool withSize, Resource[] resources, bool disabled) { - if( resources.Length == 0 ) - { + if (resources.Length == 0) return false; - } - using var t = ImRaii.TreeNode( arrayName ); - if( !t ) - { + using var t = ImRaii.TreeNode(arrayName); + if (!t) return false; - } var ret = false; - for( var idx = 0; idx < resources.Length; ++idx ) + for (var idx = 0; idx < resources.Length; ++idx) { - ref var buf = ref resources[ idx ]; + ref var buf = ref resources[idx]; var name = $"#{idx}: {buf.Name} (ID: 0x{buf.Id:X8}), {slotLabel}: {buf.Slot}" - + ( withSize ? $", size: {buf.Size} registers###{idx}: {buf.Name} (ID: 0x{buf.Id:X8})" : string.Empty ); - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - using var t2 = ImRaii.TreeNode( name, !disabled || buf.Used != null ? 0 : ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ); + + (withSize ? $", size: {buf.Size} registers###{idx}: {buf.Name} (ID: 0x{buf.Id:X8})" : string.Empty); + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var t2 = ImRaii.TreeNode(name, !disabled || buf.Used != null ? 0 : ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet); font.Dispose(); - if( t2 ) - { - ret |= DrawShaderPackageResource( slotLabel, withSize, ref buf, disabled ); - } + if (t2) + ret |= DrawShaderPackageResource(slotLabel, withSize, ref buf, disabled); } return ret; } - private static bool DrawMaterialParamLayoutHeader( string label ) + private static bool DrawMaterialParamLayoutHeader(string label) { - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); + using var font = ImRaii.PushFont(UiBuilder.MonoFont); var pos = ImGui.GetCursorScreenPos() - + new Vector2( ImGui.CalcTextSize( label ).X + 3 * ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight(), ImGui.GetStyle().FramePadding.Y ); + + new Vector2(ImGui.CalcTextSize(label).X + 3 * ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight(), + ImGui.GetStyle().FramePadding.Y); - var ret = ImGui.CollapsingHeader( label ); - ImGui.GetWindowDrawList().AddText( UiBuilder.DefaultFont, UiBuilder.DefaultFont.FontSize, pos, ImGui.GetColorU32( ImGuiCol.Text ), "Layout" ); + var ret = ImGui.CollapsingHeader(label); + ImGui.GetWindowDrawList() + .AddText(UiBuilder.DefaultFont, UiBuilder.DefaultFont.FontSize, pos, ImGui.GetColorU32(ImGuiCol.Text), "Layout"); return ret; } - private static bool DrawMaterialParamLayoutBufferSize( ShpkFile file, Resource? materialParams ) + private static bool DrawMaterialParamLayoutBufferSize(ShpkFile file, Resource? materialParams) { - var isSizeWellDefined = ( file.MaterialParamsSize & 0xF ) == 0 && ( !materialParams.HasValue || file.MaterialParamsSize == materialParams.Value.Size << 4 ); - if( isSizeWellDefined ) - { + var isSizeWellDefined = (file.MaterialParamsSize & 0xF) == 0 + && (!materialParams.HasValue || file.MaterialParamsSize == materialParams.Value.Size << 4); + if (isSizeWellDefined) return true; - } - ImGui.TextUnformatted( materialParams.HasValue + ImGui.TextUnformatted(materialParams.HasValue ? $"Buffer size mismatch: {file.MaterialParamsSize} bytes ≠ {materialParams.Value.Size} registers ({materialParams.Value.Size << 4} bytes)" - : $"Buffer size mismatch: {file.MaterialParamsSize} bytes, not a multiple of 16" ); + : $"Buffer size mismatch: {file.MaterialParamsSize} bytes, not a multiple of 16"); return false; } - private static bool DrawShaderPackageMaterialMatrix( ShpkTab tab, bool disabled ) + private static bool DrawShaderPackageMaterialMatrix(ShpkTab tab, bool disabled) { - ImGui.TextUnformatted( tab.Shpk.Disassembled + ImGui.TextUnformatted(tab.Shpk.Disassembled ? "Parameter positions (continuations are grayed out, unused values are red):" - : "Parameter positions (continuations are grayed out):" ); + : "Parameter positions (continuations are grayed out):"); - using var table = ImRaii.Table( "##MaterialParamLayout", 5, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); - if( !table ) - { + using var table = ImRaii.Table("##MaterialParamLayout", 5, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) return false; - } - ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, 25 * UiHelpers.Scale ); - ImGui.TableSetupColumn( "x", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale ); - ImGui.TableSetupColumn( "y", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale ); - ImGui.TableSetupColumn( "z", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale ); - ImGui.TableSetupColumn( "w", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale ); + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, 25 * UiHelpers.Scale); + ImGui.TableSetupColumn("x", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); + ImGui.TableSetupColumn("y", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); + ImGui.TableSetupColumn("z", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); + ImGui.TableSetupColumn("w", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); ImGui.TableHeadersRow(); - var textColorStart = ImGui.GetColorU32( ImGuiCol.Text ); - var textColorCont = ( textColorStart & 0x00FFFFFFu ) | ( ( textColorStart & 0xFE000000u ) >> 1 ); // Half opacity - var textColorUnusedStart = ( textColorStart & 0xFF000000u ) | ( ( textColorStart & 0x00FEFEFE ) >> 1 ) | 0x80u; // Half red - var textColorUnusedCont = ( textColorUnusedStart & 0x00FFFFFFu ) | ( ( textColorUnusedStart & 0xFE000000u ) >> 1 ); + var textColorStart = ImGui.GetColorU32(ImGuiCol.Text); + var textColorCont = (textColorStart & 0x00FFFFFFu) | ((textColorStart & 0xFE000000u) >> 1); // Half opacity + var textColorUnusedStart = (textColorStart & 0xFF000000u) | ((textColorStart & 0x00FEFEFE) >> 1) | 0x80u; // Half red + var textColorUnusedCont = (textColorUnusedStart & 0x00FFFFFFu) | ((textColorUnusedStart & 0xFE000000u) >> 1); var ret = false; - for( var i = 0; i < tab.Matrix.GetLength( 0 ); ++i ) + for (var i = 0; i < tab.Matrix.GetLength(0); ++i) { ImGui.TableNextColumn(); - ImGui.TableHeader( $" [{i}]" ); - for( var j = 0; j < 4; ++j ) + ImGui.TableHeader($" [{i}]"); + for (var j = 0; j < 4; ++j) { - var (name, tooltip, idx, colorType) = tab.Matrix[ i, j ]; + var (name, tooltip, idx, colorType) = tab.Matrix[i, j]; var color = colorType switch { ShpkTab.ColorType.Unused => textColorUnusedStart, @@ -332,367 +308,307 @@ private static bool DrawShaderPackageMaterialMatrix( ShpkTab tab, bool disabled ShpkTab.ColorType.Continuation | ShpkTab.ColorType.Used => textColorCont, _ => textColorStart, }; - using var _ = ImRaii.PushId( i * 4 + j ); + using var _ = ImRaii.PushId(i * 4 + j); var deletable = !disabled && idx >= 0; - using( var font = ImRaii.PushFont( UiBuilder.MonoFont, tooltip.Length > 0 ) ) + using (var font = ImRaii.PushFont(UiBuilder.MonoFont, tooltip.Length > 0)) { - using( var c = ImRaii.PushColor( ImGuiCol.Text, color ) ) + using (var c = ImRaii.PushColor(ImGuiCol.Text, color)) { ImGui.TableNextColumn(); - ImGui.Selectable( name ); - if( deletable && ImGui.IsItemClicked( ImGuiMouseButton.Right ) && ImGui.GetIO().KeyCtrl ) + ImGui.Selectable(name); + if (deletable && ImGui.IsItemClicked(ImGuiMouseButton.Right) && ImGui.GetIO().KeyCtrl) { - tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.RemoveItems( idx ); + tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.RemoveItems(idx); ret = true; tab.Update(); } } - ImGuiUtil.HoverTooltip( tooltip ); + ImGuiUtil.HoverTooltip(tooltip); } - if( deletable ) - { - ImGuiUtil.HoverTooltip( "\nControl + Right-Click to remove." ); - } + if (deletable) + ImGuiUtil.HoverTooltip("\nControl + Right-Click to remove."); } } return ret; } - private static void DrawShaderPackageMisalignedParameters( ShpkTab tab ) + private static void DrawShaderPackageMisalignedParameters(ShpkTab tab) { - using var t = ImRaii.TreeNode( "Misaligned / Overflowing Parameters" ); - if( !t ) - { + using var t = ImRaii.TreeNode("Misaligned / Overflowing Parameters"); + if (!t) return; - } - using var _ = ImRaii.PushFont( UiBuilder.MonoFont ); - foreach( var name in tab.MalformedParameters ) - { - ImRaii.TreeNode( name, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + using var _ = ImRaii.PushFont(UiBuilder.MonoFont); + foreach (var name in tab.MalformedParameters) + ImRaii.TreeNode(name, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } - private static void DrawShaderPackageStartCombo( ShpkTab tab ) + private static void DrawShaderPackageStartCombo(ShpkTab tab) { - using var s = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing ); - using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) + using var s = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing); + using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) { - ImGui.SetNextItemWidth( UiHelpers.Scale * 400 ); - using var c = ImRaii.Combo( "##Start", tab.Orphans[ tab.NewMaterialParamStart ].Name ); - if( c ) - { - foreach( var (start, idx) in tab.Orphans.WithIndex() ) + ImGui.SetNextItemWidth(UiHelpers.Scale * 400); + using var c = ImRaii.Combo("##Start", tab.Orphans[tab.NewMaterialParamStart].Name); + if (c) + foreach (var (start, idx) in tab.Orphans.WithIndex()) { - if( ImGui.Selectable( start.Name, idx == tab.NewMaterialParamStart ) ) - { - tab.UpdateOrphanStart( idx ); - } + if (ImGui.Selectable(start.Name, idx == tab.NewMaterialParamStart)) + tab.UpdateOrphanStart(idx); } - } } ImGui.SameLine(); - ImGui.TextUnformatted( "Start" ); + ImGui.TextUnformatted("Start"); } - private static void DrawShaderPackageEndCombo( ShpkTab tab ) + private static void DrawShaderPackageEndCombo(ShpkTab tab) { - using var s = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing ); - using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) + using var s = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing); + using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) { - ImGui.SetNextItemWidth( UiHelpers.Scale * 400 ); - using var c = ImRaii.Combo( "##End", tab.Orphans[ tab.NewMaterialParamEnd ].Name ); - if( c ) + ImGui.SetNextItemWidth(UiHelpers.Scale * 400); + using var c = ImRaii.Combo("##End", tab.Orphans[tab.NewMaterialParamEnd].Name); + if (c) { - var current = tab.Orphans[ tab.NewMaterialParamStart ].Index; - for( var i = tab.NewMaterialParamStart; i < tab.Orphans.Count; ++i ) + var current = tab.Orphans[tab.NewMaterialParamStart].Index; + for (var i = tab.NewMaterialParamStart; i < tab.Orphans.Count; ++i) { - var next = tab.Orphans[ i ]; - if( current++ != next.Index ) - { + var next = tab.Orphans[i]; + if (current++ != next.Index) break; - } - if( ImGui.Selectable( next.Name, i == tab.NewMaterialParamEnd ) ) - { + if (ImGui.Selectable(next.Name, i == tab.NewMaterialParamEnd)) tab.NewMaterialParamEnd = i; - } } } } ImGui.SameLine(); - ImGui.TextUnformatted( "End" ); + ImGui.TextUnformatted("End"); } - private static bool DrawShaderPackageNewParameter( ShpkTab tab ) + private static bool DrawShaderPackageNewParameter(ShpkTab tab) { - if( tab.Orphans.Count == 0 ) - { + if (tab.Orphans.Count == 0) return false; - } - DrawShaderPackageStartCombo( tab ); - DrawShaderPackageEndCombo( tab ); + DrawShaderPackageStartCombo(tab); + DrawShaderPackageEndCombo(tab); - ImGui.SetNextItemWidth( UiHelpers.Scale * 400 ); - if( ImGui.InputText( "Name", ref tab.NewMaterialParamName, 63 ) ) - { - tab.NewMaterialParamId = Crc32.Get( tab.NewMaterialParamName, 0xFFFFFFFFu ); - } + ImGui.SetNextItemWidth(UiHelpers.Scale * 400); + if (ImGui.InputText("Name", ref tab.NewMaterialParamName, 63)) + tab.NewMaterialParamId = Crc32.Get(tab.NewMaterialParamName, 0xFFFFFFFFu); - var tooltip = tab.UsedIds.Contains( tab.NewMaterialParamId ) + var tooltip = tab.UsedIds.Contains(tab.NewMaterialParamId) ? "The ID is already in use. Please choose a different name." : string.Empty; - if( !ImGuiUtil.DrawDisabledButton( $"Add ID 0x{tab.NewMaterialParamId:X8}", new Vector2( 400 * UiHelpers.Scale, ImGui.GetFrameHeight() ), tooltip, - tooltip.Length > 0 ) ) - { + if (!ImGuiUtil.DrawDisabledButton($"Add ID 0x{tab.NewMaterialParamId:X8}", new Vector2(400 * UiHelpers.Scale, ImGui.GetFrameHeight()), + tooltip, + tooltip.Length > 0)) return false; - } - tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.AddItem( new MaterialParam + tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.AddItem(new MaterialParam { Id = tab.NewMaterialParamId, - ByteOffset = ( ushort )( tab.Orphans[ tab.NewMaterialParamStart ].Index << 2 ), - ByteSize = ( ushort )( ( tab.NewMaterialParamEnd - tab.NewMaterialParamStart + 1 ) << 2 ), - } ); + ByteOffset = (ushort)(tab.Orphans[tab.NewMaterialParamStart].Index << 2), + ByteSize = (ushort)((tab.NewMaterialParamEnd - tab.NewMaterialParamStart + 1) << 2), + }); tab.Update(); return true; } - private static bool DrawShaderPackageMaterialParamLayout( ShpkTab tab, bool disabled ) + private static bool DrawShaderPackageMaterialParamLayout(ShpkTab tab, bool disabled) { var ret = false; - var materialParams = tab.Shpk.GetConstantById( MaterialParamsConstantId ); - if( !DrawMaterialParamLayoutHeader( materialParams?.Name ?? "Material Parameter" ) ) - { + var materialParams = tab.Shpk.GetConstantById(MaterialParamsConstantId); + if (!DrawMaterialParamLayoutHeader(materialParams?.Name ?? "Material Parameter")) return false; - } - var sizeWellDefined = DrawMaterialParamLayoutBufferSize( tab.Shpk, materialParams ); + var sizeWellDefined = DrawMaterialParamLayoutBufferSize(tab.Shpk, materialParams); - ret |= DrawShaderPackageMaterialMatrix( tab, disabled ); + ret |= DrawShaderPackageMaterialMatrix(tab, disabled); - if( tab.MalformedParameters.Count > 0 ) - { - DrawShaderPackageMisalignedParameters( tab ); - } - else if( !disabled && sizeWellDefined ) - { - ret |= DrawShaderPackageNewParameter( tab ); - } + if (tab.MalformedParameters.Count > 0) + DrawShaderPackageMisalignedParameters(tab); + else if (!disabled && sizeWellDefined) + ret |= DrawShaderPackageNewParameter(tab); return ret; } - private static bool DrawShaderPackageResources( ShpkTab tab, bool disabled ) + private static bool DrawShaderPackageResources(ShpkTab tab, bool disabled) { var ret = false; - if( !ImGui.CollapsingHeader( "Shader Resources" ) ) - { + if (!ImGui.CollapsingHeader("Shader Resources")) return false; - } - ret |= DrawShaderPackageResourceArray( "Constant Buffers", "type", true, tab.Shpk.Constants, disabled ); - ret |= DrawShaderPackageResourceArray( "Samplers", "type", false, tab.Shpk.Samplers, disabled ); - ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "type", false, tab.Shpk.Uavs, disabled ); + ret |= DrawShaderPackageResourceArray("Constant Buffers", "type", true, tab.Shpk.Constants, disabled); + ret |= DrawShaderPackageResourceArray("Samplers", "type", false, tab.Shpk.Samplers, disabled); + ret |= DrawShaderPackageResourceArray("Unordered Access Views", "type", false, tab.Shpk.Uavs, disabled); return ret; } - private static void DrawKeyArray( string arrayName, bool withId, IReadOnlyCollection< Key > keys ) + private static void DrawKeyArray(string arrayName, bool withId, IReadOnlyCollection keys) { - if( keys.Count == 0 ) - { + if (keys.Count == 0) return; - } - using var t = ImRaii.TreeNode( arrayName ); - if( !t ) - { + using var t = ImRaii.TreeNode(arrayName); + if (!t) return; - } - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - foreach( var (key, idx) in keys.WithIndex() ) + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + foreach (var (key, idx) in keys.WithIndex()) { - using var t2 = ImRaii.TreeNode( withId ? $"#{idx}: ID: 0x{key.Id:X8}" : $"#{idx}" ); - if( t2 ) + using var t2 = ImRaii.TreeNode(withId ? $"#{idx}: ID: 0x{key.Id:X8}" : $"#{idx}"); + if (t2) { - ImRaii.TreeNode( $"Default Value: 0x{key.DefaultValue:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - ImRaii.TreeNode( $"Known Values: {string.Join( ", ", Array.ConvertAll( key.Values, value => $"0x{value:X8}" ) )}", - ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); + ImRaii.TreeNode($"Default Value: 0x{key.DefaultValue:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImRaii.TreeNode($"Known Values: {string.Join(", ", Array.ConvertAll(key.Values, value => $"0x{value:X8}"))}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } } } - private static void DrawShaderPackageNodes( ShpkTab tab ) + private static void DrawShaderPackageNodes(ShpkTab tab) { - if( tab.Shpk.Nodes.Length <= 0 ) - { + if (tab.Shpk.Nodes.Length <= 0) return; - } - using var t = ImRaii.TreeNode( $"Nodes ({tab.Shpk.Nodes.Length})###Nodes" ); - if( !t ) - { + using var t = ImRaii.TreeNode($"Nodes ({tab.Shpk.Nodes.Length})###Nodes"); + if (!t) return; - } - foreach( var (node, idx) in tab.Shpk.Nodes.WithIndex() ) + foreach (var (node, idx) in tab.Shpk.Nodes.WithIndex()) { - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - using var t2 = ImRaii.TreeNode( $"#{idx:D4}: Selector: 0x{node.Selector:X8}" ); - if( !t2 ) - { + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var t2 = ImRaii.TreeNode($"#{idx:D4}: Selector: 0x{node.Selector:X8}"); + if (!t2) continue; - } - foreach( var (key, keyIdx) in node.SystemKeys.WithIndex() ) - { - ImRaii.TreeNode( $"System Key 0x{tab.Shpk.SystemKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + foreach (var (key, keyIdx) in node.SystemKeys.WithIndex()) + ImRaii.TreeNode($"System Key 0x{tab.Shpk.SystemKeys[keyIdx].Id:X8} = 0x{key:X8}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); - foreach( var (key, keyIdx) in node.SceneKeys.WithIndex() ) - { - ImRaii.TreeNode( $"Scene Key 0x{tab.Shpk.SceneKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + foreach (var (key, keyIdx) in node.SceneKeys.WithIndex()) + ImRaii.TreeNode($"Scene Key 0x{tab.Shpk.SceneKeys[keyIdx].Id:X8} = 0x{key:X8}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); - foreach( var (key, keyIdx) in node.MaterialKeys.WithIndex() ) - { - ImRaii.TreeNode( $"Material Key 0x{tab.Shpk.MaterialKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + foreach (var (key, keyIdx) in node.MaterialKeys.WithIndex()) + ImRaii.TreeNode($"Material Key 0x{tab.Shpk.MaterialKeys[keyIdx].Id:X8} = 0x{key:X8}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); - foreach( var (key, keyIdx) in node.SubViewKeys.WithIndex() ) - { - ImRaii.TreeNode( $"Sub-View Key #{keyIdx} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + foreach (var (key, keyIdx) in node.SubViewKeys.WithIndex()) + ImRaii.TreeNode($"Sub-View Key #{keyIdx} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); - ImRaii.TreeNode( $"Pass Indices: {string.Join( ' ', node.PassIndices.Select( c => $"{c:X2}" ) )}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - foreach( var (pass, passIdx) in node.Passes.WithIndex() ) + ImRaii.TreeNode($"Pass Indices: {string.Join(' ', node.PassIndices.Select(c => $"{c:X2}"))}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + foreach (var (pass, passIdx) in node.Passes.WithIndex()) { - ImRaii.TreeNode( $"Pass #{passIdx}: ID: 0x{pass.Id:X8}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", - ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ) - .Dispose(); + ImRaii.TreeNode($"Pass #{passIdx}: ID: 0x{pass.Id:X8}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet) + .Dispose(); } } } - private static void DrawShaderPackageSelection( ShpkTab tab ) + private static void DrawShaderPackageSelection(ShpkTab tab) { - if( !ImGui.CollapsingHeader( "Shader Selection" ) ) - { + if (!ImGui.CollapsingHeader("Shader Selection")) return; - } - DrawKeyArray( "System Keys", true, tab.Shpk.SystemKeys ); - DrawKeyArray( "Scene Keys", true, tab.Shpk.SceneKeys ); - DrawKeyArray( "Material Keys", true, tab.Shpk.MaterialKeys ); - DrawKeyArray( "Sub-View Keys", false, tab.Shpk.SubViewKeys ); + DrawKeyArray("System Keys", true, tab.Shpk.SystemKeys); + DrawKeyArray("Scene Keys", true, tab.Shpk.SceneKeys); + DrawKeyArray("Material Keys", true, tab.Shpk.MaterialKeys); + DrawKeyArray("Sub-View Keys", false, tab.Shpk.SubViewKeys); - DrawShaderPackageNodes( tab ); - using var t = ImRaii.TreeNode( $"Node Selectors ({tab.Shpk.NodeSelectors.Count})###NodeSelectors" ); - if( t ) + DrawShaderPackageNodes(tab); + using var t = ImRaii.TreeNode($"Node Selectors ({tab.Shpk.NodeSelectors.Count})###NodeSelectors"); + if (t) { - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - foreach( var selector in tab.Shpk.NodeSelectors ) - { - ImRaii.TreeNode( $"#{selector.Value:D4}: Selector: 0x{selector.Key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + foreach (var selector in tab.Shpk.NodeSelectors) + ImRaii.TreeNode($"#{selector.Value:D4}: Selector: 0x{selector.Key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet) + .Dispose(); } } - private static void DrawOtherShaderPackageDetails( ShpkTab tab ) + private static void DrawOtherShaderPackageDetails(ShpkTab tab) { - if( !ImGui.CollapsingHeader( "Further Content" ) ) - { + if (!ImGui.CollapsingHeader("Further Content")) return; - } - ImRaii.TreeNode( $"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); + ImRaii.TreeNode($"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); - if( tab.Shpk.AdditionalData.Length > 0 ) + if (tab.Shpk.AdditionalData.Length > 0) { - using var t = ImRaii.TreeNode( $"Additional Data (Size: {tab.Shpk.AdditionalData.Length})###AdditionalData" ); - if( t ) - { - ImGuiUtil.TextWrapped( string.Join( ' ', tab.Shpk.AdditionalData.Select( c => $"{c:X2}" ) ) ); - } + using var t = ImRaii.TreeNode($"Additional Data (Size: {tab.Shpk.AdditionalData.Length})###AdditionalData"); + if (t) + ImGuiUtil.TextWrapped(string.Join(' ', tab.Shpk.AdditionalData.Select(c => $"{c:X2}"))); } } - private static string UsedComponentString( bool withSize, in Resource resource ) + private static string UsedComponentString(bool withSize, in Resource resource) { - var sb = new StringBuilder( 256 ); - if( withSize ) + var sb = new StringBuilder(256); + if (withSize) { - foreach( var (components, i) in ( resource.Used ?? Array.Empty< DisassembledShader.VectorComponents >() ).WithIndex() ) + foreach (var (components, i) in (resource.Used ?? Array.Empty()).WithIndex()) { - switch( components ) + switch (components) { case 0: break; case DisassembledShader.VectorComponents.All: - sb.Append( $"[{i}], " ); + sb.Append($"[{i}], "); break; default: - sb.Append( $"[{i}]." ); - foreach( var c in components.ToString().Where( char.IsUpper ) ) - { - sb.Append( char.ToLower( c ) ); - } + sb.Append($"[{i}]."); + foreach (var c in components.ToString().Where(char.IsUpper)) + sb.Append(char.ToLower(c)); - sb.Append( ", " ); + sb.Append(", "); break; } } - switch( resource.UsedDynamically ?? 0 ) + switch (resource.UsedDynamically ?? 0) { case 0: break; case DisassembledShader.VectorComponents.All: - sb.Append( "[*], " ); + sb.Append("[*], "); break; default: - sb.Append( "[*]." ); - foreach( var c in resource.UsedDynamically!.Value.ToString().Where( char.IsUpper ) ) - { - sb.Append( char.ToLower( c ) ); - } + sb.Append("[*]."); + foreach (var c in resource.UsedDynamically!.Value.ToString().Where(char.IsUpper)) + sb.Append(char.ToLower(c)); - sb.Append( ", " ); + sb.Append(", "); break; } } else { - var components = ( resource.Used is { Length: > 0 } ? resource.Used[ 0 ] : 0 ) | ( resource.UsedDynamically ?? 0 ); - if( ( components & DisassembledShader.VectorComponents.X ) != 0 ) - { - sb.Append( "Red, " ); - } + var components = (resource.Used is { Length: > 0 } ? resource.Used[0] : 0) | (resource.UsedDynamically ?? 0); + if ((components & DisassembledShader.VectorComponents.X) != 0) + sb.Append("Red, "); - if( ( components & DisassembledShader.VectorComponents.Y ) != 0 ) - { - sb.Append( "Green, " ); - } + if ((components & DisassembledShader.VectorComponents.Y) != 0) + sb.Append("Green, "); - if( ( components & DisassembledShader.VectorComponents.Z ) != 0 ) - { - sb.Append( "Blue, " ); - } + if ((components & DisassembledShader.VectorComponents.Z) != 0) + sb.Append("Blue, "); - if( ( components & DisassembledShader.VectorComponents.W ) != 0 ) - { - sb.Append( "Alpha, " ); - } + if ((components & DisassembledShader.VectorComponents.W) != 0) + sb.Append("Alpha, "); } - return sb.Length == 0 ? string.Empty : sb.ToString( 0, sb.Length - 2 ); + return sb.Length == 0 ? string.Empty : sb.ToString(0, sb.Length - 2); } -} \ No newline at end of file +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index e40a7915..e90c148e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -533,14 +533,20 @@ private HashSet FindPathsStartingWith(ByteString prefix) var ret = new HashSet(); foreach (var path in _activeCollections.Current.ResolvedFiles.Keys) + { if (path.Path.StartsWith(prefix)) ret.Add(path); + } if (_mod != null) foreach (var option in _mod.Groups.SelectMany(g => g).Append(_mod.Default)) + { foreach (var path in option.Files.Keys) + { if (path.Path.StartsWith(prefix)) ret.Add(path); + } + } return ret; } From a768b039a8a7c0f98e54dc0872bca2615936aaea Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 31 Aug 2023 18:25:29 +0200 Subject: [PATCH 13/14] Restructure Live Preview. --- .../MaterialPreview/LiveColorSetPreviewer.cs | 131 +++++ .../MaterialPreview/LiveMaterialPreviewer.cs | 149 ++++++ .../LiveMaterialPreviewerBase.cs | 70 +++ .../Interop/MaterialPreview/MaterialInfo.cs | 120 +++++ .../ModEditWindow.Materials.LivePreview.cs | 483 ------------------ .../ModEditWindow.Materials.MtrlTab.cs | 43 +- 6 files changed, 478 insertions(+), 518 deletions(-) create mode 100644 Penumbra/Interop/MaterialPreview/LiveColorSetPreviewer.cs create mode 100644 Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs create mode 100644 Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs create mode 100644 Penumbra/Interop/MaterialPreview/MaterialInfo.cs delete mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs diff --git a/Penumbra/Interop/MaterialPreview/LiveColorSetPreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorSetPreviewer.cs new file mode 100644 index 00000000..18afa949 --- /dev/null +++ b/Penumbra/Interop/MaterialPreview/LiveColorSetPreviewer.cs @@ -0,0 +1,131 @@ +using System; +using System.Threading; +using Dalamud.Game; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using Penumbra.GameData.Files; + +namespace Penumbra.Interop.MaterialPreview; + +public 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, MaterialInfo materialInfo) + : base(objects, materialInfo) + { + _framework = framework; + + var mtrlHandle = Material->MaterialResourceHandle; + if (mtrlHandle == null) + throw new InvalidOperationException("Material doesn't have a resource handle"); + + var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures; + if (colorSetTextures == null) + throw new InvalidOperationException("Draw object doesn't have color set textures"); + + _colorSetTexture = colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.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 Clear(bool disposing, bool reset) + { + _framework.Update -= OnFrameworkUpdate; + + base.Clear(disposing, reset); + + if (reset) + { + var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)_originalColorSetTexture); + if (oldTexture != null) + 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); + if (oldTexture != null) + Structs.TextureUtility.DecRef(oldTexture); + } + else + { + Structs.TextureUtility.DecRef(newTexture); + } + } + + protected override bool IsStillValid() + { + if (!base.IsStillValid()) + return false; + + var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures; + if (colorSetTextures == null) + return false; + + if (_colorSetTexture != colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot)) + return false; + + return true; + } +} diff --git a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs new file mode 100644 index 00000000..1b280b20 --- /dev/null +++ b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs @@ -0,0 +1,149 @@ +using System; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; + +namespace Penumbra.Interop.MaterialPreview; + +public 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, MaterialInfo materialInfo) + : base(objects, materialInfo) + { + 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 Clear(bool disposing, bool reset) + { + base.Clear(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 constantBuffer = ((Structs.Material*)Material)->MaterialParameter; + if (constantBuffer == null) + return; + + if (!constantBuffer->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; + } +} diff --git a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs new file mode 100644 index 00000000..88369725 --- /dev/null +++ b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs @@ -0,0 +1,70 @@ +using System; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; + +namespace Penumbra.Interop.MaterialPreview; + +public abstract unsafe class LiveMaterialPreviewerBase : IDisposable +{ + private readonly IObjectTable _objects; + + public readonly MaterialInfo MaterialInfo; + public readonly CharacterBase* DrawObject; + protected readonly Material* Material; + + protected bool Valid; + + public LiveMaterialPreviewerBase(IObjectTable objects, MaterialInfo materialInfo) + { + _objects = objects; + + MaterialInfo = materialInfo; + var gameObject = MaterialInfo.GetCharacter(objects); + if (gameObject == nint.Zero) + throw new InvalidOperationException("Cannot retrieve game object."); + + DrawObject = (CharacterBase*)MaterialInfo.GetDrawObject(gameObject); + if (DrawObject == null) + throw new InvalidOperationException("Cannot retrieve draw object."); + + Material = MaterialInfo.GetDrawObjectMaterial(DrawObject); + if (Material == null) + throw new InvalidOperationException("Cannot retrieve material."); + + Valid = true; + } + + public void Dispose() + { + if (Valid) + Clear(true, IsStillValid()); + } + + public bool CheckValidity() + { + if (Valid && !IsStillValid()) + Clear(false, false); + return Valid; + } + + protected virtual void Clear(bool disposing, bool reset) + { + Valid = false; + } + + protected virtual bool IsStillValid() + { + var gameObject = MaterialInfo.GetCharacter(_objects); + if (gameObject == nint.Zero) + return false; + + if ((nint)DrawObject != MaterialInfo.GetDrawObject(gameObject)) + return false; + + if (Material != MaterialInfo.GetDrawObjectMaterial(DrawObject)) + return false; + + return true; + } +} diff --git a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs new file mode 100644 index 00000000..f1c9c10e --- /dev/null +++ b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Penumbra.Interop.ResourceTree; +using Penumbra.String; + +namespace Penumbra.Interop.MaterialPreview; + +public enum DrawObjectType +{ + PlayerCharacter, + PlayerMainhand, + PlayerOffhand, + PlayerVfx, + MinionCharacter, + MinionUnk1, + MinionUnk2, + MinionUnk3, +}; + +public readonly record struct MaterialInfo(DrawObjectType Type, int ModelSlot, int MaterialSlot) +{ + public nint GetCharacter(IObjectTable objects) + => GetCharacter(Type, objects); + + public static nint GetCharacter(DrawObjectType type, IObjectTable objects) + => type switch + { + DrawObjectType.PlayerCharacter => objects.GetObjectAddress(0), + DrawObjectType.PlayerMainhand => objects.GetObjectAddress(0), + DrawObjectType.PlayerOffhand => objects.GetObjectAddress(0), + DrawObjectType.PlayerVfx => objects.GetObjectAddress(0), + DrawObjectType.MinionCharacter => objects.GetObjectAddress(1), + DrawObjectType.MinionUnk1 => objects.GetObjectAddress(1), + DrawObjectType.MinionUnk2 => objects.GetObjectAddress(1), + DrawObjectType.MinionUnk3 => objects.GetObjectAddress(1), + _ => nint.Zero, + }; + + public nint GetDrawObject(nint address) + => GetDrawObject(Type, address); + + public static nint GetDrawObject(DrawObjectType type, IObjectTable objects) + => GetDrawObject(type, GetCharacter(type, objects)); + + public static unsafe nint GetDrawObject(DrawObjectType type, nint address) + { + var gameObject = (Character*)address; + if (gameObject == null) + return nint.Zero; + + return type switch + { + DrawObjectType.PlayerCharacter => (nint)gameObject->GameObject.GetDrawObject(), + DrawObjectType.PlayerMainhand => *((nint*)&gameObject->DrawData.MainHand + 1), + DrawObjectType.PlayerOffhand => *((nint*)&gameObject->DrawData.OffHand + 1), + DrawObjectType.PlayerVfx => *((nint*)&gameObject->DrawData.UnkF0 + 1), + DrawObjectType.MinionCharacter => (nint)gameObject->GameObject.GetDrawObject(), + DrawObjectType.MinionUnk1 => *((nint*)&gameObject->DrawData.MainHand + 1), + DrawObjectType.MinionUnk2 => *((nint*)&gameObject->DrawData.OffHand + 1), + DrawObjectType.MinionUnk3 => *((nint*)&gameObject->DrawData.UnkF0 + 1), + _ => nint.Zero, + }; + } + + public unsafe Material* GetDrawObjectMaterial(CharacterBase* drawObject) + { + 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]; + } + + public static unsafe List FindMaterials(IObjectTable objects, string materialPath) + { + var needle = ByteString.FromString(materialPath.Replace('/', '\\'), out var m, true) ? m : ByteString.Empty; + + var result = new List(Enum.GetValues().Length); + foreach (var type in Enum.GetValues()) + { + var drawObject = (CharacterBase*)GetDrawObject(type, objects); + if (drawObject == null) + continue; + + 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; + var path = ResolveContext.GetResourceHandlePath((Structs.ResourceHandle*)mtrlHandle); + if (path == needle) + result.Add(new MaterialInfo(type, i, j)); + } + } + } + + return result; + } +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs deleted file mode 100644 index d2ce8796..00000000 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs +++ /dev/null @@ -1,483 +0,0 @@ -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 Penumbra.String; -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, ByteString 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; - var path = ResolveContext.GetResourceHandlePath((Structs.ResourceHandle*)mtrlHandle); - if (path == materialPath) - result.Add((subActorType, childObjectIndex, i, j)); - } - } - } - - var result = new List<(int, int, int, int)>(); - - if (drawObject == null) - return result; - - var path = ByteString.FromString(materialPath.Replace('/', '\\'), out var m, true) ? m : ByteString.Empty; - CollectMaterials(result, subActorType, -1, drawObject, path); - - var firstChildObject = (CharacterBase*)drawObject->DrawObject.Object.ChildObject; - if (firstChildObject != null) - { - var childObject = firstChildObject; - var childObjectIndex = 0; - do - { - CollectMaterials(result, subActorType, childObjectIndex, childObject, path); - - 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; - - public 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 = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures; - 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); - if (oldTexture != null) - 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); - if (oldTexture != null) - Structs.TextureUtility.DecRef(oldTexture); - } - else - Structs.TextureUtility.DecRef(newTexture); - } - - protected override bool IsStillValid() - { - if (!base.IsStillValid()) - return false; - - var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures; - 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 12f7acd7..6bb8b8c8 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -16,6 +16,7 @@ using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.GameData.Structs; +using Penumbra.Interop.MaterialPreview; using Penumbra.String; using Penumbra.String.Classes; using static Penumbra.GameData.Files.ShpkFile; @@ -460,46 +461,19 @@ public unsafe void BindToMaterialInstances() { UnbindFromMaterialInstances(); - var localPlayer = LocalPlayer(_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; - drawObjects[1] = *((CharacterBase**)&localPlayer->DrawData.MainHand + 1); - drawObjects[2] = *((CharacterBase**)&localPlayer->DrawData.OffHand + 1); - drawObjects[3] = *((CharacterBase**)&localPlayer->DrawData.UnkF0 + 1); - 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 instances = MaterialInfo.FindMaterials(_edit._dalamud.Objects, FilePath); var foundMaterials = new HashSet(); - foreach (var (subActorType, childObjectIndex, modelSlot, materialSlot) in instances) + foreach (var materialInfo in instances) { - var material = GetDrawObjectMaterial(drawObjects[subActorType + 1], modelSlot, materialSlot); + var drawObject = (CharacterBase*)MaterialInfo.GetDrawObject(materialInfo.Type, _edit._dalamud.Objects); + var material = materialInfo.GetDrawObjectMaterial(drawObject); if (foundMaterials.Contains((nint)material)) continue; try { - MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._dalamud.Objects, subActorType, childObjectIndex, modelSlot, - materialSlot)); + MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._dalamud.Objects, materialInfo)); foundMaterials.Add((nint)material); } catch (InvalidOperationException) @@ -515,12 +489,11 @@ public unsafe void BindToMaterialInstances() if (!colorSet.HasValue) return; - foreach (var (subActorType, childObjectIndex, modelSlot, materialSlot) in instances) + foreach (var materialInfo in instances) { try { - ColorSetPreviewers.Add(new LiveColorSetPreviewer(_edit._dalamud.Objects, _edit._dalamud.Framework, subActorType, - childObjectIndex, modelSlot, materialSlot)); + ColorSetPreviewers.Add(new LiveColorSetPreviewer(_edit._dalamud.Objects, _edit._dalamud.Framework, materialInfo)); } catch (InvalidOperationException) { From 616a4635d197d1e6eeb944c5bc892f1778722e38 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 31 Aug 2023 18:32:18 +0200 Subject: [PATCH 14/14] Fix slash direction in material path. --- Penumbra/Interop/MaterialPreview/MaterialInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs index f1c9c10e..0146cf6f 100644 --- a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs +++ b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs @@ -86,7 +86,7 @@ public static unsafe nint GetDrawObject(DrawObjectType type, nint address) public static unsafe List FindMaterials(IObjectTable objects, string materialPath) { - var needle = ByteString.FromString(materialPath.Replace('/', '\\'), out var m, true) ? m : ByteString.Empty; + var needle = ByteString.FromString(materialPath.Replace('\\', '/'), out var m, true) ? m : ByteString.Empty; var result = new List(Enum.GetValues().Length); foreach (var type in Enum.GetValues())