From 069b28272bcec59d03fc84e89850e7acf279e180 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:09:20 +0200 Subject: [PATCH 01/12] Add TextureArraySlicer --- .../Interop/Services/TextureArraySlicer.cs | 119 ++++++++++++++++++ Penumbra/Penumbra.csproj | 8 ++ Penumbra/UI/WindowSystem.cs | 31 +++-- 3 files changed, 145 insertions(+), 13 deletions(-) create mode 100644 Penumbra/Interop/Services/TextureArraySlicer.cs diff --git a/Penumbra/Interop/Services/TextureArraySlicer.cs b/Penumbra/Interop/Services/TextureArraySlicer.cs new file mode 100644 index 00000000..c934ac2b --- /dev/null +++ b/Penumbra/Interop/Services/TextureArraySlicer.cs @@ -0,0 +1,119 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using OtterGui.Services; +using SharpDX.Direct3D; +using SharpDX.Direct3D11; + +namespace Penumbra.Interop.Services; + +/// +/// Creates ImGui handles over slices of array textures, and manages their lifetime. +/// +public sealed unsafe class TextureArraySlicer : IUiService, IDisposable +{ + private const uint InitialTimeToLive = 2; + + private readonly Dictionary<(nint XivTexture, byte SliceIndex), SliceState> _activeSlices = []; + private readonly HashSet<(nint XivTexture, byte SliceIndex)> _expiredKeys = []; + + /// Caching this across frames will cause a crash to desktop. + public nint GetImGuiHandle(Texture* texture, byte sliceIndex) + { + if (texture == null) + throw new ArgumentNullException(nameof(texture)); + if (sliceIndex >= texture->ArraySize) + throw new ArgumentOutOfRangeException(nameof(sliceIndex), $"Slice index ({sliceIndex}) is greater than or equal to the texture array size ({texture->ArraySize})"); + if (_activeSlices.TryGetValue(((nint)texture, sliceIndex), out var state)) + { + state.Refresh(); + return (nint)state.ShaderResourceView; + } + var srv = (ShaderResourceView)(nint)texture->D3D11ShaderResourceView; + var description = srv.Description; + switch (description.Dimension) + { + case ShaderResourceViewDimension.Texture1D: + case ShaderResourceViewDimension.Texture2D: + case ShaderResourceViewDimension.Texture2DMultisampled: + case ShaderResourceViewDimension.Texture3D: + case ShaderResourceViewDimension.TextureCube: + // This function treats these as single-slice arrays. + // As per the range check above, the only valid slice (i. e. 0) has been requested, therefore there is nothing to do. + break; + case ShaderResourceViewDimension.Texture1DArray: + description.Texture1DArray.FirstArraySlice = sliceIndex; + description.Texture2DArray.ArraySize = 1; + break; + case ShaderResourceViewDimension.Texture2DArray: + description.Texture2DArray.FirstArraySlice = sliceIndex; + description.Texture2DArray.ArraySize = 1; + break; + case ShaderResourceViewDimension.Texture2DMultisampledArray: + description.Texture2DMSArray.FirstArraySlice = sliceIndex; + description.Texture2DMSArray.ArraySize = 1; + break; + case ShaderResourceViewDimension.TextureCubeArray: + description.TextureCubeArray.First2DArrayFace = sliceIndex * 6; + description.TextureCubeArray.CubeCount = 1; + break; + default: + throw new NotSupportedException($"{nameof(TextureArraySlicer)} does not support dimension {description.Dimension}"); + } + state = new SliceState(new ShaderResourceView(srv.Device, srv.Resource, description)); + _activeSlices.Add(((nint)texture, sliceIndex), state); + return (nint)state.ShaderResourceView; + } + + public void Tick() + { + try + { + foreach (var (key, slice) in _activeSlices) + { + if (!slice.Tick()) + _expiredKeys.Add(key); + } + foreach (var key in _expiredKeys) + { + _activeSlices.Remove(key); + } + } + finally + { + _expiredKeys.Clear(); + } + } + + public void Dispose() + { + foreach (var slice in _activeSlices.Values) + { + slice.Dispose(); + } + } + + private sealed class SliceState(ShaderResourceView shaderResourceView) : IDisposable + { + public readonly ShaderResourceView ShaderResourceView = shaderResourceView; + + private uint _timeToLive = InitialTimeToLive; + + public void Refresh() + { + _timeToLive = InitialTimeToLive; + } + + public bool Tick() + { + if (unchecked(_timeToLive--) > 0) + return true; + + ShaderResourceView.Dispose(); + return false; + } + + public void Dispose() + { + ShaderResourceView.Dispose(); + } + } +} diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 24ffe469..8e143e3c 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -72,6 +72,14 @@ $(DalamudLibPath)Iced.dll False + + $(DalamudLibPath)SharpDX.dll + False + + + $(DalamudLibPath)SharpDX.Direct3D11.dll + False + lib\OtterTex.dll diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs index 6d382ad4..575a381f 100644 --- a/Penumbra/UI/WindowSystem.cs +++ b/Penumbra/UI/WindowSystem.cs @@ -2,6 +2,7 @@ using Dalamud.Interface; using Dalamud.Interface.Windowing; using Dalamud.Plugin; using OtterGui.Services; +using Penumbra.Interop.Services; using Penumbra.UI.AdvancedWindow; using Penumbra.UI.Knowledge; using Penumbra.UI.Tabs.Debug; @@ -10,23 +11,25 @@ namespace Penumbra.UI; public class PenumbraWindowSystem : IDisposable, IUiService { - private readonly IUiBuilder _uiBuilder; - private readonly WindowSystem _windowSystem; - private readonly FileDialogService _fileDialog; - public readonly ConfigWindow Window; - public readonly PenumbraChangelog Changelog; - public readonly KnowledgeWindow KnowledgeWindow; + private readonly IUiBuilder _uiBuilder; + private readonly WindowSystem _windowSystem; + private readonly FileDialogService _fileDialog; + private readonly TextureArraySlicer _textureArraySlicer; + public readonly ConfigWindow Window; + public readonly PenumbraChangelog Changelog; + public readonly KnowledgeWindow KnowledgeWindow; public PenumbraWindowSystem(IDalamudPluginInterface pi, Configuration config, PenumbraChangelog changelog, ConfigWindow window, LaunchButton _, ModEditWindow editWindow, FileDialogService fileDialog, ImportPopup importPopup, DebugTab debugTab, - KnowledgeWindow knowledgeWindow) + KnowledgeWindow knowledgeWindow, TextureArraySlicer textureArraySlicer) { - _uiBuilder = pi.UiBuilder; - _fileDialog = fileDialog; - KnowledgeWindow = knowledgeWindow; - Changelog = changelog; - Window = window; - _windowSystem = new WindowSystem("Penumbra"); + _uiBuilder = pi.UiBuilder; + _fileDialog = fileDialog; + _textureArraySlicer = textureArraySlicer; + KnowledgeWindow = knowledgeWindow; + Changelog = changelog; + Window = window; + _windowSystem = new WindowSystem("Penumbra"); _windowSystem.AddWindow(changelog.Changelog); _windowSystem.AddWindow(window); _windowSystem.AddWindow(editWindow); @@ -37,6 +40,7 @@ public class PenumbraWindowSystem : IDisposable, IUiService _uiBuilder.OpenConfigUi += Window.OpenSettings; _uiBuilder.Draw += _windowSystem.Draw; _uiBuilder.Draw += _fileDialog.Draw; + _uiBuilder.Draw += _textureArraySlicer.Tick; _uiBuilder.DisableGposeUiHide = !config.HideUiInGPose; _uiBuilder.DisableCutsceneUiHide = !config.HideUiInCutscenes; _uiBuilder.DisableUserUiHide = !config.HideUiWhenUiHidden; @@ -51,5 +55,6 @@ public class PenumbraWindowSystem : IDisposable, IUiService _uiBuilder.OpenConfigUi -= Window.OpenSettings; _uiBuilder.Draw -= _windowSystem.Draw; _uiBuilder.Draw -= _fileDialog.Draw; + _uiBuilder.Draw -= _textureArraySlicer.Tick; } } From 60986c78f8449b2e6aed128b5b2b42ce6aafe35a Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:17:48 +0200 Subject: [PATCH 02/12] Update GameData --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 75582ece..ee6c6faa 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 75582ece58e6ee311074ff4ecaa68b804677878c +Subproject commit ee6c6faa1e4a3e96279cb6c89df96e351f112c6a From 59b3859f117e083cb105e84838ad3d5bd8c186fc Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:31:12 +0200 Subject: [PATCH 03/12] Minor upgrades to follow dependencies --- Penumbra/Import/Models/Export/MaterialExporter.cs | 11 ++++++----- Penumbra/Import/Textures/TextureDrawer.cs | 4 +--- Penumbra/Services/MigrationManager.cs | 4 ++-- Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs | 6 +----- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 5df9e1c1..62892473 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -49,7 +49,7 @@ public class MaterialExporter private static MaterialBuilder BuildCharacter(Material material, string name) { // Build the textures from the color table. - var table = new LegacyColorTable(material.Mtrl.Table); + var table = new LegacyColorTable(material.Mtrl.Table!); var normal = material.Textures[TextureUsage.SamplerNormal]; @@ -103,6 +103,7 @@ public class MaterialExporter // TODO: It feels a little silly to request the entire normal here when extracting the normal only needs some of the components. // As a future refactor, it would be neat to accept a single-channel field here, and then do composition of other stuff later. + // TODO(Dawntrail): Use the dedicated index (_id) map, that is not embedded in the normal map's alpha channel anymore. private readonly struct ProcessCharacterNormalOperation(Image normal, LegacyColorTable table) : IRowOperation { public Image Normal { get; } = normal.Clone(); @@ -139,17 +140,17 @@ public class MaterialExporter var nextRow = table[tableRow.Next]; // Base colour (table, .b) - var lerpedDiffuse = Vector3.Lerp(prevRow.Diffuse, nextRow.Diffuse, tableRow.Weight); + var lerpedDiffuse = Vector3.Lerp((Vector3)prevRow.DiffuseColor, (Vector3)nextRow.DiffuseColor, tableRow.Weight); baseColorSpan[x].FromVector4(new Vector4(lerpedDiffuse, 1)); baseColorSpan[x].A = normalPixel.B; // Specular (table) - var lerpedSpecularColor = Vector3.Lerp(prevRow.Specular, nextRow.Specular, tableRow.Weight); - var lerpedSpecularFactor = float.Lerp(prevRow.SpecularStrength, nextRow.SpecularStrength, tableRow.Weight); + var lerpedSpecularColor = Vector3.Lerp((Vector3)prevRow.SpecularColor, (Vector3)nextRow.SpecularColor, tableRow.Weight); + var lerpedSpecularFactor = float.Lerp((float)prevRow.SpecularMask, (float)nextRow.SpecularMask, tableRow.Weight); specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, lerpedSpecularFactor)); // Emissive (table) - var lerpedEmissive = Vector3.Lerp(prevRow.Emissive, nextRow.Emissive, tableRow.Weight); + var lerpedEmissive = Vector3.Lerp((Vector3)prevRow.EmissiveColor, (Vector3)nextRow.EmissiveColor, tableRow.Weight); emissiveSpan[x].FromVector4(new Vector4(lerpedEmissive, 1)); // Normal (.rg) diff --git a/Penumbra/Import/Textures/TextureDrawer.cs b/Penumbra/Import/Textures/TextureDrawer.cs index bd95d1ab..c83604e4 100644 --- a/Penumbra/Import/Textures/TextureDrawer.cs +++ b/Penumbra/Import/Textures/TextureDrawer.cs @@ -18,9 +18,7 @@ public static class TextureDrawer { if (texture.TextureWrap != null) { - size = size.X < texture.TextureWrap.Width - ? size with { Y = texture.TextureWrap.Height * size.X / texture.TextureWrap.Width } - : new Vector2(texture.TextureWrap.Width, texture.TextureWrap.Height); + size = texture.TextureWrap.Size.Contain(size); ImGui.Image(texture.TextureWrap.ImGuiHandle, size); DrawData(texture); diff --git a/Penumbra/Services/MigrationManager.cs b/Penumbra/Services/MigrationManager.cs index 84318da6..7726f6fd 100644 --- a/Penumbra/Services/MigrationManager.cs +++ b/Penumbra/Services/MigrationManager.cs @@ -279,7 +279,7 @@ public class MigrationManager(Configuration config) : IService Directory.CreateDirectory(Path.GetDirectoryName(path)!); using var f = File.Open(path, FileMode.Create, FileAccess.Write); - if (file.IsDawnTrail) + if (file.IsDawntrail) { file.MigrateToDawntrail(); Penumbra.Log.Debug($"Migrated material {reader.Entry.Key} to Dawntrail during import."); @@ -329,7 +329,7 @@ public class MigrationManager(Configuration config) : IService try { var mtrl = new MtrlFile(data); - if (mtrl.IsDawnTrail) + if (mtrl.IsDawntrail) return data; mtrl.MigrateToDawntrail(); diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index c47414b9..e2776b2f 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -6,7 +6,6 @@ using OtterGui; using Penumbra.Interop.ResourceTree; using Penumbra.UI.Classes; using Penumbra.String; -using Penumbra.UI.Tabs; namespace Penumbra.UI.AdvancedWindow; @@ -245,10 +244,7 @@ public class ResourceTreeViewer if (visibility == NodeVisibility.Hidden) continue; - var textColor = ImGui.GetColorU32(ImGuiCol.Text); - var textColorInternal = (textColor & 0x00FFFFFFu) | ((textColor & 0xFE000000u) >> 1); // Half opacity - - using var mutedColor = ImRaii.PushColor(ImGuiCol.Text, textColorInternal, resourceNode.Internal); + using var mutedColor = ImRaii.PushColor(ImGuiCol.Text, ImGuiUtil.HalfTransparentText(), resourceNode.Internal); var filterIcon = resourceNode.Icon != 0 ? resourceNode.Icon : parentFilterIcon; From e8182f285e2fd83ee8c6ed532fa14c858f9557c1 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:47:22 +0200 Subject: [PATCH 04/12] Update StainService for DT --- .../Interop/Structs/CharacterUtilityData.cs | 28 ++++++- Penumbra/Services/StainService.cs | 84 ++++++++++++++----- Penumbra/UI/Tabs/Debug/DebugTab.cs | 51 +++++++---- 3 files changed, 124 insertions(+), 39 deletions(-) diff --git a/Penumbra/Interop/Structs/CharacterUtilityData.cs b/Penumbra/Interop/Structs/CharacterUtilityData.cs index 197de0bb..7595353f 100644 --- a/Penumbra/Interop/Structs/CharacterUtilityData.cs +++ b/Penumbra/Interop/Structs/CharacterUtilityData.cs @@ -5,10 +5,15 @@ namespace Penumbra.Interop.Structs; [StructLayout(LayoutKind.Explicit)] public unsafe struct CharacterUtilityData { - public const int IndexHumanPbd = 63; - public const int IndexTransparentTex = 79; - public const int IndexDecalTex = 80; - public const int IndexSkinShpk = 83; + public const int IndexHumanPbd = 63; + public const int IndexTransparentTex = 79; + public const int IndexDecalTex = 80; + public const int IndexTileOrbArrayTex = 81; + public const int IndexTileNormArrayTex = 82; + public const int IndexSkinShpk = 83; + public const int IndexGudStm = 94; + public const int IndexLegacyStm = 95; + public const int IndexSphereDArrayTex = 96; public static readonly MetaIndex[] EqdpIndices = Enum.GetNames() .Zip(Enum.GetValues()) @@ -97,8 +102,23 @@ public unsafe struct CharacterUtilityData [FieldOffset(8 + IndexDecalTex * 8)] public TextureResourceHandle* DecalTexResource; + [FieldOffset(8 + IndexTileOrbArrayTex * 8)] + public TextureResourceHandle* TileOrbArrayTexResource; + + [FieldOffset(8 + IndexTileNormArrayTex * 8)] + public TextureResourceHandle* TileNormArrayTexResource; + [FieldOffset(8 + IndexSkinShpk * 8)] public ResourceHandle* SkinShpkResource; + [FieldOffset(8 + IndexGudStm * 8)] + public ResourceHandle* GudStmResource; + + [FieldOffset(8 + IndexLegacyStm * 8)] + public ResourceHandle* LegacyStmResource; + + [FieldOffset(8 + IndexSphereDArrayTex * 8)] + public TextureResourceHandle* SphereDArrayTexResource; + // not included resources have no known use case. } diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index 26b39229..50713968 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -6,19 +6,25 @@ using OtterGui.Services; using OtterGui.Widgets; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Files; -using Penumbra.UI.AdvancedWindow; +using Penumbra.GameData.Files.StainMapStructs; +using Penumbra.Interop.Services; +using Penumbra.Interop.Structs; +using Penumbra.UI.AdvancedWindow.Materials; namespace Penumbra.Services; public class StainService : IService { - public sealed class StainTemplateCombo(FilterComboColors stainCombo, StmFile stmFile) - : FilterComboCache(stmFile.Entries.Keys.Prepend((ushort)0), MouseWheelType.None, Penumbra.Log) + public sealed class StainTemplateCombo(FilterComboColors[] stainCombos, StmFile stmFile) + : FilterComboCache(stmFile.Entries.Keys.Prepend((ushort)0), MouseWheelType.None, Penumbra.Log) where TDyePack : unmanaged, IDyePack { + // FIXME There might be a better way to handle that. + public int CurrentDyeChannel = 0; + protected override float GetFilterWidth() { var baseSize = ImGui.CalcTextSize("0000").X + ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemInnerSpacing.X; - if (stainCombo.CurrentSelection.Key == 0) + if (stainCombos[CurrentDyeChannel].CurrentSelection.Key == 0) return baseSize; return baseSize + ImGui.GetTextLineHeight() * 3 + ImGui.GetStyle().ItemInnerSpacing.X * 3; @@ -47,33 +53,73 @@ public class StainService : IService protected override bool DrawSelectable(int globalIdx, bool selected) { var ret = base.DrawSelectable(globalIdx, selected); - var selection = stainCombo.CurrentSelection.Key; + var selection = stainCombos[CurrentDyeChannel].CurrentSelection.Key; if (selection == 0 || !stmFile.TryGetValue(Items[globalIdx], selection, out var colors)) return ret; ImGui.SameLine(); var frame = new Vector2(ImGui.GetTextLineHeight()); - ImGui.ColorButton("D", new Vector4(ModEditWindow.PseudoSqrtRgb(colors.Diffuse), 1), 0, frame); + ImGui.ColorButton("D", new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.DiffuseColor), 1), 0, frame); ImGui.SameLine(); - ImGui.ColorButton("S", new Vector4(ModEditWindow.PseudoSqrtRgb(colors.Specular), 1), 0, frame); + ImGui.ColorButton("S", new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.SpecularColor), 1), 0, frame); ImGui.SameLine(); - ImGui.ColorButton("E", new Vector4(ModEditWindow.PseudoSqrtRgb(colors.Emissive), 1), 0, frame); + ImGui.ColorButton("E", new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.EmissiveColor), 1), 0, frame); return ret; } } - public readonly DictStain StainData; - public readonly FilterComboColors StainCombo; - public readonly StmFile StmFile; - public readonly StainTemplateCombo TemplateCombo; + public const int ChannelCount = 2; - public StainService(IDataManager dataManager, DictStain stainData) + public readonly DictStain StainData; + public readonly FilterComboColors StainCombo1; + public readonly FilterComboColors StainCombo2; // FIXME is there a better way to handle this? + public readonly StmFile LegacyStmFile; + public readonly StmFile GudStmFile; + public readonly StainTemplateCombo LegacyTemplateCombo; + public readonly StainTemplateCombo GudTemplateCombo; + + public unsafe StainService(IDataManager dataManager, CharacterUtility characterUtility, DictStain stainData) { - StainData = stainData; - StainCombo = new FilterComboColors(140, MouseWheelType.None, - () => StainData.Value.Prepend(new KeyValuePair(0, ("None", 0, false))).ToList(), - Penumbra.Log); - StmFile = new StmFile(dataManager); - TemplateCombo = new StainTemplateCombo(StainCombo, StmFile); + StainData = stainData; + StainCombo1 = CreateStainCombo(); + StainCombo2 = CreateStainCombo(); + LegacyStmFile = LoadStmFile(characterUtility.Address->LegacyStmResource, dataManager); + GudStmFile = LoadStmFile(characterUtility.Address->GudStmResource, dataManager); + + FilterComboColors[] stainCombos = [StainCombo1, StainCombo2]; + + LegacyTemplateCombo = new StainTemplateCombo(stainCombos, LegacyStmFile); + GudTemplateCombo = new StainTemplateCombo(stainCombos, GudStmFile); } + + /// Retrieves the instance for the given channel. Indexing is zero-based. + public FilterComboColors GetStainCombo(int channel) + => channel switch + { + 0 => StainCombo1, + 1 => StainCombo2, + _ => throw new ArgumentOutOfRangeException(nameof(channel), channel, $"Unsupported dye channel {channel} (supported values are 0 and 1)") + }; + + /// Loads a STM file. Opportunistically attempts to re-use the file already read by the game, with Lumina fallback. + private static unsafe StmFile LoadStmFile(ResourceHandle* stmResourceHandle, IDataManager dataManager) where TDyePack : unmanaged, IDyePack + { + if (stmResourceHandle != null) + { + var stmData = stmResourceHandle->CsHandle.GetDataSpan(); + if (stmData.Length > 0) + { + Penumbra.Log.Debug($"[StainService] Loading StmFile<{typeof(TDyePack)}> from ResourceHandle 0x{(nint)stmResourceHandle:X}"); + return new StmFile(stmData); + } + } + + Penumbra.Log.Debug($"[StainService] Loading StmFile<{typeof(TDyePack)}> from Lumina"); + return new StmFile(dataManager); + } + + private FilterComboColors CreateStainCombo() + => new(140, MouseWheelType.None, + () => StainData.Value.Prepend(new KeyValuePair(0, ("None", 0, false))).ToList(), + Penumbra.Log); } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 3a64e556..ead02874 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -42,6 +42,9 @@ using ImGuiClip = OtterGui.ImGuiClip; using Penumbra.Api.IpcTester; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.ResourceLoading; +using Penumbra.GameData.Files.StainMapStructs; +using Penumbra.UI.AdvancedWindow; +using Penumbra.UI.AdvancedWindow.Materials; namespace Penumbra.UI.Tabs.Debug; @@ -697,32 +700,48 @@ public class DebugTab : Window, ITab, IUiService if (!mainTree) return; - foreach (var (key, data) in _stains.StmFile.Entries) + using (var legacyTree = TreeNode("stainingtemplate.stm")) + { + if (legacyTree) + DrawStainTemplatesFile(_stains.LegacyStmFile); + } + + using (var gudTree = TreeNode("stainingtemplate_gud.stm")) + { + if (gudTree) + DrawStainTemplatesFile(_stains.GudStmFile); + } + } + + private static void DrawStainTemplatesFile(StmFile stmFile) where TDyePack : unmanaged, IDyePack + { + foreach (var (key, data) in stmFile.Entries) { using var tree = TreeNode($"Template {key}"); if (!tree) continue; - using var table = Table("##table", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + using var table = Table("##table", data.Colors.Length + data.Scalars.Length, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); if (!table) continue; - for (var i = 0; i < StmFile.StainingTemplateEntry.NumElements; ++i) + for (var i = 0; i < StmFile.StainingTemplateEntry.NumElements; ++i) { - var (r, g, b) = data.DiffuseEntries[i]; - ImGuiUtil.DrawTableColumn($"{r:F6} | {g:F6} | {b:F6}"); + foreach (var list in data.Colors) + { + var color = list[i]; + ImGui.TableNextColumn(); + var frame = new Vector2(ImGui.GetTextLineHeight()); + ImGui.ColorButton("###color", new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)color), 1), 0, frame); + ImGui.SameLine(); + ImGui.TextUnformatted($"{color.Red:F6} | {color.Green:F6} | {color.Blue:F6}"); + } - (r, g, b) = data.SpecularEntries[i]; - ImGuiUtil.DrawTableColumn($"{r:F6} | {g:F6} | {b:F6}"); - - (r, g, b) = data.EmissiveEntries[i]; - ImGuiUtil.DrawTableColumn($"{r:F6} | {g:F6} | {b:F6}"); - - var a = data.SpecularPowerEntries[i]; - ImGuiUtil.DrawTableColumn($"{a:F6}"); - - a = data.GlossEntries[i]; - ImGuiUtil.DrawTableColumn($"{a:F6}"); + foreach (var list in data.Scalars) + { + var scalar = list[i]; + ImGuiUtil.DrawTableColumn($"{scalar:F6}"); + } } } } From 450751e43fe4d9627da938715b3930e9d25ebe10 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:47:36 +0200 Subject: [PATCH 05/12] DT material editor, supporting components --- .../Materials/ConstantEditors.cs | 71 +++++++ .../Materials/MaterialTemplatePickers.cs | 177 ++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 Penumbra/UI/AdvancedWindow/Materials/ConstantEditors.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs diff --git a/Penumbra/UI/AdvancedWindow/Materials/ConstantEditors.cs b/Penumbra/UI/AdvancedWindow/Materials/ConstantEditors.cs new file mode 100644 index 00000000..690580df --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/ConstantEditors.cs @@ -0,0 +1,71 @@ +using System.Collections.Frozen; +using OtterGui.Text.Widget.Editors; +using Penumbra.GameData.Files.ShaderStructs; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public static class ConstantEditors +{ + public static readonly IEditor DefaultFloat = Editors.DefaultFloat.AsByteEditor(); + public static readonly IEditor DefaultInt = Editors.DefaultInt.AsByteEditor(); + public static readonly IEditor DefaultIntAsFloat = Editors.DefaultInt.IntAsFloatEditor().AsByteEditor(); + public static readonly IEditor DefaultColor = ColorEditor.HighDynamicRange.Reinterpreting(); + + /// + /// Material constants known to be encoded as native s. + /// + /// A editor is nonfunctional for them, as typical values for these constants would fall into the IEEE 754 denormalized number range. + /// + private static readonly FrozenSet KnownIntConstants; + + static ConstantEditors() + { + IReadOnlyList knownIntConstants = [ + "g_ToonIndex", + "g_ToonSpecIndex", + ]; + + KnownIntConstants = knownIntConstants.ToFrozenSet(); + } + + public static IEditor DefaultFor(Name name, MaterialTemplatePickers? materialTemplatePickers = null) + { + if (materialTemplatePickers != null) + { + if (name == Names.SphereMapIndexConstantName) + return materialTemplatePickers.SphereMapIndexPicker; + else if (name == Names.TileIndexConstantName) + return materialTemplatePickers.TileIndexPicker; + } + + if (name.Value != null && name.Value.EndsWith("Color")) + return DefaultColor; + + if (KnownIntConstants.Contains(name)) + return DefaultInt; + + return DefaultFloat; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEditor AsByteEditor(this IEditor inner) where T : unmanaged + => inner.Reinterpreting(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEditor IntAsFloatEditor(this IEditor inner) + => inner.Converting(value => int.CreateSaturating(MathF.Round(value)), value => value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEditor WithExponent(this IEditor inner, T exponent) + where T : unmanaged, IPowerFunctions, IComparisonOperators + => exponent == T.MultiplicativeIdentity + ? inner + : inner.Converting(value => value < T.Zero ? -T.Pow(-value, T.MultiplicativeIdentity / exponent) : T.Pow(value, T.MultiplicativeIdentity / exponent), value => value < T.Zero ? -T.Pow(-value, exponent) : T.Pow(value, exponent)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEditor WithFactorAndBias(this IEditor inner, T factor, T bias) + where T : unmanaged, IMultiplicativeIdentity, IAdditiveIdentity, IMultiplyOperators, IAdditionOperators, ISubtractionOperators, IDivisionOperators, IEqualityOperators + => factor == T.MultiplicativeIdentity && bias == T.AdditiveIdentity + ? inner + : inner.Converting(value => (value - bias) / factor, value => value * factor + bias); +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs b/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs new file mode 100644 index 00000000..6ffd1f88 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs @@ -0,0 +1,177 @@ +using Dalamud.Interface; +using FFXIVClientStructs.Interop; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; +using OtterGui.Text.Widget.Editors; +using Penumbra.Interop.Services; +using Penumbra.Interop.Structs; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public sealed unsafe class MaterialTemplatePickers : IUiService +{ + private const float MaximumTextureSize = 64.0f; + + private readonly TextureArraySlicer _textureArraySlicer; + private readonly CharacterUtility _characterUtility; + + public readonly IEditor TileIndexPicker; + public readonly IEditor SphereMapIndexPicker; + + public MaterialTemplatePickers(TextureArraySlicer textureArraySlicer, CharacterUtility characterUtility) + { + _textureArraySlicer = textureArraySlicer; + _characterUtility = characterUtility; + + TileIndexPicker = new Editor(DrawTileIndexPicker).AsByteEditor(); + SphereMapIndexPicker = new Editor(DrawSphereMapIndexPicker).AsByteEditor(); + } + + public bool DrawTileIndexPicker(ReadOnlySpan label, ReadOnlySpan description, ref ushort value, bool compact) + => _characterUtility.Address != null + && DrawTextureArrayIndexPicker(label, description, ref value, compact, [ + _characterUtility.Address->TileOrbArrayTexResource, + _characterUtility.Address->TileNormArrayTexResource, + ]); + + public bool DrawSphereMapIndexPicker(ReadOnlySpan label, ReadOnlySpan description, ref ushort value, bool compact) + => _characterUtility.Address != null + && DrawTextureArrayIndexPicker(label, description, ref value, compact, [ + _characterUtility.Address->SphereDArrayTexResource, + ]); + + public bool DrawTextureArrayIndexPicker(ReadOnlySpan label, ReadOnlySpan description, ref ushort value, bool compact, ReadOnlySpan> textureRHs) + { + TextureResourceHandle* firstNonNullTextureRH = null; + foreach (var texture in textureRHs) + { + if (texture.Value != null && texture.Value->CsHandle.Texture != null) + { + firstNonNullTextureRH = texture; + break; + } + } + var firstNonNullTexture = firstNonNullTextureRH != null ? firstNonNullTextureRH->CsHandle.Texture : null; + + var textureSize = firstNonNullTexture != null ? new Vector2(firstNonNullTexture->Width, firstNonNullTexture->Height).Contain(new Vector2(MaximumTextureSize)) : Vector2.Zero; + var count = firstNonNullTexture != null ? firstNonNullTexture->ArraySize : 0; + + var ret = false; + + var framePadding = ImGui.GetStyle().FramePadding; + var itemSpacing = ImGui.GetStyle().ItemSpacing; + using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) + { + var spaceSize = ImUtf8.CalcTextSize(" "u8).X; + var spaces = (int)((ImGui.CalcItemWidth() - framePadding.X * 2.0f - (compact ? 0.0f : (textureSize.X + itemSpacing.X) * textureRHs.Length)) / spaceSize); + using var padding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, framePadding + new Vector2(0.0f, Math.Max(textureSize.Y - ImGui.GetFrameHeight() + itemSpacing.Y, 0.0f) * 0.5f), !compact); + using var combo = ImUtf8.Combo(label, (value == ushort.MaxValue ? "-" : value.ToString()).PadLeft(spaces), ImGuiComboFlags.NoArrowButton | ImGuiComboFlags.HeightLarge); + if (combo.Success && firstNonNullTextureRH != null) + { + var lineHeight = Math.Max(ImGui.GetTextLineHeightWithSpacing(), framePadding.Y * 2.0f + textureSize.Y); + var itemWidth = Math.Max(ImGui.GetContentRegionAvail().X, ImUtf8.CalcTextSize("MMM"u8).X + (itemSpacing.X + textureSize.X) * textureRHs.Length + framePadding.X * 2.0f); + using var center = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)); + using var clipper = ImUtf8.ListClipper(count, lineHeight); + while (clipper.Step()) + { + for (var i = clipper.DisplayStart; i < clipper.DisplayEnd && i < count; i++) + { + if (ImUtf8.Selectable($"{i,3}", i == value, size: new(itemWidth, lineHeight))) + { + ret = value != i; + value = (ushort)i; + } + var rectMin = ImGui.GetItemRectMin(); + var rectMax = ImGui.GetItemRectMax(); + var textureRegionStart = new Vector2( + rectMax.X - framePadding.X - textureSize.X * textureRHs.Length - itemSpacing.X * (textureRHs.Length - 1), + rectMin.Y + framePadding.Y); + var maxSize = new Vector2(textureSize.X, rectMax.Y - framePadding.Y - textureRegionStart.Y); + DrawTextureSlices(textureRegionStart, maxSize, itemSpacing.X, textureRHs, (byte)i); + } + } + } + } + if (!compact && value != ushort.MaxValue) + { + var cbRectMin = ImGui.GetItemRectMin(); + var cbRectMax = ImGui.GetItemRectMax(); + var cbTextureRegionStart = new Vector2(cbRectMax.X - framePadding.X - textureSize.X * textureRHs.Length - itemSpacing.X * (textureRHs.Length - 1), cbRectMin.Y + framePadding.Y); + var cbMaxSize = new Vector2(textureSize.X, cbRectMax.Y - framePadding.Y - cbTextureRegionStart.Y); + DrawTextureSlices(cbTextureRegionStart, cbMaxSize, itemSpacing.X, textureRHs, (byte)value); + } + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled) && (description.Length > 0 || compact && value != ushort.MaxValue)) + { + using var disabled = ImRaii.Enabled(); + using var tt = ImUtf8.Tooltip(); + if (description.Length > 0) + ImUtf8.Text(description); + if (compact && value != ushort.MaxValue) + { + ImGui.Dummy(new Vector2(textureSize.X * textureRHs.Length + itemSpacing.X * (textureRHs.Length - 1), textureSize.Y)); + var rectMin = ImGui.GetItemRectMin(); + var rectMax = ImGui.GetItemRectMax(); + DrawTextureSlices(rectMin, textureSize, itemSpacing.X, textureRHs, (byte)value); + } + } + + return ret; + } + + public void DrawTextureSlices(Vector2 regionStart, Vector2 itemSize, float itemSpacing, ReadOnlySpan> textureRHs, byte sliceIndex) + { + for (var j = 0; j < textureRHs.Length; ++j) + { + if (textureRHs[j].Value == null) + continue; + var texture = textureRHs[j].Value->CsHandle.Texture; + if (texture == null) + continue; + var handle = _textureArraySlicer.GetImGuiHandle(texture, sliceIndex); + if (handle == 0) + continue; + + var position = regionStart with { X = regionStart.X + (itemSize.X + itemSpacing) * j }; + var size = new Vector2(texture->Width, texture->Height).Contain(itemSize); + position += (itemSize - size) * 0.5f; + ImGui.GetWindowDrawList().AddImage(handle, position, position + size, Vector2.Zero, + new Vector2(texture->Width / (float)texture->Width2, texture->Height / (float)texture->Height2)); + } + } + + private delegate bool DrawEditor(ReadOnlySpan label, ReadOnlySpan description, ref ushort value, bool compact); + + private sealed class Editor(DrawEditor draw) : IEditor + { + public bool Draw(Span values, bool disabled) + { + var helper = Editors.PrepareMultiComponent(values.Length); + var ret = false; + + for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) + { + helper.SetupComponent(valueIdx); + + var value = ushort.CreateSaturating(MathF.Round(values[valueIdx])); + if (disabled) + { + using var _ = ImRaii.Disabled(); + draw(helper.Id, default, ref value, true); + } + else + { + if (draw(helper.Id, default, ref value, true)) + { + values[valueIdx] = value; + ret = true; + } + } + } + + return ret; + } + } +} From 36ab9573aec81e56655be92f62c7e3e473d737f5 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:41:38 +0200 Subject: [PATCH 06/12] DT material editor, main part --- Penumbra/Configuration.cs | 1 + .../LiveColorTablePreviewer.cs | 24 +- .../MaterialPreview/LiveMaterialPreviewer.cs | 20 +- .../Materials/MtrlTab.CommonColorTable.cs | 509 ++++++++++++ .../Materials/MtrlTab.Constants.cs | 277 +++++++ .../Materials/MtrlTab.Devkit.cs | 240 ++++++ .../Materials/MtrlTab.LegacyColorTable.cs | 368 ++++++++ .../Materials/MtrlTab.LivePreview.cs | 272 ++++++ .../Materials/MtrlTab.ShaderPackage.cs | 505 +++++++++++ .../Materials/MtrlTab.Textures.cs | 276 ++++++ .../UI/AdvancedWindow/Materials/MtrlTab.cs | 199 +++++ .../Materials/MtrlTabFactory.cs | 18 + .../ModEditWindow.Materials.ColorTable.cs | 538 ------------ .../ModEditWindow.Materials.ConstantEditor.cs | 247 ------ .../ModEditWindow.Materials.MtrlTab.cs | 783 ------------------ .../ModEditWindow.Materials.Shpk.cs | 481 ----------- .../AdvancedWindow/ModEditWindow.Materials.cs | 179 +--- .../ModEditWindow.QuickImport.cs | 1 + Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 76 +- Penumbra/UI/Classes/Colors.cs | 4 +- Penumbra/UI/Tabs/SettingsTab.cs | 12 + 21 files changed, 2744 insertions(+), 2286 deletions(-) create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs delete mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs delete mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs delete mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs delete mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 63325433..50426b38 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -107,6 +107,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool AlwaysOpenDefaultImport { get; set; } = false; public bool KeepDefaultMetaChanges { get; set; } = false; public string DefaultModAuthor { get; set; } = DefaultTexToolsData.Author; + public bool EditRawTileTransforms { get; set; } = false; public Dictionary Colors { get; set; } = Enum.GetValues().ToDictionary(c => c, c => c.Data().DefaultColor); diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index 8e75a895..bbd3b16c 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -1,5 +1,6 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.GameData.Interop; using Penumbra.Interop.SafeHandles; @@ -7,10 +8,6 @@ namespace Penumbra.Interop.MaterialPreview; public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase { - public const int TextureWidth = 4; - public const int TextureHeight = GameData.Files.MaterialStructs.LegacyColorTable.NumUsedRows; - public const int TextureLength = TextureWidth * TextureHeight * 4; - private readonly IFramework _framework; private readonly Texture** _colorTableTexture; @@ -18,6 +15,9 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase private bool _updatePending; + public int Width { get; } + public int Height { get; } + public Half[] ColorTable { get; } public LiveColorTablePreviewer(ObjectManager objects, IFramework framework, MaterialInfo materialInfo) @@ -33,18 +33,24 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase if (colorSetTextures == null) throw new InvalidOperationException("Draw object doesn't have color table textures"); - _colorTableTexture = colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot); + _colorTableTexture = colorSetTextures + (MaterialInfo.ModelSlot * CharacterBase.MaterialsPerSlot + MaterialInfo.MaterialSlot); + _originalColorTableTexture = new SafeTextureHandle(*_colorTableTexture, true); if (_originalColorTableTexture == null) throw new InvalidOperationException("Material doesn't have a color table"); - ColorTable = new Half[TextureLength]; + Width = (int)_originalColorTableTexture.Texture->Width; + Height = (int)_originalColorTableTexture.Texture->Height; + ColorTable = new Half[Width * Height * 4]; _updatePending = true; framework.Update += OnFrameworkUpdate; } + public Span GetColorRow(int i) + => ColorTable.AsSpan().Slice(Width * 4 * i, Width * 4); + protected override void Clear(bool disposing, bool reset) { _framework.Update -= OnFrameworkUpdate; @@ -74,8 +80,8 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase return; var textureSize = stackalloc int[2]; - textureSize[0] = TextureWidth; - textureSize[1] = TextureHeight; + textureSize[0] = Width; + textureSize[1] = Height; using var texture = new SafeTextureHandle(Device.Instance()->CreateTexture2D(textureSize, 1, 0x2460, 0x80000804, 7), false); @@ -104,6 +110,6 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase if (colorSetTextures == null) return false; - return _colorTableTexture == colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot); + return _colorTableTexture == colorSetTextures + (MaterialInfo.ModelSlot * CharacterBase.MaterialsPerSlot + MaterialInfo.MaterialSlot); } } diff --git a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs index 0556fdc4..60762ac7 100644 --- a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs @@ -7,9 +7,9 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase { private readonly ShaderPackage* _shaderPackage; - private readonly uint _originalShPkFlags; - private readonly float[] _originalMaterialParameter; - private readonly uint[] _originalSamplerFlags; + private readonly uint _originalShPkFlags; + private readonly byte[] _originalMaterialParameter; + private readonly uint[] _originalSamplerFlags; public LiveMaterialPreviewer(ObjectManager objects, MaterialInfo materialInfo) : base(objects, materialInfo) @@ -28,7 +28,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase _originalShPkFlags = Material->ShaderFlags; - _originalMaterialParameter = Material->MaterialParameterCBuffer->TryGetBuffer().ToArray(); + _originalMaterialParameter = Material->MaterialParameterCBuffer->TryGetBuffer().ToArray(); _originalSamplerFlags = new uint[Material->TextureCount]; for (var i = 0; i < _originalSamplerFlags.Length; ++i) @@ -43,7 +43,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase return; Material->ShaderFlags = _originalShPkFlags; - var materialParameter = Material->MaterialParameterCBuffer->TryGetBuffer(); + var materialParameter = Material->MaterialParameterCBuffer->TryGetBuffer(); if (!materialParameter.IsEmpty) _originalMaterialParameter.AsSpan().CopyTo(materialParameter); @@ -59,7 +59,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase Material->ShaderFlags = shPkFlags; } - public void SetMaterialParameter(uint parameterCrc, Index offset, Span value) + public void SetMaterialParameter(uint parameterCrc, Index offset, ReadOnlySpan value) { if (!CheckValidity()) return; @@ -68,7 +68,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase if (constantBuffer == null) return; - var buffer = constantBuffer->TryGetBuffer(); + var buffer = constantBuffer->TryGetBuffer(); if (buffer.IsEmpty) return; @@ -78,12 +78,10 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase if (parameter.CRC != parameterCrc) continue; - if ((parameter.Offset & 0x3) != 0 - || (parameter.Size & 0x3) != 0 - || (parameter.Offset + parameter.Size) >> 2 > buffer.Length) + if (parameter.Offset + parameter.Size > buffer.Length) return; - value.TryCopyTo(buffer.Slice(parameter.Offset >> 2, parameter.Size >> 2)[offset..]); + value.TryCopyTo(buffer.Slice(parameter.Offset, parameter.Size)[offset..]); return; } } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs new file mode 100644 index 00000000..937614de --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs @@ -0,0 +1,509 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using ImGuiNET; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Files; +using OtterGui.Text; +using Penumbra.GameData.Structs; +using OtterGui.Raii; +using OtterGui.Text.Widget; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public partial class MtrlTab +{ + 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 static readonly FontAwesomeCheckbox ApplyStainCheckbox = new(FontAwesomeIcon.FillDrip); + + private static (Vector2 Scale, float Rotation, float Shear)? _pinnedTileTransform; + + private bool DrawColorTableSection(bool disabled) + { + if ((!ShpkLoading && !SamplerIds.Contains(ShpkFile.TableSamplerId)) || Mtrl.Table == null) + return false; + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + if (!ImGui.CollapsingHeader("Color Table", ImGuiTreeNodeFlags.DefaultOpen)) + return false; + + ColorTableCopyAllClipboardButton(); + ImGui.SameLine(); + var ret = ColorTablePasteAllClipboardButton(disabled); + if (!disabled) + { + ImGui.SameLine(); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImGui.SameLine(); + ret |= ColorTableDyeableCheckbox(); + } + + if (Mtrl.DyeTable != null) + { + ImGui.SameLine(); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImGui.SameLine(); + ret |= DrawPreviewDye(disabled); + } + + ret |= Mtrl.Table switch + { + LegacyColorTable legacyTable => DrawLegacyColorTable(legacyTable, Mtrl.DyeTable as LegacyColorDyeTable, disabled), + ColorTable table when Mtrl.ShaderPackage.Name is "characterlegacy.shpk" => DrawLegacyColorTable(table, Mtrl.DyeTable as ColorDyeTable, disabled), + _ => false, + }; + + return ret; + } + + private void ColorTableCopyAllClipboardButton() + { + if (Mtrl.Table == null) + return; + + if (!ImGui.Button("Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2(200, 0))) + return; + + try + { + var data1 = Mtrl.Table.AsBytes(); + var data2 = Mtrl.DyeTable != null ? Mtrl.DyeTable.AsBytes() : []; + + 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); + } + catch + { + // ignored + } + } + + private bool DrawPreviewDye(bool disabled) + { + var (dyeId1, (name1, dyeColor1, gloss1)) = _stainService.StainCombo1.CurrentSelection; + var (dyeId2, (name2, dyeColor2, gloss2)) = _stainService.StainCombo2.CurrentSelection; + var tt = dyeId1 == 0 && dyeId2 == 0 + ? "Select a preview dye first."u8 + : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."u8; + if (ImUtf8.ButtonEx("Apply Preview Dye"u8, tt, disabled: disabled || dyeId1 == 0 && dyeId2 == 0)) + { + var ret = false; + if (Mtrl.DyeTable != null) + { + ret |= Mtrl.ApplyDye(_stainService.LegacyStmFile, [dyeId1, dyeId2]); + ret |= Mtrl.ApplyDye(_stainService.GudStmFile, [dyeId1, dyeId2]); + } + + UpdateColorTablePreview(); + + return ret; + } + + ImGui.SameLine(); + var label = dyeId1 == 0 ? "Preview Dye 1###previewDye1" : $"{name1} (Preview 1)###previewDye1"; + if (_stainService.StainCombo1.Draw(label, dyeColor1, string.Empty, true, gloss1)) + UpdateColorTablePreview(); + ImGui.SameLine(); + label = dyeId2 == 0 ? "Preview Dye 2###previewDye2" : $"{name2} (Preview 2)###previewDye2"; + if (_stainService.StainCombo2.Draw(label, dyeColor2, string.Empty, true, gloss2)) + UpdateColorTablePreview(); + return false; + } + + private bool ColorTablePasteAllClipboardButton(bool disabled) + { + if (Mtrl.Table == null) + return false; + + if (!ImUtf8.ButtonEx("Import All Rows from Clipboard"u8, ImGuiHelpers.ScaledVector2(200, 0), disabled)) + return false; + + try + { + var text = ImGui.GetClipboardText(); + var data = Convert.FromBase64String(text); + var table = Mtrl.Table.AsBytes(); + var dyeTable = Mtrl.DyeTable != null ? Mtrl.DyeTable.AsBytes() : []; + if (data.Length != table.Length && data.Length != table.Length + dyeTable.Length) + return false; + + data.AsSpan(0, table.Length).TryCopyTo(table); + data.AsSpan(table.Length).TryCopyTo(dyeTable); + + UpdateColorTablePreview(); + + return true; + } + catch + { + return false; + } + } + + [SkipLocalsInit] + private void ColorTableCopyClipboardButton(int rowIdx) + { + if (Mtrl.Table == null) + return; + + if (!ImUtf8.IconButton(FontAwesomeIcon.Clipboard, "Export this row to your clipboard."u8, + ImGui.GetFrameHeight() * Vector2.One)) + return; + + try + { + var data1 = Mtrl.Table.RowAsBytes(rowIdx); + var data2 = Mtrl.DyeTable != null ? Mtrl.DyeTable.RowAsBytes(rowIdx) : []; + + 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); + } + catch + { + // ignored + } + } + + private bool ColorTableDyeableCheckbox() + { + var dyeable = Mtrl.DyeTable != null; + var ret = ImGui.Checkbox("Dyeable", ref dyeable); + + if (ret) + { + Mtrl.DyeTable = dyeable ? Mtrl.Table switch + { + ColorTable => new ColorDyeTable(), + LegacyColorTable => new LegacyColorDyeTable(), + _ => null, + } : null; + UpdateColorTablePreview(); + } + + return ret; + } + + private bool ColorTablePasteFromClipboardButton(int rowIdx, bool disabled) + { + if (Mtrl.Table == null) + return false; + + if (!ImUtf8.IconButton(FontAwesomeIcon.Paste, "Import an exported row from your clipboard onto this row."u8, + ImGui.GetFrameHeight() * Vector2.One, disabled)) + return false; + + try + { + var text = ImGui.GetClipboardText(); + var data = Convert.FromBase64String(text); + var row = Mtrl.Table.RowAsBytes(rowIdx); + var dyeRow = Mtrl.DyeTable != null ? Mtrl.DyeTable.RowAsBytes(rowIdx) : []; + if (data.Length != row.Length && data.Length != row.Length + dyeRow.Length) + return false; + + data.AsSpan(0, row.Length).TryCopyTo(row); + data.AsSpan(row.Length).TryCopyTo(dyeRow); + + UpdateColorTableRowPreview(rowIdx); + + return true; + } + catch + { + return false; + } + } + + private void ColorTableHighlightButton(int pairIdx, bool disabled) + { + ImUtf8.IconButton(FontAwesomeIcon.Crosshairs, "Highlight this pair of rows on your character, if possible.\n\nHighlight colors can be configured in Penumbra's settings."u8, + ImGui.GetFrameHeight() * Vector2.One, disabled || ColorTablePreviewers.Count == 0); + + if (ImGui.IsItemHovered()) + HighlightColorTablePair(pairIdx); + else if (HighlightedColorTablePair == pairIdx) + CancelColorTableHighlight(); + } + + private static void CtBlendRect(Vector2 rcMin, Vector2 rcMax, uint topColor, uint bottomColor) + { + var style = ImGui.GetStyle(); + var frameRounding = style.FrameRounding; + var frameThickness = style.FrameBorderSize; + var borderColor = ImGui.GetColorU32(ImGuiCol.Border); + var drawList = ImGui.GetWindowDrawList(); + if (topColor == bottomColor) + drawList.AddRectFilled(rcMin, rcMax, topColor, frameRounding, ImDrawFlags.RoundCornersDefault); + else + { + drawList.AddRectFilled( + rcMin, rcMax with { Y = float.Lerp(rcMin.Y, rcMax.Y, 1.0f / 3) }, + topColor, frameRounding, ImDrawFlags.RoundCornersTopLeft | ImDrawFlags.RoundCornersTopRight); + drawList.AddRectFilledMultiColor( + rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 1.0f / 3) }, + rcMax with { Y = float.Lerp(rcMin.Y, rcMax.Y, 2.0f / 3) }, + topColor, topColor, bottomColor, bottomColor); + drawList.AddRectFilled( + rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 2.0f / 3) }, rcMax, + bottomColor, frameRounding, ImDrawFlags.RoundCornersBottomLeft | ImDrawFlags.RoundCornersBottomRight); + } + drawList.AddRect(rcMin, rcMax, borderColor, frameRounding, ImDrawFlags.RoundCornersDefault, frameThickness); + } + + private static bool CtColorPicker(ReadOnlySpan label, ReadOnlySpan description, HalfColor current, Action setter, ReadOnlySpan letter = default) + { + var ret = false; + var inputSqrt = PseudoSqrtRgb((Vector3)current); + var tmp = inputSqrt; + if (ImUtf8.ColorEdit(label, ref tmp, + ImGuiColorEditFlags.NoInputs + | ImGuiColorEditFlags.DisplayRGB + | ImGuiColorEditFlags.InputRGB + | ImGuiColorEditFlags.NoTooltip + | ImGuiColorEditFlags.HDR) + && tmp != inputSqrt) + { + setter((HalfColor)PseudoSquareRgb(tmp)); + ret = true; + } + + if (letter.Length > 0 && ImGui.IsItemVisible()) + { + var textSize = ImUtf8.CalcTextSize(letter); + var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2; + var textColor = inputSqrt.LengthSquared() < 0.25f ? 0x80FFFFFFu : 0x80000000u; + ImGui.GetWindowDrawList().AddText(letter, center, textColor); + } + + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); + + return ret; + } + + private static void CtColorPicker(ReadOnlySpan label, ReadOnlySpan description, HalfColor? current, ReadOnlySpan letter = default) + { + if (current.HasValue) + CtColorPicker(label, description, current.Value, Nop, letter); + else + { + var tmp = Vector4.Zero; + ImUtf8.ColorEdit(label, ref tmp, + ImGuiColorEditFlags.NoInputs + | ImGuiColorEditFlags.DisplayRGB + | ImGuiColorEditFlags.InputRGB + | ImGuiColorEditFlags.NoTooltip + | ImGuiColorEditFlags.HDR + | ImGuiColorEditFlags.AlphaPreview); + + if (letter.Length > 0 && ImGui.IsItemVisible()) + { + var textSize = ImUtf8.CalcTextSize(letter); + var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2; + ImGui.GetWindowDrawList().AddText(letter, center, 0x80000000u); + } + + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); + } + } + + private static bool CtApplyStainCheckbox(ReadOnlySpan label, ReadOnlySpan description, bool current, Action setter) + { + var tmp = current; + var result = ApplyStainCheckbox.Draw(label, ref tmp); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); + if (!result || tmp == current) + return false; + + setter(tmp); + return true; + } + + private static bool CtDragHalf(ReadOnlySpan label, ReadOnlySpan description, Half value, ReadOnlySpan format, float min, float max, float speed, Action setter) + { + var tmp = (float)value; + var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); + if (!result) + return false; + var newValue = (Half)tmp; + if (newValue == value) + return false; + setter(newValue); + return true; + } + + private static bool CtDragHalf(ReadOnlySpan label, ReadOnlySpan description, ref Half value, ReadOnlySpan format, float min, float max, float speed) + { + var tmp = (float)value; + var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); + if (!result) + return false; + var newValue = (Half)tmp; + if (newValue == value) + return false; + value = newValue; + return true; + } + + private static void CtDragHalf(ReadOnlySpan label, ReadOnlySpan description, Half? value, ReadOnlySpan format) + { + using var _ = ImRaii.Disabled(); + var valueOrDefault = value ?? Half.Zero; + var floatValue = (float)valueOrDefault; + CtDragHalf(label, description, valueOrDefault, value.HasValue ? format : "-"u8, floatValue, floatValue, 0.0f, Nop); + } + + private static bool CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, T value, ReadOnlySpan format, T min, T max, float speed, Action setter) where T : unmanaged, INumber + { + var tmp = value; + var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); + if (!result || tmp == value) + return false; + setter(tmp); + return true; + } + + private static bool CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, ref T value, ReadOnlySpan format, T min, T max, float speed) where T : unmanaged, INumber + { + var tmp = value; + var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); + if (!result || tmp == value) + return false; + value = tmp; + return true; + } + + private static void CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, T? value, ReadOnlySpan format) where T : unmanaged, INumber + { + using var _ = ImRaii.Disabled(); + var valueOrDefault = value ?? T.Zero; + CtDragScalar(label, description, valueOrDefault, value.HasValue ? format : "-"u8, valueOrDefault, valueOrDefault, 0.0f, Nop); + } + + private bool CtTileIndexPicker(ReadOnlySpan label, ReadOnlySpan description, ushort value, bool compact, Action setter) + { + if (!_materialTemplatePickers.DrawTileIndexPicker(label, description, ref value, compact)) + return false; + setter(value); + return true; + } + + private bool CtSphereMapIndexPicker(ReadOnlySpan label, ReadOnlySpan description, ushort value, bool compact, Action setter) + { + if (!_materialTemplatePickers.DrawSphereMapIndexPicker(label, description, ref value, compact)) + return false; + setter(value); + return true; + } + + private bool CtTileTransformMatrix(HalfMatrix2x2 value, float floatSize, bool twoRowLayout, Action setter) + { + var ret = false; + if (_config.EditRawTileTransforms) + { + var tmp = value; + ImGui.SetNextItemWidth(floatSize); + ret |= CtDragHalf("##TileTransformUU"u8, "Tile Repeat U"u8, ref tmp.UU, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(floatSize); + ret |= CtDragHalf("##TileTransformVV"u8, "Tile Repeat V"u8, ref tmp.VV, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); + if (!twoRowLayout) + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(floatSize); + ret |= CtDragHalf("##TileTransformUV"u8, "Tile Skew U"u8, ref tmp.UV, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(floatSize); + ret |= CtDragHalf("##TileTransformVU"u8, "Tile Skew V"u8, ref tmp.VU, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); + if (!ret || tmp == value) + return false; + setter(tmp); + } + else + { + value.Decompose(out var scale, out var rotation, out var shear); + rotation *= 180.0f / MathF.PI; + shear *= 180.0f / MathF.PI; + ImGui.SetNextItemWidth(floatSize); + var scaleXChanged = CtDragScalar("##TileScaleU"u8, "Tile Scale U"u8, ref scale.X, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); + var activated = ImGui.IsItemActivated(); + var deactivated = ImGui.IsItemDeactivated(); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(floatSize); + var scaleYChanged = CtDragScalar("##TileScaleV"u8, "Tile Scale V"u8, ref scale.Y, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); + activated |= ImGui.IsItemActivated(); + deactivated |= ImGui.IsItemDeactivated(); + if (!twoRowLayout) + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(floatSize); + var rotationChanged = CtDragScalar("##TileRotation"u8, "Tile Rotation"u8, ref rotation, "%.0f°"u8, -180.0f, 180.0f, 1.0f); + activated |= ImGui.IsItemActivated(); + deactivated |= ImGui.IsItemDeactivated(); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(floatSize); + var shearChanged = CtDragScalar("##TileShear"u8, "Tile Shear"u8, ref shear, "%.0f°"u8, -90.0f, 90.0f, 1.0f); + activated |= ImGui.IsItemActivated(); + deactivated |= ImGui.IsItemDeactivated(); + if (deactivated) + _pinnedTileTransform = null; + else if (activated) + _pinnedTileTransform = (scale, rotation, shear); + ret = scaleXChanged | scaleYChanged | rotationChanged | shearChanged; + if (!ret) + return false; + if (_pinnedTileTransform.HasValue) + { + var (pinScale, pinRotation, pinShear) = _pinnedTileTransform.Value; + if (!scaleXChanged) + scale.X = pinScale.X; + if (!scaleYChanged) + scale.Y = pinScale.Y; + if (!rotationChanged) + rotation = pinRotation; + if (!shearChanged) + shear = pinShear; + } + var newValue = HalfMatrix2x2.Compose(scale, rotation * MathF.PI / 180.0f, shear * MathF.PI / 180.0f); + if (newValue == value) + return false; + setter(newValue); + } + return true; + } + + /// For use as setter of read-only fields. + private static void Nop(T _) + { } + + // Functions to deal with squared RGB values without making negatives useless. + + internal static float PseudoSquareRgb(float x) + => x < 0.0f ? -(x * x) : x * x; + + internal static Vector3 PseudoSquareRgb(Vector3 vec) + => new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z)); + + internal static Vector4 PseudoSquareRgb(Vector4 vec) + => new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z), vec.W); + + internal static float PseudoSqrtRgb(float x) + => x < 0.0f ? -MathF.Sqrt(-x) : MathF.Sqrt(x); + + internal static Vector3 PseudoSqrtRgb(Vector3 vec) + => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z)); + + internal static Vector4 PseudoSqrtRgb(Vector4 vec) + => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z), vec.W); +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs new file mode 100644 index 00000000..56496005 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs @@ -0,0 +1,277 @@ +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Text; +using OtterGui.Text.Widget.Editors; +using Penumbra.GameData.Files.ShaderStructs; +using static Penumbra.GameData.Files.ShpkFile; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public partial class MtrlTab +{ + private const float MaterialConstantSize = 250.0f; + + public readonly + List<(string Header, List<(string Label, int ConstantIndex, Range Slice, string Description, bool MonoFont, IEditor Editor)> + Constants)> Constants = new(16); + + 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)); + return newGroup; + } + + Constants.Clear(); + string mpPrefix; + if (AssociatedShpk == null) + { + mpPrefix = MaterialParamsConstantName.Value!; + var fcGroup = FindOrAddGroup(Constants, "Further Constants"); + foreach (var (constant, index) in Mtrl.ShaderPackage.Constants.WithIndex()) + { + var values = Mtrl.GetConstantValue(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, + ConstantEditors.DefaultFloat)); + } + } + } + else + { + mpPrefix = AssociatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? MaterialParamsConstantName.Value!; + var autoNameMaxLength = Math.Max(Names.LongestKnownNameLength, mpPrefix.Length + 8); + foreach (var shpkConstant in AssociatedShpk.MaterialParams) + { + var name = Names.KnownNames.TryResolve(shpkConstant.Id); + var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, AssociatedShpk, out var constantIndex); + var values = Mtrl.GetConstantValue(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.EffectiveByteOffset; + var length = values.Length - offset; + var constantSize = dkConstant.EffectiveByteSize; + if (constantSize.HasValue) + length = Math.Min(length, (int)constantSize.Value); + if (length <= 0) + continue; + + var editor = dkConstant.CreateEditor(_materialTemplatePickers); + 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); + } + + if (handledElements.IsFull) + continue; + + var fcGroup = FindOrAddGroup(Constants, "Further Constants"); + foreach (var (start, end) in handledElements.Ranges(complement: true)) + { + if (start == 0 && end == values.Length && end - start <= 16) + { + if (name.Value != null) + { + fcGroup.Add(( + $"{name.Value.PadRight(autoNameMaxLength)} (0x{shpkConstant.Id:X8})", + constantIndex, 0..values.Length, string.Empty, true, DefaultConstantEditorFor(name))); + continue; + } + } + + if ((shpkConstant.ByteOffset & 0x3) == 0 && (shpkConstant.ByteSize & 0x3) == 0) + { + var offset = shpkConstant.ByteOffset; + for (int i = (start & ~0xF) - (offset & 0xF), j = offset >> 4; i < end; i += 16, ++j) + { + var rangeStart = Math.Max(i, start); + var rangeEnd = Math.Min(i + 16, end); + if (rangeEnd > rangeStart) + { + var autoName = $"{mpPrefix}[{j,2:D}]{VectorSwizzle(((offset + rangeStart) & 0xF) >> 2, ((offset + rangeEnd - 1) & 0xF) >> 2)}"; + fcGroup.Add(( + $"{autoName.PadRight(autoNameMaxLength)} (0x{shpkConstant.Id:X8})", + constantIndex, rangeStart..rangeEnd, string.Empty, true, DefaultConstantEditorFor(name))); + } + } + } + else + { + for (var i = start; i < end; i += 16) + { + fcGroup.Add(($"{"???".PadRight(autoNameMaxLength)} (0x{shpkConstant.Id:X8})", constantIndex, i..Math.Min(i + 16, end), string.Empty, true, + DefaultConstantEditorFor(name))); + } + } + } + } + } + + 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, and cbuffer-location names appear after known variable names + foreach (var (_, group) in Constants) + { + group.Sort((x, y) => string.CompareOrdinal( + x.MonoFont ? x.Label.Replace("].w", "].{").Replace(mpPrefix, "}_MaterialParameter") : x.Label, + y.MonoFont ? y.Label.Replace("].w", "].{").Replace(mpPrefix, "}_MaterialParameter") : y.Label)); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private IEditor DefaultConstantEditorFor(Name name) + => ConstantEditors.DefaultFor(name, _materialTemplatePickers); + + private bool DrawConstantsSection(bool disabled) + { + if (Constants.Count == 0) + return false; + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + if (!ImGui.CollapsingHeader("Material Constants")) + return false; + + using var _ = ImRaii.PushId("MaterialConstants"); + + var ret = false; + foreach (var (header, group) in Constants) + { + using var t = ImRaii.TreeNode(header, ImGuiTreeNodeFlags.DefaultOpen); + if (!t) + continue; + + foreach (var (label, constantIndex, slice, description, monoFont, editor) in group) + { + var constant = Mtrl.ShaderPackage.Constants[constantIndex]; + var buffer = Mtrl.GetConstantValue(constant); + if (buffer.Length > 0) + { + using var id = ImRaii.PushId($"##{constant.Id:X8}:{slice.Start}"); + ImGui.SetNextItemWidth(MaterialConstantSize * UiHelpers.Scale); + if (editor.Draw(buffer[slice], disabled)) + { + ret = true; + SetMaterialParameter(constant.Id, slice.Start, buffer[slice]); + } + var shpkConstant = AssociatedShpk?.GetMaterialParamById(constant.Id); + var defaultConstantValue = shpkConstant.HasValue ? AssociatedShpk!.GetMaterialParamDefault(shpkConstant.Value) : []; + var defaultValue = IsValid(slice, defaultConstantValue.Length) ? defaultConstantValue[slice] : []; + var canReset = AssociatedShpk?.MaterialParamsDefaults != null + ? defaultValue.Length > 0 && !defaultValue.SequenceEqual(buffer[slice]) + : buffer[slice].ContainsAnyExcept((byte)0); + ImUtf8.SameLineInner(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Backspace.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Reset this constant to its default value.\n\nHold Ctrl to unlock.", !ImGui.GetIO().KeyCtrl || !canReset, true)) + { + ret = true; + if (defaultValue.Length > 0) + defaultValue.CopyTo(buffer[slice]); + else + buffer[slice].Clear(); + + 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 bool IsValid(Range range, int length) + { + var start = range.Start.GetOffset(length); + var end = range.End.GetOffset(length); + return start >= 0 && start <= length && end >= start && end <= length; + } + + internal static string? MaterialParamName(bool componentOnly, int offset) + { + if (offset < 0) + return null; + + return (componentOnly, offset & 0x3) switch + { + (true, 0) => "x", + (true, 1) => "y", + (true, 2) => "z", + (true, 3) => "w", + (false, 0) => $"[{offset >> 2:D2}].x", + (false, 1) => $"[{offset >> 2:D2}].y", + (false, 2) => $"[{offset >> 2:D2}].z", + (false, 3) => $"[{offset >> 2:D2}].w", + _ => null, + }; + } + + /// Returned string is 4 chars long. + 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, + }; + + internal static (string? Name, bool ComponentOnly) MaterialParamRangeName(string prefix, int valueOffset, int valueLength) + { + if (valueLength == 0 || valueOffset < 0) + return (null, false); + + var firstVector = valueOffset >> 2; + var lastVector = (valueOffset + valueLength - 1) >> 2; + var firstComponent = valueOffset & 0x3; + var lastComponent = (valueOffset + valueLength - 1) & 0x3; + if (firstVector == lastVector) + return ($"{prefix}[{firstVector}]{VectorSwizzle(firstComponent, lastComponent)}", true); + + var sb = new StringBuilder(128); + sb.Append($"{prefix}[{firstVector}]{VectorSwizzle(firstComponent, 3).TrimEnd()}"); + for (var i = firstVector + 1; i < lastVector; ++i) + sb.Append($", [{i}]"); + + sb.Append($", [{lastVector}]{VectorSwizzle(0, lastComponent)}"); + return (sb.ToString(), false); + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs new file mode 100644 index 00000000..cd62d58f --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs @@ -0,0 +1,240 @@ +using Newtonsoft.Json.Linq; +using OtterGui.Text.Widget.Editors; +using Penumbra.String.Classes; +using static Penumbra.GameData.Files.ShpkFile; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public partial class MtrlTab +{ + private JObject? TryLoadShpkDevkit(string shpkBaseName, out string devkitPathName) + { + try + { + 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; + } + } + + private T? TryGetShpkDevkitData(string category, uint? id, bool mayVary) where T : class + => 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 + { + if (devkit == null) + return null; + + try + { + 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]; + } + + return data?.ToObject(typeof(T)) as T; + } + catch (Exception e) + { + // 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; + } + } + + 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 = []; + } + + 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 encoded as a float. + Integer = 1, + Color = 2, + Enum = 3, + /// Native integer. + Int32 = 4, + Int32Enum = 5, + Int8 = 6, + Int8Enum = 7, + Int16 = 8, + Int16Enum = 9, + Int64 = 10, + Int64Enum = 11, + Half = 12, + Double = 13, + TileIndex = 14, + SphereMapIndex = 15, + } + + private sealed class DevkitConstantValue + { + public string Label = string.Empty; + public string Description = string.Empty; + public double Value = 0; + } + + private sealed class DevkitConstant + { + public uint Offset = 0; + public uint? Length = null; + public uint? ByteOffset = null; + public uint? ByteSize = 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 Step = 0.0f; + public float StepFast = 0.0f; + public float? Speed = null; + public float RelativeSpeed = 0.0f; + public float Exponent = 1.0f; + public float Factor = 1.0f; + public float Bias = 0.0f; + public byte Precision = 3; + public bool Hex = false; + public bool Slider = true; + public bool Drag = true; + public string Unit = string.Empty; + + public bool SquaredRgb = false; + public bool Clamped = false; + + public DevkitConstantValue[] Values = []; + + public uint EffectiveByteOffset + => ByteOffset ?? Offset * ValueSize; + + public uint? EffectiveByteSize + => ByteSize ?? (Length * ValueSize); + + public unsafe uint ValueSize + => Type switch + { + DevkitConstantType.Hidden => sizeof(byte), + DevkitConstantType.Float => sizeof(float), + DevkitConstantType.Integer => sizeof(float), + DevkitConstantType.Color => sizeof(float), + DevkitConstantType.Enum => sizeof(float), + DevkitConstantType.Int32 => sizeof(int), + DevkitConstantType.Int32Enum => sizeof(int), + DevkitConstantType.Int8 => sizeof(byte), + DevkitConstantType.Int8Enum => sizeof(byte), + DevkitConstantType.Int16 => sizeof(short), + DevkitConstantType.Int16Enum => sizeof(short), + DevkitConstantType.Int64 => sizeof(long), + DevkitConstantType.Int64Enum => sizeof(long), + DevkitConstantType.Half => (uint)sizeof(Half), + DevkitConstantType.Double => sizeof(double), + DevkitConstantType.TileIndex => sizeof(float), + DevkitConstantType.SphereMapIndex => sizeof(float), + _ => sizeof(float), + }; + + public IEditor? CreateEditor(MaterialTemplatePickers? materialTemplatePickers) + => Type switch + { + DevkitConstantType.Hidden => null, + DevkitConstantType.Float => CreateFloatEditor().AsByteEditor(), + DevkitConstantType.Integer => CreateIntegerEditor().IntAsFloatEditor().AsByteEditor(), + DevkitConstantType.Color => ColorEditor.Get(!Clamped).WithExponent(SquaredRgb ? 2.0f : 1.0f).AsByteEditor(), + DevkitConstantType.Enum => CreateEnumEditor(float.CreateSaturating).AsByteEditor(), + DevkitConstantType.Int32 => CreateIntegerEditor().AsByteEditor(), + DevkitConstantType.Int32Enum => CreateEnumEditor(ToInteger).AsByteEditor(), + DevkitConstantType.Int8 => CreateIntegerEditor(), + DevkitConstantType.Int8Enum => CreateEnumEditor(ToInteger), + DevkitConstantType.Int16 => CreateIntegerEditor().AsByteEditor(), + DevkitConstantType.Int16Enum => CreateEnumEditor(ToInteger).AsByteEditor(), + DevkitConstantType.Int64 => CreateIntegerEditor().AsByteEditor(), + DevkitConstantType.Int64Enum => CreateEnumEditor(ToInteger).AsByteEditor(), + DevkitConstantType.Half => CreateFloatEditor().AsByteEditor(), + DevkitConstantType.Double => CreateFloatEditor().AsByteEditor(), + DevkitConstantType.TileIndex => materialTemplatePickers?.TileIndexPicker ?? ConstantEditors.DefaultIntAsFloat, + DevkitConstantType.SphereMapIndex => materialTemplatePickers?.SphereMapIndexPicker ?? ConstantEditors.DefaultIntAsFloat, + _ => ConstantEditors.DefaultFloat, + }; + + private IEditor CreateIntegerEditor() + where T : unmanaged, INumber + => ((Drag || Slider) && !Hex + ? (Drag + ? (IEditor)DragEditor.CreateInteger(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed, Unit, 0) + : SliderEditor.CreateInteger(ToInteger(Minimum) ?? default, ToInteger(Maximum) ?? default, Unit, 0)) + : InputEditor.CreateInteger(ToInteger(Minimum), ToInteger(Maximum), ToInteger(Step), ToInteger(StepFast), Hex, Unit, 0)) + .WithFactorAndBias(ToInteger(Factor), ToInteger(Bias)); + + private IEditor CreateFloatEditor() + where T : unmanaged, INumber, IPowerFunctions + => ((Drag || Slider) + ? (Drag + ? (IEditor)DragEditor.CreateFloat(ToFloat(Minimum), ToFloat(Maximum), Speed ?? 0.1f, RelativeSpeed, Precision, Unit, 0) + : SliderEditor.CreateFloat(ToFloat(Minimum) ?? default, ToFloat(Maximum) ?? default, Precision, Unit, 0)) + : InputEditor.CreateFloat(ToFloat(Minimum), ToFloat(Maximum), T.CreateSaturating(Step), T.CreateSaturating(StepFast), Precision, Unit, 0)) + .WithExponent(T.CreateSaturating(Exponent)) + .WithFactorAndBias(T.CreateSaturating(Factor), T.CreateSaturating(Bias)); + + private EnumEditor CreateEnumEditor(Func convertValue) + where T : unmanaged, IUtf8SpanFormattable, IEqualityOperators + => new(Array.ConvertAll(Values, value => (ToUtf8(value.Label), convertValue(value.Value), ToUtf8(value.Description)))); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T ToInteger(float value) where T : struct, INumberBase + => T.CreateSaturating(MathF.Round(value)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T ToInteger(double value) where T : struct, INumberBase + => T.CreateSaturating(Math.Round(value)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T? ToInteger(float? value) where T : struct, INumberBase + => value.HasValue ? ToInteger(value.Value) : null; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T? ToFloat(float? value) where T : struct, INumberBase + => value.HasValue ? T.CreateSaturating(value.Value) : null; + + private static ReadOnlyMemory ToUtf8(string value) + => Encoding.UTF8.GetBytes(value); + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs new file mode 100644 index 00000000..f3ec5307 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs @@ -0,0 +1,368 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using OtterGui; +using OtterGui.Text; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Files.StainMapStructs; +using Penumbra.Services; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public partial class MtrlTab +{ + private const float LegacyColorTableFloatSize = 65.0f; + private const float LegacyColorTablePercentageSize = 50.0f; + private const float LegacyColorTableIntegerSize = 40.0f; + private const float LegacyColorTableByteSize = 25.0f; + + private bool DrawLegacyColorTable(LegacyColorTable table, LegacyColorDyeTable? dyeTable, bool disabled) + { + using var imTable = ImUtf8.Table("##ColorTable"u8, dyeTable != null ? 10 : 8, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV); + if (!imTable) + return false; + + DrawLegacyColorTableHeader(dyeTable != null); + + var ret = false; + for (var i = 0; i < LegacyColorTable.NumRows; ++i) + { + if (DrawLegacyColorTableRow(table, dyeTable, i, disabled)) + { + UpdateColorTableRowPreview(i); + ret = true; + } + ImGui.TableNextRow(); + } + + return ret; + } + + private bool DrawLegacyColorTable(ColorTable table, ColorDyeTable? dyeTable, bool disabled) + { + using var imTable = ImUtf8.Table("##ColorTable"u8, dyeTable != null ? 10 : 8, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV); + if (!imTable) + return false; + + DrawLegacyColorTableHeader(dyeTable != null); + + var ret = false; + for (var i = 0; i < ColorTable.NumRows; ++i) + { + if (DrawLegacyColorTableRow(table, dyeTable, i, disabled)) + { + UpdateColorTableRowPreview(i); + ret = true; + } + ImGui.TableNextRow(); + } + + return ret; + } + + private static void DrawLegacyColorTableHeader(bool hasDyeTable) + { + ImGui.TableNextColumn(); + ImUtf8.TableHeader(default(ReadOnlySpan)); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Row"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Diffuse"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Specular"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Emissive"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Gloss"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Tile"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Repeat / Skew"u8); + if (hasDyeTable) + { + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Dye"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Dye Preview"u8); + } + } + + private bool DrawLegacyColorTableRow(LegacyColorTable table, LegacyColorDyeTable? dyeTable, int rowIdx, bool disabled) + { + using var id = ImRaii.PushId(rowIdx); + ref var row = ref table[rowIdx]; + var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var floatSize = LegacyColorTableFloatSize * UiHelpers.Scale; + var pctSize = LegacyColorTablePercentageSize * UiHelpers.Scale; + var intSize = LegacyColorTableIntegerSize * UiHelpers.Scale; + ImGui.TableNextColumn(); + ColorTableCopyClipboardButton(rowIdx); + ImUtf8.SameLineInner(); + var ret = ColorTablePasteFromClipboardButton(rowIdx, disabled); + if ((rowIdx & 1) == 0) + { + ImUtf8.SameLineInner(); + ColorTableHighlightButton(rowIdx >> 1, disabled); + } + + ImGui.TableNextColumn(); + using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) + ImUtf8.Text($"{(rowIdx >> 1) + 1,2:D}{"AB"[rowIdx & 1]}"); + + ImGui.TableNextColumn(); + using var dis = ImRaii.Disabled(disabled); + ret |= CtColorPicker("##Diffuse"u8, "Diffuse Color"u8, row.DiffuseColor, + c => table[rowIdx].DiffuseColor = c); + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeDiffuse"u8, "Apply Diffuse Color on Dye"u8, dye.DiffuseColor, + b => dyeTable[rowIdx].DiffuseColor = b); + } + + ImGui.TableNextColumn(); + ret |= CtColorPicker("##Specular"u8, "Specular Color"u8, row.SpecularColor, + c => table[rowIdx].SpecularColor = c); + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeSpecular"u8, "Apply Specular Color on Dye"u8, dye.SpecularColor, + b => dyeTable[rowIdx].SpecularColor = b); + } + ImGui.SameLine(); + ImGui.SetNextItemWidth(pctSize); + ret |= CtDragScalar("##SpecularMask"u8, "Specular Strength"u8, (float)row.SpecularMask * 100.0f, "%.0f%%"u8, 0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].SpecularMask = (Half)(v * 0.01f)); + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeSpecularMask"u8, "Apply Specular Strength on Dye"u8, dye.SpecularMask, + b => dyeTable[rowIdx].SpecularMask = b); + } + + ImGui.TableNextColumn(); + ret |= CtColorPicker("##Emissive"u8, "Emissive Color"u8, row.EmissiveColor, + c => table[rowIdx].EmissiveColor = c); + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeEmissive"u8, "Apply Emissive Color on Dye"u8, dye.EmissiveColor, + b => dyeTable[rowIdx].EmissiveColor = b); + } + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(floatSize); + var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon; + ret |= CtDragHalf("##Shininess"u8, "Gloss Strength"u8, row.Shininess, "%.1f"u8, glossStrengthMin, HalfMaxValue, Math.Max(0.1f, (float)row.Shininess * 0.025f), + v => table[rowIdx].Shininess = v); + + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeShininess"u8, "Apply Gloss Strength on Dye"u8, dye.Shininess, + b => dyeTable[rowIdx].Shininess = b); + } + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(intSize); + ret |= CtTileIndexPicker("##TileIndex"u8, "Tile Index"u8, row.TileIndex, true, + value => table[rowIdx].TileIndex = value); + + ImGui.TableNextColumn(); + ret |= CtTileTransformMatrix(row.TileTransform, floatSize, false, + m => table[rowIdx].TileTransform = m); + + if (dyeTable != null) + { + ImGui.TableNextColumn(); + if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) + { + dyeTable[rowIdx].Template = _stainService.LegacyTemplateCombo.CurrentSelection; + ret = true; + } + + ImGuiUtil.HoverTooltip("Dye Template", ImGuiHoveredFlags.AllowWhenDisabled); + + ImGui.TableNextColumn(); + ret |= DrawLegacyDyePreview(rowIdx, disabled, dye, floatSize); + } + + return ret; + } + + private bool DrawLegacyColorTableRow(ColorTable table, ColorDyeTable? dyeTable, int rowIdx, bool disabled) + { + using var id = ImRaii.PushId(rowIdx); + ref var row = ref table[rowIdx]; + var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var floatSize = LegacyColorTableFloatSize * UiHelpers.Scale; + var pctSize = LegacyColorTablePercentageSize * UiHelpers.Scale; + var intSize = LegacyColorTableIntegerSize * UiHelpers.Scale; + var byteSize = LegacyColorTableByteSize * UiHelpers.Scale; + ImGui.TableNextColumn(); + ColorTableCopyClipboardButton(rowIdx); + ImUtf8.SameLineInner(); + var ret = ColorTablePasteFromClipboardButton(rowIdx, disabled); + if ((rowIdx & 1) == 0) + { + ImUtf8.SameLineInner(); + ColorTableHighlightButton(rowIdx >> 1, disabled); + } + + ImGui.TableNextColumn(); + using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) + ImUtf8.Text($"{(rowIdx >> 1) + 1,2:D}{"AB"[rowIdx & 1]}"); + + ImGui.TableNextColumn(); + using var dis = ImRaii.Disabled(disabled); + ret |= CtColorPicker("##Diffuse"u8, "Diffuse Color"u8, row.DiffuseColor, + c => table[rowIdx].DiffuseColor = c); + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeDiffuse"u8, "Apply Diffuse Color on Dye"u8, dye.DiffuseColor, + b => dyeTable[rowIdx].DiffuseColor = b); + } + + ImGui.TableNextColumn(); + ret |= CtColorPicker("##Specular"u8, "Specular Color"u8, row.SpecularColor, + c => table[rowIdx].SpecularColor = c); + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeSpecular"u8, "Apply Specular Color on Dye"u8, dye.SpecularColor, + b => dyeTable[rowIdx].SpecularColor = b); + } + ImGui.SameLine(); + ImGui.SetNextItemWidth(pctSize); + ret |= CtDragScalar("##SpecularMask"u8, "Specular Strength"u8, (float)row.Scalar7 * 100.0f, "%.0f%%"u8, 0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].Scalar7 = (Half)(v * 0.01f)); + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeSpecularMask"u8, "Apply Specular Strength on Dye"u8, dye.Metalness, + b => dyeTable[rowIdx].Metalness = b); + } + + ImGui.TableNextColumn(); + ret |= CtColorPicker("##Emissive"u8, "Emissive Color"u8, row.EmissiveColor, + c => table[rowIdx].EmissiveColor = c); + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeEmissive"u8, "Apply Emissive Color on Dye"u8, dye.EmissiveColor, + b => dyeTable[rowIdx].EmissiveColor = b); + } + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(floatSize); + var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon; + ret |= CtDragHalf("##Shininess"u8, "Gloss Strength"u8, row.Scalar3, "%.1f"u8, glossStrengthMin, HalfMaxValue, Math.Max(0.1f, (float)row.Scalar3 * 0.025f), + v => table[rowIdx].Scalar3 = v); + + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeShininess"u8, "Apply Gloss Strength on Dye"u8, dye.Scalar3, + b => dyeTable[rowIdx].Scalar3 = b); + } + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(intSize); + ret |= CtTileIndexPicker("##TileIndex"u8, "Tile Index"u8, row.TileIndex, true, + value => table[rowIdx].TileIndex = value); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(pctSize); + ret |= CtDragScalar("##TileAlpha"u8, "Tile Opacity"u8, (float)row.TileAlpha * 100.0f, "%.0f%%"u8, 0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].TileAlpha = (Half)(v * 0.01f)); + + ImGui.TableNextColumn(); + ret |= CtTileTransformMatrix(row.TileTransform, floatSize, false, + m => table[rowIdx].TileTransform = m); + + if (dyeTable != null) + { + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(byteSize); + ret |= CtDragScalar("##DyeChannel"u8, "Dye Channel"u8, dye.Channel + 1, "%hhd"u8, 1, StainService.ChannelCount, 0.25f, + value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1)); + ImUtf8.SameLineInner(); + _stainService.LegacyTemplateCombo.CurrentDyeChannel = dye.Channel; + if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) + { + dyeTable[rowIdx].Template = _stainService.LegacyTemplateCombo.CurrentSelection; + ret = true; + } + + ImGuiUtil.HoverTooltip("Dye Template", ImGuiHoveredFlags.AllowWhenDisabled); + + ImGui.TableNextColumn(); + ret |= DrawLegacyDyePreview(rowIdx, disabled, dye, floatSize); + } + + return ret; + } + + private bool DrawLegacyDyePreview(int rowIdx, bool disabled, LegacyColorDyeTable.Row dye, float floatSize) + { + var stain = _stainService.StainCombo1.CurrentSelection.Key; + if (stain == 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values)) + return false; + + 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); + + ret = ret && Mtrl.ApplyDyeToRow(_stainService.LegacyStmFile, [stain], rowIdx); + + ImGui.SameLine(); + DrawLegacyDyePreview(values, floatSize); + + return ret; + } + + private bool DrawLegacyDyePreview(int rowIdx, bool disabled, ColorDyeTable.Row dye, float floatSize) + { + var stain = _stainService.GetStainCombo(dye.Channel).CurrentSelection.Key; + if (stain == 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values)) + return false; + + 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); + + ret = ret && Mtrl.ApplyDyeToRow(_stainService.LegacyStmFile, [ + _stainService.StainCombo1.CurrentSelection.Key, + _stainService.StainCombo2.CurrentSelection.Key, + ], rowIdx); + + ImGui.SameLine(); + DrawLegacyDyePreview(values, floatSize); + + return ret; + } + + private static void DrawLegacyDyePreview(LegacyDyePack values, float floatSize) + { + CtColorPicker("##diffusePreview"u8, default, values.DiffuseColor, "D"u8); + ImUtf8.SameLineInner(); + CtColorPicker("##specularPreview"u8, default, values.SpecularColor, "S"u8); + ImUtf8.SameLineInner(); + CtColorPicker("##emissivePreview"u8, default, values.EmissiveColor, "E"u8); + ImUtf8.SameLineInner(); + using var dis = ImRaii.Disabled(); + ImGui.SetNextItemWidth(floatSize); + var shininess = (float)values.Shininess; + ImGui.DragFloat("##shininessPreview", ref shininess, 0, shininess, shininess, "%.1f G"); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(floatSize); + var specularMask = (float)values.SpecularMask * 100.0f; + ImGui.DragFloat("##specularMaskPreview", ref specularMask, 0, specularMask, specularMask, "%.0f%% S"); + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs new file mode 100644 index 00000000..bb346534 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs @@ -0,0 +1,272 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Structs; +using Penumbra.Interop.MaterialPreview; +using Penumbra.Services; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public partial class MtrlTab +{ + public readonly List MaterialPreviewers = new(4); + public readonly List ColorTablePreviewers = new(4); + public int HighlightedColorTablePair = -1; + public readonly Stopwatch HighlightTime = new(); + + private void DrawMaterialLivePreviewRebind(bool disabled) + { + if (disabled) + return; + + if (ImGui.Button("Reload live preview")) + BindToMaterialInstances(); + + if (MaterialPreviewers.Count != 0 || ColorTablePreviewers.Count != 0) + return; + + ImGui.SameLine(); + using var c = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); + ImUtf8.Text( + "The current material has not been found on your character. Please check the Import from Screen tab for more information."u8); + } + + public unsafe void BindToMaterialInstances() + { + UnbindFromMaterialInstances(); + + var instances = MaterialInfo.FindMaterials(_resourceTreeFactory.GetLocalPlayerRelatedCharacters().Select(ch => ch.Address), + FilePath); + + var foundMaterials = new HashSet(); + foreach (var materialInfo in instances) + { + var material = materialInfo.GetDrawObjectMaterial(_objects); + if (foundMaterials.Contains((nint)material)) + continue; + + try + { + MaterialPreviewers.Add(new LiveMaterialPreviewer(_objects, materialInfo)); + foundMaterials.Add((nint)material); + } + catch (InvalidOperationException) + { + // Carry on without that previewer. + } + } + + UpdateMaterialPreview(); + + if (Mtrl.Table == null) + return; + + foreach (var materialInfo in instances) + { + try + { + ColorTablePreviewers.Add(new LiveColorTablePreviewer(_objects, _framework, materialInfo)); + } + catch (InvalidOperationException) + { + // Carry on without that previewer. + } + } + + UpdateColorTablePreview(); + } + + private void UnbindFromMaterialInstances() + { + foreach (var previewer in MaterialPreviewers) + previewer.Dispose(); + MaterialPreviewers.Clear(); + + foreach (var previewer in ColorTablePreviewers) + previewer.Dispose(); + ColorTablePreviewers.Clear(); + } + + private unsafe void UnbindFromDrawObjectMaterialInstances(CharacterBase* characterBase) + { + for (var i = MaterialPreviewers.Count; i-- > 0;) + { + var previewer = MaterialPreviewers[i]; + if (previewer.DrawObject != characterBase) + continue; + + previewer.Dispose(); + MaterialPreviewers.RemoveAt(i); + } + + for (var i = ColorTablePreviewers.Count; i-- > 0;) + { + var previewer = ColorTablePreviewers[i]; + if (previewer.DrawObject != characterBase) + continue; + + previewer.Dispose(); + ColorTablePreviewers.RemoveAt(i); + } + } + + 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); + } + + private void UpdateMaterialPreview() + { + SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); + foreach (var constant in Mtrl.ShaderPackage.Constants) + { + var values = Mtrl.GetConstantValue(constant); + if (values != null) + SetMaterialParameter(constant.Id, 0, values); + } + + foreach (var sampler in Mtrl.ShaderPackage.Samplers) + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + public void HighlightColorTablePair(int pairIdx) + { + var oldPairIdx = HighlightedColorTablePair; + + if (HighlightedColorTablePair != pairIdx) + { + HighlightedColorTablePair = pairIdx; + HighlightTime.Restart(); + } + + if (oldPairIdx >= 0) + { + UpdateColorTableRowPreview(oldPairIdx << 1); + UpdateColorTableRowPreview((oldPairIdx << 1) | 1); + } + if (pairIdx >= 0) + { + UpdateColorTableRowPreview(pairIdx << 1); + UpdateColorTableRowPreview((pairIdx << 1) | 1); + } + } + + public void CancelColorTableHighlight() + { + var pairIdx = HighlightedColorTablePair; + + HighlightedColorTablePair = -1; + HighlightTime.Reset(); + + if (pairIdx >= 0) + { + UpdateColorTableRowPreview(pairIdx << 1); + UpdateColorTableRowPreview((pairIdx << 1) | 1); + } + } + + public void UpdateColorTableRowPreview(int rowIdx) + { + if (ColorTablePreviewers.Count == 0) + return; + + if (Mtrl.Table == null) + return; + + var row = Mtrl.Table switch + { + LegacyColorTable legacyTable => new ColorTable.Row(legacyTable[rowIdx]), + ColorTable table => table[rowIdx], + _ => throw new InvalidOperationException($"Unsupported color table type {Mtrl.Table.GetType()}"), + }; + if (Mtrl.DyeTable != null) + { + var dyeRow = Mtrl.DyeTable switch + { + LegacyColorDyeTable legacyDyeTable => new ColorDyeTable.Row(legacyDyeTable[rowIdx]), + ColorDyeTable dyeTable => dyeTable[rowIdx], + _ => throw new InvalidOperationException($"Unsupported color dye table type {Mtrl.DyeTable.GetType()}"), + }; + if (dyeRow.Channel < StainService.ChannelCount) + { + StainId stainId = _stainService.GetStainCombo(dyeRow.Channel).CurrentSelection.Key; + if (_stainService.LegacyStmFile.TryGetValue(dyeRow.Template, stainId, out var legacyDyes)) + row.ApplyDye(dyeRow, legacyDyes); + if (_stainService.GudStmFile.TryGetValue(dyeRow.Template, stainId, out var gudDyes)) + row.ApplyDye(dyeRow, gudDyes); + } + } + + if (HighlightedColorTablePair << 1 == rowIdx) + ApplyHighlight(ref row, ColorId.InGameHighlight, (float)HighlightTime.Elapsed.TotalSeconds); + else if (((HighlightedColorTablePair << 1) | 1) == rowIdx) + ApplyHighlight(ref row, ColorId.InGameHighlight2, (float)HighlightTime.Elapsed.TotalSeconds); + + foreach (var previewer in ColorTablePreviewers) + { + row[..].CopyTo(previewer.GetColorRow(rowIdx)); + previewer.ScheduleUpdate(); + } + } + + public void UpdateColorTablePreview() + { + if (ColorTablePreviewers.Count == 0) + return; + + if (Mtrl.Table == null) + return; + + var rows = new ColorTable(Mtrl.Table); + var dyeRows = Mtrl.DyeTable != null ? ColorDyeTable.CastOrConvert(Mtrl.DyeTable) : null; + if (dyeRows != null) + { + ReadOnlySpan stainIds = [ + _stainService.StainCombo1.CurrentSelection.Key, + _stainService.StainCombo2.CurrentSelection.Key, + ]; + rows.ApplyDye(_stainService.LegacyStmFile, stainIds, dyeRows); + rows.ApplyDye(_stainService.GudStmFile, stainIds, dyeRows); + } + + if (HighlightedColorTablePair >= 0) + { + ApplyHighlight(ref rows[HighlightedColorTablePair << 1], ColorId.InGameHighlight, (float)HighlightTime.Elapsed.TotalSeconds); + ApplyHighlight(ref rows[(HighlightedColorTablePair << 1) | 1], ColorId.InGameHighlight2, (float)HighlightTime.Elapsed.TotalSeconds); + } + + foreach (var previewer in ColorTablePreviewers) + { + rows.AsHalves().CopyTo(previewer.ColorTable); + previewer.ScheduleUpdate(); + } + } + + private static void ApplyHighlight(ref ColorTable.Row row, ColorId colorId, float time) + { + var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; + var baseColor = colorId.Value(); + var color = level * new Vector3(baseColor & 0xFF, (baseColor >> 8) & 0xFF, (baseColor >> 16) & 0xFF); + var halfColor = (HalfColor)(color * color); + + row.DiffuseColor = halfColor; + row.SpecularColor = halfColor; + row.EmissiveColor = halfColor; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs new file mode 100644 index 00000000..21557939 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs @@ -0,0 +1,505 @@ +using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; +using ImGuiNET; +using Newtonsoft.Json.Linq; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.GameData; +using Penumbra.GameData.Data; +using Penumbra.GameData.Files; +using Penumbra.GameData.Files.ShaderStructs; +using Penumbra.String.Classes; +using static Penumbra.GameData.Files.ShpkFile; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public partial class MtrlTab +{ + // 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 severe performance/memory issues when calculating the effective shader set + // 2. they probably aren't intended for use with materials anyway + internal static readonly IReadOnlyList StandardShaderPackages = new[] + { + "3dui.shpk", + // "apricot_decal_dummy.shpk", + // "apricot_decal_ring.shpk", + // "apricot_decal.shpk", + // "apricot_fogModel.shpk", + // "apricot_gbuffer_decal_dummy.shpk", + // "apricot_gbuffer_decal_ring.shpk", + // "apricot_gbuffer_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", + "bg_composite.shpk", + "bgcrestchange.shpk", + "bgdecal.shpk", + "bgprop.shpk", + "bg.shpk", + "bguvscroll.shpk", + "characterglass.shpk", + "characterinc.shpk", + "characterlegacy.shpk", + "characterocclusion.shpk", + "characterreflection.shpk", + "characterscroll.shpk", + "charactershadowoffset.shpk", + "character.shpk", + "characterstockings.shpk", + "charactertattoo.shpk", + "charactertransparency.shpk", + "cloud.shpk", + "createviewposition.shpk", + "crystal.shpk", + "directionallighting.shpk", + "directionalshadow.shpk", + "furblur.shpk", + "grassdynamicwave.shpk", + "grass.shpk", + "hairmask.shpk", + "hair.shpk", + "iris.shpk", + "lightshaft.shpk", + "linelighting.shpk", + "planelighting.shpk", + "pointlighting.shpk", + "river.shpk", + "shadowmask.shpk", + "skin.shpk", + "spotlighting.shpk", + "subsurfaceblur.shpk", + "verticalfog.shpk", + "water.shpk", + "weather.shpk", + }; + + private static readonly byte[] UnknownShadersString = Encoding.UTF8.GetBytes("Vertex Shaders: ???\nPixel Shaders: ???"); + + 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 bool ShpkLoading; + public JObject? AssociatedShpkDevkit; + + 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 HashSet VertexShaders = new(16); + public readonly HashSet PixelShaders = new(16); + public bool ShadersKnown; + public ReadOnlyMemory ShadersString = UnknownShadersString; + + 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 FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath) + { + defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name); + if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath)) + return FullPath.Empty; + + return _edit.FindBestMatch(defaultGamePath); + } + + public void LoadShpk(FullPath path) + => Task.Run(() => DoLoadShpk(path)); + + private async Task DoLoadShpk(FullPath path) + { + ShadersKnown = false; + ShaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader"; + ShpkLoading = true; + + try + { + var data = path.IsRooted + ? await File.ReadAllBytesAsync(path.FullName) + : _gameData.GetFile(path.InternalName.ToString())?.Data; + LoadedShpkPath = path; + AssociatedShpk = data?.Length > 0 ? new ShpkFile(data) : throw new Exception("Failure to load file data."); + LoadedShpkPathName = path.ToPath(); + } + catch (Exception e) + { + LoadedShpkPath = FullPath.Empty; + LoadedShpkPathName = string.Empty; + AssociatedShpk = null; + Penumbra.Messager.NotificationMessage(e, $"Could not load {LoadedShpkPath.ToPath()}.", NotificationType.Error, false); + } + finally + { + ShpkLoading = false; + } + + if (LoadedShpkPath.InternalName.IsEmpty) + { + AssociatedShpkDevkit = null; + LoadedShpkDevkitPathName = string.Empty; + } + else + { + AssociatedShpkDevkit = + TryLoadShpkDevkit(Path.GetFileNameWithoutExtension(Mtrl.ShaderPackage.Name), out LoadedShpkDevkitPathName); + } + + UpdateShaderKeys(); + _updateOnNextFrame = true; + } + + private void UpdateShaderKeys() + { + ShaderKeys.Clear(); + if (AssociatedShpk != null) + foreach (var key in AssociatedShpk.MaterialKeys) + { + var keyName = Names.KnownNames.TryResolve(key.Id); + 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 valueKnownNames = keyName.WithKnownSuffixes(); + + var mtrlKeyIndex = Mtrl.FindOrAddShaderKey(key.Id, key.DefaultValue); + var values = valueSet.Select(value => + { + var valueName = valueKnownNames.TryResolve(Names.KnownNames, value); + if (dkData != null && dkData.Values.TryGetValue(value, out var dkValue)) + return (dkValue.Label.Length > 0 ? dkValue.Label : valueName.ToString(), value, dkValue.Description); + + return (valueName.ToString(), value, string.Empty); + }).ToArray(); + Array.Sort(values, (x, y) => + { + if (x.Value == key.DefaultValue) + return -1; + if (y.Value == key.DefaultValue) + return 1; + + return string.Compare(x.Label, y.Label, StringComparison.Ordinal); + }); + ShaderKeys.Add((hasDkLabel ? dkData!.Label : keyName.ToString(), mtrlKeyIndex, dkData?.Description ?? string.Empty, + !hasDkLabel, values)); + } + else + foreach (var (key, index) in Mtrl.ShaderPackage.ShaderKeys.WithIndex()) + { + var keyName = Names.KnownNames.TryResolve(key.Category); + var valueName = keyName.WithKnownSuffixes().TryResolve(Names.KnownNames, key.Value); + ShaderKeys.Add((keyName.ToString(), index, string.Empty, true, [(valueName.ToString(), key.Value, string.Empty)])); + } + } + + private void UpdateShaders() + { + static void AddShader(HashSet globalSet, Dictionary> byPassSets, uint passId, int shaderIndex) + { + globalSet.Add(shaderIndex); + if (!byPassSets.TryGetValue(passId, out var passSet)) + { + passSet = []; + byPassSets.Add(passId, passSet); + } + passSet.Add(shaderIndex); + } + + VertexShaders.Clear(); + PixelShaders.Clear(); + + var vertexShadersByPass = new Dictionary>(); + var pixelShadersByPass = new Dictionary>(); + + if (AssociatedShpk == null || !AssociatedShpk.IsExhaustiveNodeAnalysisFeasible()) + { + 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) + { + foreach (var sceneKeySelector in sceneKeySelectors) + { + foreach (var subViewKeySelector in subViewKeySelectors) + { + var selector = BuildSelector(systemKeySelector, sceneKeySelector, materialKeySelector, subViewKeySelector); + var node = AssociatedShpk.GetNodeBySelector(selector); + if (node.HasValue) + foreach (var pass in node.Value.Passes) + { + AddShader(VertexShaders, vertexShadersByPass, pass.Id, (int)pass.VertexShader); + AddShader(PixelShaders, pixelShadersByPass, pass.Id, (int)pass.PixelShader); + } + else + ShadersKnown = false; + } + } + } + } + + if (ShadersKnown) + { + var builder = new StringBuilder(); + foreach (var (passId, passVS) in vertexShadersByPass) + { + if (builder.Length > 0) + builder.Append("\n\n"); + + var passName = Names.KnownNames.TryResolve(passId); + var shaders = passVS.OrderBy(i => i).Select(i => $"#{i}"); + builder.Append($"Vertex Shaders ({passName}): {string.Join(", ", shaders)}"); + if (pixelShadersByPass.TryGetValue(passId, out var passPS)) + { + shaders = passPS.OrderBy(i => i).Select(i => $"#{i}"); + builder.Append($"\nPixel Shaders ({passName}): {string.Join(", ", shaders)}"); + } + } + foreach (var (passId, passPS) in pixelShadersByPass) + { + if (vertexShadersByPass.ContainsKey(passId)) + continue; + + if (builder.Length > 0) + builder.Append("\n\n"); + + var passName = Names.KnownNames.TryResolve(passId); + var shaders = passPS.OrderBy(i => i).Select(i => $"#{i}"); + builder.Append($"Pixel Shaders ({passName}): {string.Join(", ", shaders)}"); + } + + ShadersString = Encoding.UTF8.GetBytes(builder.ToString()); + } + else + ShadersString = UnknownShadersString; + + ShaderComment = TryGetShpkDevkitData("Comment", null, true) ?? string.Empty; + } + + private bool DrawShaderSection(bool disabled) + { + var ret = false; + if (ImGui.CollapsingHeader(ShaderHeader)) + { + ret |= DrawPackageNameInput(disabled); + ret |= DrawShaderFlagsInput(disabled); + DrawCustomAssociations(); + ret |= DrawMaterialShaderKeys(disabled); + DrawMaterialShaders(); + } + + if (!ShpkLoading && (AssociatedShpk == null || AssociatedShpkDevkit == null)) + { + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + + if (AssociatedShpk == null) + { + ImUtf8.Text("Unable to find a suitable shader (.shpk) file for cross-references. Some functionality will be missing."u8, + ImGuiUtil.HalfBlendText(0x80u)); // Half red + } + else + { + ImUtf8.Text("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."u8, + ImGuiUtil.HalfBlendText(0x8080u)); // Half yellow + } + } + + return ret; + } + + private bool DrawPackageNameInput(bool disabled) + { + if (disabled) + { + ImGui.TextUnformatted("Shader Package: " + Mtrl.ShaderPackage.Name); + return false; + } + + var ret = false; + ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); + using var c = ImRaii.Combo("Shader Package", Mtrl.ShaderPackage.Name); + if (c) + foreach (var value in GetShpkNames()) + { + if (ImGui.Selectable(value, value == Mtrl.ShaderPackage.Name)) + { + Mtrl.ShaderPackage.Name = value; + ret = true; + AssociatedShpk = null; + LoadedShpkPath = FullPath.Empty; + LoadShpk(FindAssociatedShpk(out _, out _)); + } + } + + return ret; + } + + private bool DrawShaderFlagsInput(bool disabled) + { + var shpkFlags = (int)Mtrl.ShaderPackage.Flags; + ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); + if (!ImGui.InputInt("Shader Flags", ref shpkFlags, 0, 0, + ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) + return false; + + Mtrl.ShaderPackage.Flags = (uint)shpkFlags; + SetShaderPackageFlags((uint)shpkFlags); + return true; + } + + /// + /// Show the currently associated shpk file, if any, and the buttons to associate + /// a specific shpk from your drive, the modded shpk by path or the default shpk. + /// + private void DrawCustomAssociations() + { + const string tooltip = "Click to copy file path to clipboard."; + var text = AssociatedShpk == null + ? "Associated .shpk file: None" + : $"Associated .shpk file: {LoadedShpkPathName}"; + var devkitText = AssociatedShpkDevkit == null + ? "Associated dev-kit file: None" + : $"Associated dev-kit file: {LoadedShpkDevkitPathName}"; + var baseDevkitText = AssociatedBaseDevkit == null + ? "Base dev-kit file: None" + : $"Base dev-kit file: {LoadedBaseDevkitPathName}"; + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + + ImGuiUtil.CopyOnClickSelectable(text, LoadedShpkPathName, tooltip); + ImGuiUtil.CopyOnClickSelectable(devkitText, LoadedShpkDevkitPathName, tooltip); + ImGuiUtil.CopyOnClickSelectable(baseDevkitText, LoadedBaseDevkitPathName, tooltip); + + if (ImGui.Button("Associate Custom .shpk File")) + _fileDialog.OpenFilePicker("Associate Custom .shpk File...", ".shpk", (success, name) => + { + if (success) + LoadShpk(new FullPath(name[0])); + }, 1, _edit.Mod!.ModPath.FullName, false); + + var moddedPath = FindAssociatedShpk(out var defaultPath, out var gamePath); + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Associate Default .shpk File", Vector2.Zero, moddedPath.ToPath(), + moddedPath.Equals(LoadedShpkPath))) + LoadShpk(moddedPath); + + if (!gamePath.Path.Equals(moddedPath.InternalName)) + { + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Associate Unmodded .shpk File", Vector2.Zero, defaultPath, + gamePath.Path.Equals(LoadedShpkPath.InternalName))) + LoadShpk(new FullPath(gamePath)); + } + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + } + + private bool DrawMaterialShaderKeys(bool disabled) + { + if (ShaderKeys.Count == 0) + return false; + + var ret = false; + foreach (var (label, index, description, monoFont, values) in ShaderKeys) + { + using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); + ref var key = ref Mtrl.ShaderPackage.ShaderKeys[index]; + var shpkKey = 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 (c) + foreach (var (valueLabel, value, valueDescription) in values) + { + if (ImGui.Selectable(valueLabel, value == currentValue)) + { + key.Value = value; + ret = true; + Update(); + } + + if (valueDescription.Length > 0) + ImGuiUtil.SelectableHelpMarker(valueDescription); + } + } + + 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 void DrawMaterialShaders() + { + if (AssociatedShpk == null) + return; + + using (var node = ImUtf8.TreeNode("Candidate Shaders"u8)) + { + if (node) + ImUtf8.Text(ShadersString.Span); + } + + if (ShaderComment.Length > 0) + { + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ImGui.TextUnformatted(ShaderComment); + } + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs new file mode 100644 index 00000000..3181dafe --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs @@ -0,0 +1,276 @@ +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.GameData; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.String.Classes; +using static Penumbra.GameData.Files.MaterialStructs.SamplerFlags; +using static Penumbra.GameData.Files.ShpkFile; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public partial class MtrlTab +{ + public readonly List<(string Label, int TextureIndex, int SamplerIndex, string Description, bool MonoFont)> Textures = new(4); + + public readonly HashSet UnfoldedTextures = new(4); + public readonly HashSet SamplerIds = new(16); + public float TextureLabelWidth; + + private void UpdateTextures() + { + Textures.Clear(); + SamplerIds.Clear(); + if (AssociatedShpk == null) + { + SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); + if (Mtrl.Table != null) + SamplerIds.Add(TableSamplerId); + + foreach (var (sampler, index) in Mtrl.ShaderPackage.Samplers.WithIndex()) + Textures.Add(($"0x{sampler.SamplerId:X8}", sampler.TextureIndex, index, string.Empty, true)); + } + else + { + 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) + { + SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); + if (Mtrl.Table != null) + SamplerIds.Add(TableSamplerId); + } + + foreach (var samplerId in SamplerIds) + { + var shpkSampler = AssociatedShpk.GetSamplerById(samplerId); + if (shpkSampler is not { Slot: 2 }) + continue; + + 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)); + } + + if (SamplerIds.Contains(TableSamplerId)) + Mtrl.Table ??= new ColorTable(); + } + + 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 = TextureLabelWidth / UiHelpers.Scale + 4; + } + + private static ReadOnlySpan TextureAddressModeTooltip(TextureAddressMode addressMode) + => addressMode switch + { + TextureAddressMode.Wrap => "Tile the texture at every UV integer junction.\n\nFor example, for U values between 0 and 3, the texture is repeated three times."u8, + TextureAddressMode.Mirror => "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."u8, + TextureAddressMode.Clamp => "Texture coordinates outside the range [0.0, 1.0] are set to the texture color at 0.0 or 1.0, respectively."u8, + TextureAddressMode.Border => "Texture coordinates outside the range [0.0, 1.0] are set to the border color (generally black)."u8, + _ => ""u8, + }; + + private bool DrawTextureSection(bool disabled) + { + if (Textures.Count == 0) + 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, TextureLabelWidth * UiHelpers.Scale); + foreach (var (label, textureI, samplerI, description, monoFont) in Textures) + { + using var _ = ImRaii.PushId(samplerI); + var tmp = Mtrl.Textures[textureI].Path; + var unfolded = 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) + UnfoldedTextures.Add(samplerI); + else + 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 != Mtrl.Textures[textureI].Path) + { + ret = true; + Mtrl.Textures[textureI].Path = tmp; + } + + ImGui.TableNextColumn(); + 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(disabled, textureI, samplerI); + ImGui.TableNextColumn(); + } + } + + return ret; + } + + private static bool ComboTextureAddressMode(ReadOnlySpan label, ref TextureAddressMode value) + { + using var c = ImUtf8.Combo(label, value.ToString()); + if (!c) + return false; + + var ret = false; + foreach (var mode in Enum.GetValues()) + { + if (ImGui.Selectable(mode.ToString(), mode == value)) + { + value = mode; + ret = true; + } + + ImUtf8.SelectableHelpMarker(TextureAddressModeTooltip(mode)); + } + + return ret; + } + + private bool DrawMaterialSampler(bool disabled, int textureIdx, int samplerIdx) + { + var ret = false; + ref var texture = ref Mtrl.Textures[textureIdx]; + ref var sampler = ref Mtrl.ShaderPackage.Samplers[samplerIdx]; + + var dx11 = texture.DX11; + if (ImUtf8.Checkbox("Prepend -- to the file name on DirectX 11"u8, ref dx11)) + { + texture.DX11 = dx11; + ret = true; + } + + ref var samplerFlags = ref SamplerFlags.Wrap(ref sampler.Flags); + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + var addressMode = samplerFlags.UAddressMode; + if (ComboTextureAddressMode("##UAddressMode"u8, ref addressMode)) + { + samplerFlags.UAddressMode = addressMode; + ret = true; + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + ImGui.SameLine(); + ImUtf8.LabeledHelpMarker("U Address Mode"u8, "Method to use for resolving a U texture coordinate that is outside the 0 to 1 range."); + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + addressMode = samplerFlags.VAddressMode; + if (ComboTextureAddressMode("##VAddressMode"u8, ref addressMode)) + { + samplerFlags.VAddressMode = addressMode; + ret = true; + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + ImGui.SameLine(); + ImUtf8.LabeledHelpMarker("V Address Mode"u8, "Method to use for resolving a V texture coordinate that is outside the 0 to 1 range."); + + var lodBias = samplerFlags.LodBias; + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ImUtf8.DragScalar("##LoDBias"u8, ref lodBias, -8.0f, 7.984375f, 0.1f)) + { + samplerFlags.LodBias = lodBias; + ret = true; + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + ImGui.SameLine(); + ImUtf8.LabeledHelpMarker("Level of Detail Bias"u8, + "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 = samplerFlags.MinLod; + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ImUtf8.DragScalar("##MinLoD"u8, ref minLod, 0, 15, 0.1f)) + { + samplerFlags.MinLod = minLod; + ret = true; + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + ImGui.SameLine(); + ImUtf8.LabeledHelpMarker("Minimum Level of Detail"u8, + "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 = ImUtf8.TreeNode("Advanced Settings"u8); + if (!t) + return ret; + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ImUtf8.InputScalar("Texture Flags"u8, ref texture.Flags, "%04X"u8, + flags: disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) + ret = true; + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ImUtf8.InputScalar("Sampler Flags"u8, ref sampler.Flags, "%08X"u8, + flags: ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) + { + ret = true; + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + return ret; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs new file mode 100644 index 00000000..2d4e93f1 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs @@ -0,0 +1,199 @@ +using Dalamud.Plugin.Services; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Text; +using OtterGui.Widgets; +using Penumbra.GameData; +using Penumbra.GameData.Files; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Interop; +using Penumbra.Interop.Hooks.Objects; +using Penumbra.Interop.ResourceTree; +using Penumbra.Services; +using Penumbra.String; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public sealed partial class MtrlTab : IWritable, IDisposable +{ + private const int ShpkPrefixLength = 16; + + private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true); + + private readonly IDataManager _gameData; + private readonly IFramework _framework; + private readonly ObjectManager _objects; + private readonly CharacterBaseDestructor _characterBaseDestructor; + private readonly StainService _stainService; + private readonly ResourceTreeFactory _resourceTreeFactory; + private readonly FileDialogService _fileDialog; + private readonly MaterialTemplatePickers _materialTemplatePickers; + private readonly Configuration _config; + + private readonly ModEditWindow _edit; + public readonly MtrlFile Mtrl; + public readonly string FilePath; + public readonly bool Writable; + + private bool _updateOnNextFrame; + + public unsafe MtrlTab(IDataManager gameData, IFramework framework, ObjectManager objects, CharacterBaseDestructor characterBaseDestructor, + StainService stainService, ResourceTreeFactory resourceTreeFactory, FileDialogService fileDialog, MaterialTemplatePickers materialTemplatePickers, + Configuration config, ModEditWindow edit, MtrlFile file, string filePath, bool writable) + { + _gameData = gameData; + _framework = framework; + _objects = objects; + _characterBaseDestructor = characterBaseDestructor; + _stainService = stainService; + _resourceTreeFactory = resourceTreeFactory; + _fileDialog = fileDialog; + _materialTemplatePickers = materialTemplatePickers; + _config = config; + + _edit = edit; + Mtrl = file; + FilePath = filePath; + Writable = writable; + AssociatedBaseDevkit = TryLoadShpkDevkit("_base", out LoadedBaseDevkitPathName); + Update(); + LoadShpk(FindAssociatedShpk(out _, out _)); + if (writable) + { + _characterBaseDestructor.Subscribe(UnbindFromDrawObjectMaterialInstances, CharacterBaseDestructor.Priority.MtrlTab); + BindToMaterialInstances(); + } + } + + public bool DrawVersionUpdate(bool disabled) + { + if (disabled || Mtrl.IsDawntrail) + return false; + + if (!ImUtf8.ButtonEx("Update MTRL Version to Dawntrail"u8, + "Try using this if the material can not be loaded or should use legacy shaders.\n\nThis is not revertible."u8, + new Vector2(-0.1f, 0), false, 0, Colors.PressEnterWarningBg)) + return false; + + Mtrl.MigrateToDawntrail(); + Update(); + LoadShpk(FindAssociatedShpk(out _, out _)); + return true; + } + + public bool DrawPanel(bool disabled) + { + if (_updateOnNextFrame) + { + _updateOnNextFrame = false; + Update(); + } + + DrawMaterialLivePreviewRebind(disabled); + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + var ret = DrawBackFaceAndTransparency(disabled); + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ret |= DrawShaderSection(disabled); + + ret |= DrawTextureSection(disabled); + ret |= DrawColorTableSection(disabled); + ret |= DrawConstantsSection(disabled); + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + DrawOtherMaterialDetails(disabled); + + return !disabled && ret; + } + + private bool DrawBackFaceAndTransparency(bool disabled) + { + ref var shaderFlags = ref ShaderFlags.Wrap(ref Mtrl.ShaderPackage.Flags); + + var ret = false; + + using var dis = ImRaii.Disabled(disabled); + + var tmp = shaderFlags.EnableTransparency; + if (ImGui.Checkbox("Enable Transparency", ref tmp)) + { + shaderFlags.EnableTransparency = tmp; + ret = true; + SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); + } + + ImGui.SameLine(200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X); + tmp = shaderFlags.HideBackfaces; + if (ImGui.Checkbox("Hide Backfaces", ref tmp)) + { + shaderFlags.HideBackfaces = tmp; + ret = true; + SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); + } + + if (ShpkLoading) + { + ImGui.SameLine(400 * UiHelpers.Scale + 2 * ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X); + + ImUtf8.Text("Loading shader (.shpk) file. Some functionality will only be available after this is done."u8, + ImGuiUtil.HalfBlendText(0x808000u)); // Half cyan + } + + return ret; + } + + private void DrawOtherMaterialDetails(bool _) + { + if (!ImGui.CollapsingHeader("Further Content")) + return; + + using (var sets = ImRaii.TreeNode("UV Sets", ImGuiTreeNodeFlags.DefaultOpen)) + { + if (sets) + foreach (var set in Mtrl.UvSets) + ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); + } + + using (var sets = ImRaii.TreeNode("Color Sets", ImGuiTreeNodeFlags.DefaultOpen)) + { + if (sets) + foreach (var set in Mtrl.ColorSets) + ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); + } + + if (Mtrl.AdditionalData.Length <= 0) + return; + + using var t = ImRaii.TreeNode($"Additional Data (Size: {Mtrl.AdditionalData.Length})###AdditionalData"); + if (t) + Widget.DrawHexViewer(Mtrl.AdditionalData); + } + + public void Update() + { + UpdateShaders(); + UpdateTextures(); + UpdateConstants(); + } + + public unsafe void Dispose() + { + UnbindFromMaterialInstances(); + if (Writable) + _characterBaseDestructor.Unsubscribe(UnbindFromDrawObjectMaterialInstances); + } + + public bool Valid + => ShadersKnown && Mtrl.Valid; + + public byte[] Write() + { + var output = Mtrl.Clone(); + output.GarbageCollect(AssociatedShpk, SamplerIds); + + return output.Write(); + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs new file mode 100644 index 00000000..af8b7db2 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs @@ -0,0 +1,18 @@ +using Dalamud.Plugin.Services; +using OtterGui.Services; +using Penumbra.GameData.Files; +using Penumbra.GameData.Interop; +using Penumbra.Interop.Hooks.Objects; +using Penumbra.Interop.ResourceTree; +using Penumbra.Services; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public sealed class MtrlTabFactory(IDataManager gameData, IFramework framework, ObjectManager objects, + CharacterBaseDestructor characterBaseDestructor, StainService stainService, ResourceTreeFactory resourceTreeFactory, + FileDialogService fileDialog, MaterialTemplatePickers materialTemplatePickers, Configuration config) : IUiService +{ + public MtrlTab Create(ModEditWindow edit, MtrlFile file, string filePath, bool writable) + => new(gameData, framework, objects, characterBaseDestructor, stainService, resourceTreeFactory, fileDialog, + materialTemplatePickers, config, edit, file, filePath, writable); +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs deleted file mode 100644 index 25c0e448..00000000 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs +++ /dev/null @@ -1,538 +0,0 @@ -using Dalamud.Interface; -using Dalamud.Interface.Utility; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.GameData.Files; -using Penumbra.GameData.Files.MaterialStructs; -using Penumbra.String.Functions; - -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 DrawMaterialColorTableChange(MtrlTab tab, bool disabled) - { - if (!tab.SamplerIds.Contains(ShpkFile.TableSamplerId) || !tab.Mtrl.HasTable) - return false; - - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if (!ImGui.CollapsingHeader("Color Table", ImGuiTreeNodeFlags.DefaultOpen)) - return false; - - ColorTableCopyAllClipboardButton(tab.Mtrl); - ImGui.SameLine(); - var ret = ColorTablePasteAllClipboardButton(tab, disabled); - if (!disabled) - { - ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); - ImGui.SameLine(); - ret |= ColorTableDyeableCheckbox(tab); - } - - var hasDyeTable = tab.Mtrl.HasDyeTable; - if (hasDyeTable) - { - ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); - ImGui.SameLine(); - ret |= DrawPreviewDye(tab, disabled); - } - - using var table = ImRaii.Table("##ColorTable", hasDyeTable ? 11 : 9, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV); - if (!table) - return false; - - ImGui.TableNextColumn(); - ImGui.TableHeader(string.Empty); - ImGui.TableNextColumn(); - ImGui.TableHeader("Row"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Diffuse"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Specular"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Emissive"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Gloss"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Tile"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Repeat"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Skew"); - if (hasDyeTable) - { - ImGui.TableNextColumn(); - ImGui.TableHeader("Dye"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Dye Preview"); - } - - for (var i = 0; i < ColorTable.NumRows; ++i) - { - ret |= DrawColorTableRow(tab, i, disabled); - ImGui.TableNextRow(); - } - - return ret; - } - - - private static void ColorTableCopyAllClipboardButton(MtrlFile file) - { - if (!ImGui.Button("Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2(200, 0))) - return; - - try - { - var data1 = file.Table.AsBytes(); - var data2 = file.HasDyeTable ? file.DyeTable.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); - } - catch - { - // ignored - } - } - - 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; - if (tab.Mtrl.HasDyeTable) - for (var i = 0; i < LegacyColorTable.NumUsedRows; ++i) - ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, i, dyeId, 0); - - tab.UpdateColorTablePreview(); - - return ret; - } - - ImGui.SameLine(); - var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye"; - if (_stainService.StainCombo.Draw(label, dyeColor, string.Empty, true, gloss)) - tab.UpdateColorTablePreview(); - return false; - } - - private static unsafe bool ColorTablePasteAllClipboardButton(MtrlTab tab, bool disabled) - { - if (!ImGuiUtil.DrawDisabledButton("Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2(200, 0), string.Empty, disabled) - || !tab.Mtrl.HasTable) - return false; - - try - { - var text = ImGui.GetClipboardText(); - var data = Convert.FromBase64String(text); - if (data.Length < Marshal.SizeOf()) - return false; - - ref var rows = ref tab.Mtrl.Table; - fixed (void* ptr = data, output = &rows) - { - MemoryUtility.MemCpyUnchecked(output, ptr, Marshal.SizeOf()); - if (data.Length >= Marshal.SizeOf() + Marshal.SizeOf() - && tab.Mtrl.HasDyeTable) - { - ref var dyeRows = ref tab.Mtrl.DyeTable; - fixed (void* output2 = &dyeRows) - { - MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf(), - Marshal.SizeOf()); - } - } - } - - tab.UpdateColorTablePreview(); - - return true; - } - catch - { - return false; - } - } - - [SkipLocalsInit] - private static unsafe void ColorTableCopyClipboardButton(ColorTable.Row row, ColorDyeTable.Row dye) - { - if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Export this row to your clipboard.", false, true)) - return; - - try - { - Span data = stackalloc byte[ColorTable.Row.Size + ColorDyeTable.Row.Size]; - fixed (byte* ptr = data) - { - MemoryUtility.MemCpyUnchecked(ptr, &row, ColorTable.Row.Size); - MemoryUtility.MemCpyUnchecked(ptr + ColorTable.Row.Size, &dye, ColorDyeTable.Row.Size); - } - - var text = Convert.ToBase64String(data); - ImGui.SetClipboardText(text); - } - catch - { - // ignored - } - } - - private static bool ColorTableDyeableCheckbox(MtrlTab tab) - { - var dyeable = tab.Mtrl.HasDyeTable; - var ret = ImGui.Checkbox("Dyeable", ref dyeable); - - if (ret) - { - tab.Mtrl.HasDyeTable = dyeable; - tab.UpdateColorTablePreview(); - } - - return ret; - } - - private static unsafe bool ColorTablePasteFromClipboardButton(MtrlTab tab, 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)) - return false; - - try - { - var text = ImGui.GetClipboardText(); - var data = Convert.FromBase64String(text); - if (data.Length != ColorTable.Row.Size + ColorDyeTable.Row.Size - || !tab.Mtrl.HasTable) - return false; - - fixed (byte* ptr = data) - { - tab.Mtrl.Table[rowIdx] = *(ColorTable.Row*)ptr; - if (tab.Mtrl.HasDyeTable) - tab.Mtrl.DyeTable[rowIdx] = *(ColorDyeTable.Row*)(ptr + ColorTable.Row.Size); - } - - tab.UpdateColorTableRowPreview(rowIdx); - - return true; - } - catch - { - return false; - } - } - - private static void ColorTableHighlightButton(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.ColorTablePreviewers.Count == 0, true); - - if (ImGui.IsItemHovered()) - tab.HighlightColorTableRow(rowIdx); - else if (tab.HighlightedColorTableRow == rowIdx) - tab.CancelColorTableHighlight(); - } - - private bool DrawColorTableRow(MtrlTab tab, int rowIdx, bool disabled) - { - static bool FixFloat(ref float val, float current) - { - val = (float)(Half)val; - return val != current; - } - - using var id = ImRaii.PushId(rowIdx); - ref var row = ref tab.Mtrl.Table[rowIdx]; - var hasDye = tab.Mtrl.HasDyeTable; - ref var dye = ref tab.Mtrl.DyeTable[rowIdx]; - var floatSize = 70 * UiHelpers.Scale; - var intSize = 45 * UiHelpers.Scale; - ImGui.TableNextColumn(); - ColorTableCopyClipboardButton(row, dye); - ImGui.SameLine(); - var ret = ColorTablePasteFromClipboardButton(tab, rowIdx, disabled); - ImGui.SameLine(); - ColorTableHighlightButton(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 => - { - tab.Mtrl.Table[rowIdx].Diffuse = c; - tab.UpdateColorTableRowPreview(rowIdx); - }); - if (hasDye) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse, - b => - { - tab.Mtrl.DyeTable[rowIdx].Diffuse = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - } - - ImGui.TableNextColumn(); - ret |= ColorPicker("##Specular", "Specular Color", row.Specular, c => - { - tab.Mtrl.Table[rowIdx].Specular = c; - tab.UpdateColorTableRowPreview(rowIdx); - }); - ImGui.SameLine(); - var tmpFloat = row.SpecularStrength; - ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##SpecularStrength", ref tmpFloat, 0.01f, 0f, HalfMaxValue, "%.2f") - && FixFloat(ref tmpFloat, row.SpecularStrength)) - { - row.SpecularStrength = tmpFloat; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled); - - if (hasDye) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeSpecular", "Apply Specular Color on Dye", dye.Specular, - b => - { - tab.Mtrl.DyeTable[rowIdx].Specular = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength, - b => - { - tab.Mtrl.DyeTable[rowIdx].SpecularStrength = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - } - - ImGui.TableNextColumn(); - ret |= ColorPicker("##Emissive", "Emissive Color", row.Emissive, c => - { - tab.Mtrl.Table[rowIdx].Emissive = c; - tab.UpdateColorTableRowPreview(rowIdx); - }); - if (hasDye) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive, - b => - { - tab.Mtrl.DyeTable[rowIdx].Emissive = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - } - - ImGui.TableNextColumn(); - tmpFloat = row.GlossStrength; - ImGui.SetNextItemWidth(floatSize); - var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon; - if (ImGui.DragFloat("##GlossStrength", ref tmpFloat, Math.Max(0.1f, tmpFloat * 0.025f), glossStrengthMin, HalfMaxValue, "%.1f") - && FixFloat(ref tmpFloat, row.GlossStrength)) - { - row.GlossStrength = Math.Max(tmpFloat, glossStrengthMin); - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled); - if (hasDye) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss, - b => - { - tab.Mtrl.DyeTable[rowIdx].Gloss = b; - tab.UpdateColorTableRowPreview(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) - { - row.TileSet = (ushort)Math.Clamp(tmpInt, 0, 63); - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - 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)) - { - row.MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - 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)) - { - row.MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - 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)) - { - row.MaterialSkew = row.MaterialSkew with { X = tmpFloat }; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - 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)) - { - row.MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Skew Y", ImGuiHoveredFlags.AllowWhenDisabled); - - if (hasDye) - { - ImGui.TableNextColumn(); - if (_stainService.TemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize - + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) - { - dye.Template = _stainService.TemplateCombo.CurrentSelection; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Dye Template", ImGuiHoveredFlags.AllowWhenDisabled); - - ImGui.TableNextColumn(); - ret |= DrawDyePreview(tab, rowIdx, disabled, dye, floatSize); - } - - - return ret; - } - - private bool DrawDyePreview(MtrlTab tab, int rowIdx, bool disabled, ColorDyeTable.Row dye, float floatSize) - { - var stain = _stainService.StainCombo.CurrentSelection.Key; - 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 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, rowIdx, stain, 0); - if (ret) - tab.UpdateColorTableRowPreview(rowIdx); - - ImGui.SameLine(); - ColorPicker("##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D"); - ImGui.SameLine(); - ColorPicker("##specularPreview", string.Empty, values.Specular, _ => { }, "S"); - ImGui.SameLine(); - 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.SameLine(); - 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 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)); - ret = true; - } - - if (letter.Length > 0 && ImGui.IsItemVisible()) - { - 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); - } - - ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled); - - 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); - - internal 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); -} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs deleted file mode 100644 index 1f5db38e..00000000 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs +++ /dev/null @@ -1,247 +0,0 @@ -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); - } - - 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) - { - var spacing = ImGui.GetStyle().ItemInnerSpacing.X; - var fieldWidth = (ImGui.CalcItemWidth() - (values.Length - 1) * spacing) / values.Length; - - var ret = false; - - // Not using DragScalarN because of _relativeSpeed and other points of lost flexibility. - for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) - { - if (valueIdx > 0) - ImGui.SameLine(0.0f, spacing); - - 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) - { - var spacing = ImGui.GetStyle().ItemInnerSpacing.X; - var fieldWidth = (ImGui.CalcItemWidth() - (values.Length - 1) * spacing) / values.Length; - - var ret = false; - - // Not using DragScalarN because of _relativeSpeed and other points of lost flexibility. - for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) - { - if (valueIdx > 0) - ImGui.SameLine(0.0f, spacing); - - 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) - { - switch (values.Length) - { - case 3: - { - 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) - value = Vector3.Clamp(value, Vector3.Zero, Vector3.One); - value.CopyTo(values); - return true; - } - case 4: - { - 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) - value = Vector4.Clamp(value, Vector4.Zero, Vector4.One); - value.CopyTo(values); - return true; - } - default: return FloatConstantEditor.Default.Draw(values, disabled); - } - } - } - - 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) - { - var spacing = ImGui.GetStyle().ItemInnerSpacing.X; - var fieldWidth = (ImGui.CalcItemWidth() - (values.Length - 1) * spacing) / values.Length; - - var ret = false; - - for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) - { - using var id = ImRaii.PushId(valueIdx); - if (valueIdx > 0) - ImGui.SameLine(0.0f, spacing); - - ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx)); - - var currentValue = values[valueIdx]; - 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)) - { - 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 deleted file mode 100644 index 29fd7531..00000000 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ /dev/null @@ -1,783 +0,0 @@ -using Dalamud.Interface; -using Dalamud.Interface.ImGuiNotification; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using ImGuiNET; -using Newtonsoft.Json.Linq; -using OtterGui; -using OtterGui.Classes; -using OtterGui.Raii; -using Penumbra.GameData.Data; -using Penumbra.GameData.Files; -using Penumbra.GameData.Files.MaterialStructs; -using Penumbra.GameData.Structs; -using Penumbra.Interop.Hooks.Objects; -using Penumbra.Interop.MaterialPreview; -using Penumbra.String; -using Penumbra.String.Classes; -using Penumbra.UI.Classes; -using static Penumbra.GameData.Files.ShpkFile; - -namespace Penumbra.UI.AdvancedWindow; - -public partial class ModEditWindow -{ - private sealed class MtrlTab : IWritable, IDisposable - { - private const int ShpkPrefixLength = 16; - - private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true); - - private readonly ModEditWindow _edit; - public readonly MtrlFile Mtrl; - public readonly string FilePath; - public readonly bool Writable; - - 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 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 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 HashSet UnfoldedTextures = new(4); - public readonly HashSet SamplerIds = new(16); - public float TextureLabelWidth; - - // Material Constants - 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); - public readonly List ColorTablePreviewers = new(4); - public int HighlightedColorTableRow = -1; - public readonly Stopwatch HighlightTime = new(); - - public FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath) - { - defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name); - if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath)) - return FullPath.Empty; - - 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; - var data = LoadedShpkPath.IsRooted - ? File.ReadAllBytes(LoadedShpkPath.FullName) - : _edit._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) - { - LoadedShpkPath = FullPath.Empty; - LoadedShpkPathName = string.Empty; - AssociatedShpk = null; - Penumbra.Messager.NotificationMessage(e, $"Could not load {LoadedShpkPath.ToPath()}.", NotificationType.Error, false); - } - - if (LoadedShpkPath.InternalName.IsEmpty) - { - AssociatedShpkDevkit = null; - LoadedShpkDevkitPathName = string.Empty; - } - else - { - AssociatedShpkDevkit = - TryLoadShpkDevkit(Path.GetFileNameWithoutExtension(Mtrl.ShaderPackage.Name), out LoadedShpkDevkitPathName); - } - - UpdateShaderKeys(); - Update(); - } - - private JObject? TryLoadShpkDevkit(string shpkBaseName, out string devkitPathName) - { - try - { - 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; - } - } - - private T? TryGetShpkDevkitData(string category, uint? id, bool mayVary) where T : class - => 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 - { - if (devkit == null) - return null; - - try - { - 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]; - } - - return data?.ToObject(typeof(T)) as T; - } - catch (Exception e) - { - // 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; - } - } - - private void UpdateShaderKeys() - { - ShaderKeys.Clear(); - if (AssociatedShpk != null) - foreach (var key in AssociatedShpk.MaterialKeys) - { - 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); - - 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 string.Compare(x.Label, y.Label, StringComparison.Ordinal); - }); - 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)>())); - } - - 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)); - foreach (var systemKeySelector in systemKeySelectors) - { - foreach (var sceneKeySelector in sceneKeySelectors) - { - foreach (var subViewKeySelector in subViewKeySelectors) - { - 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; - } - } - } - } - - 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; - } - - private void UpdateTextures() - { - Textures.Clear(); - SamplerIds.Clear(); - if (AssociatedShpk == null) - { - SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); - if (Mtrl.HasTable) - SamplerIds.Add(TableSamplerId); - - foreach (var (sampler, index) in Mtrl.ShaderPackage.Samplers.WithIndex()) - Textures.Add(($"0x{sampler.SamplerId:X8}", sampler.TextureIndex, index, string.Empty, true)); - } - else - { - 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) - { - SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); - if (Mtrl.HasTable) - SamplerIds.Add(TableSamplerId); - } - - foreach (var samplerId in SamplerIds) - { - var shpkSampler = AssociatedShpk.GetSamplerById(samplerId); - if (shpkSampler is not { Slot: 2 }) - continue; - - 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)); - } - - if (SamplerIds.Contains(TableSamplerId)) - Mtrl.HasTable = true; - } - - 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 = TextureLabelWidth / UiHelpers.Scale + 4; - } - - 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)); - return newGroup; - } - - Constants.Clear(); - if (AssociatedShpk == null) - { - var fcGroup = FindOrAddGroup(Constants, "Further Constants"); - foreach (var (constant, index) in Mtrl.ShaderPackage.Constants.WithIndex()) - { - 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) - 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); - } - - var fcGroup = FindOrAddGroup(Constants, "Further Constants"); - foreach (var (start, end) in handledElements.Ranges(complement: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() - { - UnbindFromMaterialInstances(); - - var instances = MaterialInfo.FindMaterials(_edit._resourceTreeFactory.GetLocalPlayerRelatedCharacters().Select(ch => ch.Address), - FilePath); - - var foundMaterials = new HashSet(); - foreach (var materialInfo in instances) - { - var material = materialInfo.GetDrawObjectMaterial(_edit._objects); - if (foundMaterials.Contains((nint)material)) - continue; - - try - { - MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._objects, materialInfo)); - foundMaterials.Add((nint)material); - } - catch (InvalidOperationException) - { - // Carry on without that previewer. - } - } - - UpdateMaterialPreview(); - - if (!Mtrl.HasTable) - return; - - foreach (var materialInfo in instances) - { - try - { - ColorTablePreviewers.Add(new LiveColorTablePreviewer(_edit._objects, _edit._framework, materialInfo)); - } - catch (InvalidOperationException) - { - // Carry on without that previewer. - } - } - - UpdateColorTablePreview(); - } - - private void UnbindFromMaterialInstances() - { - foreach (var previewer in MaterialPreviewers) - previewer.Dispose(); - MaterialPreviewers.Clear(); - - foreach (var previewer in ColorTablePreviewers) - previewer.Dispose(); - ColorTablePreviewers.Clear(); - } - - private unsafe void UnbindFromDrawObjectMaterialInstances(CharacterBase* characterBase) - { - for (var i = MaterialPreviewers.Count; i-- > 0;) - { - var previewer = MaterialPreviewers[i]; - if (previewer.DrawObject != characterBase) - continue; - - previewer.Dispose(); - MaterialPreviewers.RemoveAt(i); - } - - for (var i = ColorTablePreviewers.Count; i-- > 0;) - { - var previewer = ColorTablePreviewers[i]; - if (previewer.DrawObject != characterBase) - continue; - - previewer.Dispose(); - ColorTablePreviewers.RemoveAt(i); - } - } - - 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); - } - - private 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 HighlightColorTableRow(int rowIdx) - { - var oldRowIdx = HighlightedColorTableRow; - - if (HighlightedColorTableRow != rowIdx) - { - HighlightedColorTableRow = rowIdx; - HighlightTime.Restart(); - } - - if (oldRowIdx >= 0) - UpdateColorTableRowPreview(oldRowIdx); - if (rowIdx >= 0) - UpdateColorTableRowPreview(rowIdx); - } - - public void CancelColorTableHighlight() - { - var rowIdx = HighlightedColorTableRow; - - HighlightedColorTableRow = -1; - HighlightTime.Reset(); - - if (rowIdx >= 0) - UpdateColorTableRowPreview(rowIdx); - } - - public void UpdateColorTableRowPreview(int rowIdx) - { - if (ColorTablePreviewers.Count == 0) - return; - - if (!Mtrl.HasTable) - return; - - var row = new LegacyColorTable.Row(Mtrl.Table[rowIdx]); - if (Mtrl.HasDyeTable) - { - var stm = _edit._stainService.StmFile; - var dye = new LegacyColorDyeTable.Row(Mtrl.DyeTable[rowIdx]); - if (stm.TryGetValue(dye.Template, _edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) - row.ApplyDyeTemplate(dye, dyes); - } - - if (HighlightedColorTableRow == rowIdx) - ApplyHighlight(ref row, (float)HighlightTime.Elapsed.TotalSeconds); - - foreach (var previewer in ColorTablePreviewers) - { - row.AsHalves().CopyTo(previewer.ColorTable.AsSpan() - .Slice(LiveColorTablePreviewer.TextureWidth * 4 * rowIdx, LiveColorTablePreviewer.TextureWidth * 4)); - previewer.ScheduleUpdate(); - } - } - - public void UpdateColorTablePreview() - { - if (ColorTablePreviewers.Count == 0) - return; - - if (!Mtrl.HasTable) - return; - - var rows = new LegacyColorTable(Mtrl.Table); - var dyeRows = new LegacyColorDyeTable(Mtrl.DyeTable); - if (Mtrl.HasDyeTable) - { - var stm = _edit._stainService.StmFile; - var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; - for (var i = 0; i < LegacyColorTable.NumUsedRows; ++i) - { - ref var row = ref rows[i]; - var dye = dyeRows[i]; - if (stm.TryGetValue(dye.Template, stainId, out var dyes)) - row.ApplyDyeTemplate(dye, dyes); - } - } - - if (HighlightedColorTableRow >= 0) - ApplyHighlight(ref rows[HighlightedColorTableRow], (float)HighlightTime.Elapsed.TotalSeconds); - - foreach (var previewer in ColorTablePreviewers) - { - // TODO: Dawntrail - rows.AsHalves().CopyTo(previewer.ColorTable); - previewer.ScheduleUpdate(); - } - } - - private static void ApplyHighlight(ref LegacyColorTable.Row row, float time) - { - var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; - var baseColor = ColorId.InGameHighlight.Value(); - var color = level * new Vector3(baseColor & 0xFF, (baseColor >> 8) & 0xFF, (baseColor >> 16) & 0xFF); - - row.Diffuse = Vector3.Zero; - row.Specular = Vector3.Zero; - row.Emissive = color * color; - } - - public void Update() - { - UpdateShaders(); - UpdateTextures(); - UpdateConstants(); - } - - public unsafe MtrlTab(ModEditWindow edit, MtrlFile file, string filePath, bool writable) - { - _edit = edit; - Mtrl = file; - FilePath = filePath; - Writable = writable; - AssociatedBaseDevkit = TryLoadShpkDevkit("_base", out LoadedBaseDevkitPathName); - LoadShpk(FindAssociatedShpk(out _, out _)); - if (writable) - { - _edit._characterBaseDestructor.Subscribe(UnbindFromDrawObjectMaterialInstances, CharacterBaseDestructor.Priority.MtrlTab); - BindToMaterialInstances(); - } - } - - public unsafe void Dispose() - { - UnbindFromMaterialInstances(); - if (Writable) - _edit._characterBaseDestructor.Unsubscribe(UnbindFromDrawObjectMaterialInstances); - } - - // TODO Readd ShadersKnown - public bool Valid - => (true || ShadersKnown) && Mtrl.Valid; - - public byte[] Write() - { - var output = Mtrl.Clone(); - output.GarbageCollect(AssociatedShpk, SamplerIds); - - 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; - } - - 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() - => Type switch - { - 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 deleted file mode 100644 index b9525b29..00000000 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs +++ /dev/null @@ -1,481 +0,0 @@ -using Dalamud.Interface; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.GameData; -using Penumbra.String.Classes; - -namespace Penumbra.UI.AdvancedWindow; - -public partial class ModEditWindow -{ - private readonly FileDialogService _fileDialog; - - // 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[] - { - "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", - "charactershadowoffset.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[] - { - "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) - { - ImGui.TextUnformatted("Shader Package: " + tab.Mtrl.ShaderPackage.Name); - return false; - } - - 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; - } - - private static bool DrawShaderFlagsInput(MtrlTab tab, bool disabled) - { - var shpkFlags = (int)tab.Mtrl.ShaderPackage.Flags; - ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); - if (!ImGui.InputInt("Shader Flags", ref shpkFlags, 0, 0, - ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) - return false; - - tab.Mtrl.ShaderPackage.Flags = (uint)shpkFlags; - tab.SetShaderPackageFlags((uint)shpkFlags); - return true; - } - - /// - /// Show the currently associated shpk file, if any, and the buttons to associate - /// a specific shpk from your drive, the modded shpk by path or the default shpk. - /// - 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}"; - 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)); - - 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) => - { - if (success) - tab.LoadShpk(new FullPath(name[0])); - }, 1, Mod!.ModPath.FullName, false); - - var moddedPath = tab.FindAssociatedShpk(out var defaultPath, out var gamePath); - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Associate Default .shpk File", Vector2.Zero, moddedPath.ToPath(), - moddedPath.Equals(tab.LoadedShpkPath))) - tab.LoadShpk(moddedPath); - - if (!gamePath.Path.Equals(moddedPath.InternalName)) - { - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Associate Unmodded .shpk File", Vector2.Zero, defaultPath, - gamePath.Path.Equals(tab.LoadedShpkPath.InternalName))) - tab.LoadShpk(new FullPath(gamePath)); - } - - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - } - - private static bool DrawMaterialShaderKeys(MtrlTab tab, bool disabled) - { - if (tab.ShaderKeys.Count == 0) - return false; - - 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); - if (!disabled && shpkKey.HasValue) - { - ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); - using (var c = ImRaii.Combo($"##{key.Category:X8}", currentLabel)) - { - 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); - } - } - - 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 void DrawMaterialShaders(MtrlTab tab) - { - if (tab.AssociatedShpk == null) - return; - - ImRaii.TreeNode(tab.VertexShadersString, ImGuiTreeNodeFlags.Leaf).Dispose(); - ImRaii.TreeNode(tab.PixelShadersString, ImGuiTreeNodeFlags.Leaf).Dispose(); - - if (tab.ShaderComment.Length > 0) - { - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - ImGui.TextUnformatted(tab.ShaderComment); - } - } - - private static bool DrawMaterialConstants(MtrlTab tab, bool disabled) - { - if (tab.Constants.Count == 0) - return false; - - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if (!ImGui.CollapsingHeader("Material Constants")) - return false; - - using var _ = ImRaii.PushId("MaterialConstants"); - - var ret = false; - foreach (var (header, group) in tab.Constants) - { - using var t = ImRaii.TreeNode(header, ImGuiTreeNodeFlags.DefaultOpen); - if (!t) - continue; - - 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}"); - ImGui.SetNextItemWidth(250.0f); - if (editor.Draw(buffer[slice], 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) - ImGuiUtil.LabeledHelpMarker(label, description); - else - ImGui.TextUnformatted(label); - } - } - } - - return ret; - } - - private static bool DrawMaterialSampler(MtrlTab tab, bool disabled, int textureIdx, int samplerIdx) - { - var ret = false; - ref var texture = ref tab.Mtrl.Textures[textureIdx]; - ref var sampler = ref tab.Mtrl.ShaderPackage.Samplers[samplerIdx]; - - // 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, nint.Zero, nint.Zero, "%04X", flags); - } - } - - static bool ComboTextureAddressMode(string label, ref uint samplerFlags, int bitOffset) - { - var current = (TextureAddressMode)((samplerFlags >> bitOffset) & 0x3u); - using var c = ImRaii.Combo(label, current.ToString()); - if (!c) - return false; - - var ret = false; - foreach (var value in Enum.GetValues()) - { - if (ImGui.Selectable(value.ToString(), value == current)) - { - samplerFlags = (samplerFlags & ~(0x3u << bitOffset)) | ((uint)value << bitOffset); - ret = true; - } - - ImGuiUtil.SelectableHelpMarker(TextureAddressModeTooltips[(int)value]); - } - - return ret; - } - - var dx11 = texture.DX11; - if (ImGui.Checkbox("Prepend -- to the file name on DirectX 11", ref dx11)) - { - texture.DX11 = dx11; - ret = true; - } - - ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); - if (ComboTextureAddressMode("##UAddressMode", ref sampler.Flags, 2)) - { - 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."); - - ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); - if (ComboTextureAddressMode("##VAddressMode", ref sampler.Flags, 0)) - { - 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."); - - 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)) - { - 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."); - - var minLod = (int)((sampler.Flags >> 20) & 0xF); - ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); - if (ImGui.DragInt("##MinLoD", ref minLod, 0.1f, 0, 15)) - { - 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."); - - using var t = ImRaii.TreeNode("Advanced Settings"); - if (!t) - return ret; - - 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 * 100.0f); - if (ImGui.InputInt("Sampler Flags", ref samplerFlags, 0, 0, - ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) - { - sampler.Flags = (uint)samplerFlags; - ret = true; - tab.SetSamplerFlags(sampler.SamplerId, (uint)samplerFlags); - } - - return ret; - } - - private bool DrawMaterialShader(MtrlTab tab, bool disabled) - { - var ret = false; - if (ImGui.CollapsingHeader(tab.ShaderHeader)) - { - ret |= DrawPackageNameInput(tab, disabled); - ret |= DrawShaderFlagsInput(tab, disabled); - DrawCustomAssociations(tab); - ret |= DrawMaterialShaderKeys(tab, disabled); - DrawMaterialShaders(tab); - } - - if (tab.AssociatedShpkDevkit == null) - { - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - 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 - - using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning); - - 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 static string? MaterialParamName(bool componentOnly, int offset) - { - if (offset < 0) - return null; - - return (componentOnly, offset & 0x3) switch - { - (true, 0) => "x", - (true, 1) => "y", - (true, 2) => "z", - (true, 3) => "w", - (false, 0) => $"[{offset >> 2:D2}].x", - (false, 1) => $"[{offset >> 2:D2}].y", - (false, 2) => $"[{offset >> 2:D2}].z", - (false, 3) => $"[{offset >> 2:D2}].w", - _ => 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) - { - if (valueLength == 0 || valueOffset < 0) - return (null, false); - - var firstVector = valueOffset >> 2; - var lastVector = (valueOffset + valueLength - 1) >> 2; - var firstComponent = valueOffset & 0x3; - var lastComponent = (valueOffset + valueLength - 1) & 0x3; - if (firstVector == lastVector) - return ($"{prefix}[{firstVector}]{VectorSwizzle(firstComponent, lastComponent)}", true); - - var sb = new StringBuilder(128); - sb.Append($"{prefix}[{firstVector}]{VectorSwizzle(firstComponent, 3).TrimEnd()}"); - for (var i = firstVector + 1; i < lastVector; ++i) - sb.Append($", [{i}]"); - - sb.Append($", [{lastVector}]{VectorSwizzle(0, lastComponent)}"); - return (sb.ToString(), false); - } -} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index 5a8fb13a..ee883daf 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -3,11 +3,7 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using OtterGui.Text; -using OtterGui.Widgets; -using Penumbra.GameData.Files; -using Penumbra.String.Classes; -using Penumbra.UI.Classes; +using Penumbra.UI.AdvancedWindow.Materials; namespace Penumbra.UI.AdvancedWindow; @@ -17,177 +13,10 @@ public partial class ModEditWindow private bool DrawMaterialPanel(MtrlTab tab, bool disabled) { - DrawVersionUpdate(tab, disabled); - DrawMaterialLivePreviewRebind(tab, disabled); + if (tab.DrawVersionUpdate(disabled)) + _materialTab.SaveFile(); - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - var ret = DrawBackFaceAndTransparency(tab, disabled); - - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - ret |= DrawMaterialShader(tab, disabled); - - ret |= DrawMaterialTextureChange(tab, disabled); - ret |= DrawMaterialColorTableChange(tab, disabled); - ret |= DrawMaterialConstants(tab, disabled); - - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - DrawOtherMaterialDetails(tab.Mtrl, disabled); - - return !disabled && ret; - } - - private void DrawVersionUpdate(MtrlTab tab, bool disabled) - { - if (disabled || tab.Mtrl.IsDawnTrail) - return; - - if (!ImUtf8.ButtonEx("Update MTRL Version to Dawntrail"u8, - "Try using this if the material can not be loaded or should use legacy shaders.\n\nThis is not revertible."u8, - new Vector2(-0.1f, 0), false, 0, Colors.PressEnterWarningBg)) - return; - - tab.Mtrl.MigrateToDawntrail(); - _materialTab.SaveFile(); - } - - private static void DrawMaterialLivePreviewRebind(MtrlTab tab, bool disabled) - { - if (disabled) - return; - - if (ImGui.Button("Reload live preview")) - tab.BindToMaterialInstances(); - - if (tab.MaterialPreviewers.Count != 0 || tab.ColorTablePreviewers.Count != 0) - return; - - 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) - { - if (tab.Textures.Count == 0) - 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); - foreach (var (label, textureI, samplerI, description, monoFont) in tab.Textures) - { - 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[textureI].Path) - { - ret = true; - tab.Mtrl.Textures[textureI].Path = tmp; - } - - ImGui.TableNextColumn(); - using (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; - } - - 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); - - 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.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)) - { - 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; - } - - private static void DrawOtherMaterialDetails(MtrlFile file, bool _) - { - if (!ImGui.CollapsingHeader("Further Content")) - return; - - 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(); - } - - using (var sets = ImRaii.TreeNode("Color Sets", ImGuiTreeNodeFlags.DefaultOpen)) - { - if (sets) - foreach (var set in file.ColorSets) - ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); - } - - if (file.AdditionalData.Length <= 0) - return; - - using var t = ImRaii.TreeNode($"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData"); - if (t) - Widget.DrawHexViewer(file.AdditionalData); + return tab.DrawPanel(disabled); } private void DrawMaterialReassignmentTab() diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 55b7e748..6fb223df 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -15,6 +15,7 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { + private readonly FileDialogService _fileDialog; private readonly ResourceTreeFactory _resourceTreeFactory; private readonly ResourceTreeViewer _quickImportViewer; private readonly Dictionary _quickImportWritables = new(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 0d3dce8c..f28cb632 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -13,10 +13,8 @@ using Penumbra.Collections.Manager; using Penumbra.Communication; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; -using Penumbra.GameData.Interop; using Penumbra.Import.Models; using Penumbra.Import.Textures; -using Penumbra.Interop.Hooks.Objects; using Penumbra.Interop.ResourceTree; using Penumbra.Meta; using Penumbra.Mods; @@ -26,6 +24,7 @@ using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; +using Penumbra.UI.AdvancedWindow.Materials; using Penumbra.UI.AdvancedWindow.Meta; using Penumbra.UI.Classes; using Penumbra.Util; @@ -39,20 +38,17 @@ public partial class ModEditWindow : Window, IDisposable, IUiService public readonly MigrationManager MigrationManager; - private readonly PerformanceTracker _performance; - private readonly ModEditor _editor; - private readonly Configuration _config; - private readonly ItemSwapTab _itemSwapTab; - private readonly MetaFileManager _metaFileManager; - private readonly ActiveCollections _activeCollections; - private readonly StainService _stainService; - private readonly ModMergeTab _modMergeTab; - private readonly CommunicatorService _communicator; - private readonly IDragDropManager _dragDropManager; - private readonly IDataManager _gameData; - private readonly IFramework _framework; - private readonly ObjectManager _objects; - private readonly CharacterBaseDestructor _characterBaseDestructor; + private readonly PerformanceTracker _performance; + private readonly ModEditor _editor; + private readonly Configuration _config; + private readonly ItemSwapTab _itemSwapTab; + private readonly MetaFileManager _metaFileManager; + private readonly ActiveCollections _activeCollections; + private readonly ModMergeTab _modMergeTab; + private readonly CommunicatorService _communicator; + private readonly IDragDropManager _dragDropManager; + private readonly IDataManager _gameData; + private readonly IFramework _framework; private Vector2 _iconSize = Vector2.Zero; private bool _allowReduplicate; @@ -541,7 +537,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService /// If none exists, goes through all options in the currently selected mod (if any) in order of priority and resolves in them. /// If no redirection is found in either of those options, returns the original path. /// - private FullPath FindBestMatch(Utf8GamePath path) + internal FullPath FindBestMatch(Utf8GamePath path) { var currentFile = _activeCollections.Current.ResolvePath(path); if (currentFile != null) @@ -562,7 +558,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService return new FullPath(path); } - private HashSet FindPathsStartingWith(CiByteString prefix) + internal HashSet FindPathsStartingWith(CiByteString prefix) { var ret = new HashSet(); @@ -587,34 +583,32 @@ public partial class ModEditWindow : Window, IDisposable, IUiService public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, - StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab, + ActiveCollections activeCollections, ModMergeTab modMergeTab, CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager, - ResourceTreeViewerFactory resourceTreeViewerFactory, ObjectManager objects, IFramework framework, - CharacterBaseDestructor characterBaseDestructor, MetaDrawers metaDrawers, MigrationManager migrationManager) + ResourceTreeViewerFactory resourceTreeViewerFactory, IFramework framework, + MetaDrawers metaDrawers, MigrationManager migrationManager, + MtrlTabFactory mtrlTabFactory) : base(WindowBaseLabel) { - _performance = performance; - _itemSwapTab = itemSwapTab; - _gameData = gameData; - _config = config; - _editor = editor; - _metaFileManager = metaFileManager; - _stainService = stainService; - _activeCollections = activeCollections; - _modMergeTab = modMergeTab; - _communicator = communicator; - _dragDropManager = dragDropManager; - _textures = textures; - _models = models; - _fileDialog = fileDialog; - _objects = objects; - _framework = framework; - _characterBaseDestructor = characterBaseDestructor; - MigrationManager = migrationManager; - _metaDrawers = metaDrawers; + _performance = performance; + _itemSwapTab = itemSwapTab; + _gameData = gameData; + _config = config; + _editor = editor; + _metaFileManager = metaFileManager; + _activeCollections = activeCollections; + _modMergeTab = modMergeTab; + _communicator = communicator; + _dragDropManager = dragDropManager; + _textures = textures; + _models = models; + _fileDialog = fileDialog; + _framework = framework; + MigrationManager = migrationManager; + _metaDrawers = metaDrawers; _materialTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty, - (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); + (bytes, path, writable) => mtrlTabFactory.Create(this, new MtrlFile(bytes), path, writable)); _modelTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl", () => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => Mod?.ModPath.FullName ?? string.Empty, (bytes, path, _) => new MdlTab(this, bytes, path)); diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 4d0c62af..d135e10c 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -24,6 +24,7 @@ public enum ColorId NoAssignment, SelectorPriority, InGameHighlight, + InGameHighlight2, ResTreeLocalPlayer, ResTreePlayer, ResTreeNetworked, @@ -70,7 +71,8 @@ public static class Colors ColorId.NoModsAssignment => ( 0x50000080, "'Use No Mods' Collection Assignment", "A collection assignment set to not use any mods at all."), ColorId.NoAssignment => ( 0x00000000, "Unassigned Collection Assignment", "A collection assignment that is not configured to any collection and thus just has no specific treatment."), ColorId.SelectorPriority => ( 0xFF808080, "Mod Selector Priority", "The priority displayed for non-zero priority mods in the mod selector."), - ColorId.InGameHighlight => ( 0xFFEBCF89, "In-Game Highlight", "An in-game element that has been highlighted for ease of editing."), + ColorId.InGameHighlight => ( 0xFFEBCF89, "In-Game Highlight (Primary)", "An in-game element that has been highlighted for ease of editing."), + ColorId.InGameHighlight2 => ( 0xFF446CC0, "In-Game Highlight (Secondary)", "Another in-game element that has been highlighted for ease of editing."), ColorId.ResTreeLocalPlayer => ( 0xFFFFE0A0, "On-Screen: You", "You and what you own (mount, minion, accessory, pets and so on), in the On-Screen tab." ), ColorId.ResTreePlayer => ( 0xFFC0FFC0, "On-Screen: Other Players", "Other players and what they own, in the On-Screen tab." ), ColorId.ResTreeNetworked => ( 0xFFFFFFFF, "On-Screen: Non-Players (Networked)", "Non-player entities handled by the game server, in the On-Screen tab." ), diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index ab47ce7c..41ca8d6e 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -327,6 +327,9 @@ public class SettingsTab : ITab, IUiService UiHelpers.DefaultLineSpace(); DrawModHandlingSettings(); + UiHelpers.DefaultLineSpace(); + + DrawModEditorSettings(); ImGui.NewLine(); } @@ -723,6 +726,15 @@ public class SettingsTab : ITab, IUiService "Set the default Penumbra mod folder to place newly imported mods into.\nLeave blank to import into Root."); } + + /// Draw all settings pertaining to advanced editing of mods. + private void DrawModEditorSettings() + { + Checkbox("Advanced Editing: Edit Raw Tile UV Transforms", + "Edit the raw matrix components of tile UV transforms, instead of having them decomposed into scale, rotation and shear.", + _config.EditRawTileTransforms, v => _config.EditRawTileTransforms = v); + } + #endregion /// Draw the entire Color subsection. From f4fe3605f003456a2ba7d1310c907b2d54d9547b Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:42:32 +0200 Subject: [PATCH 07/12] DT material editor, new color tables --- .../Materials/MtrlTab.ColorTable.cs | 562 ++++++++++++++++++ .../Materials/MtrlTab.CommonColorTable.cs | 1 + 2 files changed, 563 insertions(+) create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs new file mode 100644 index 00000000..dc87ec41 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs @@ -0,0 +1,562 @@ +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Files.StainMapStructs; +using Penumbra.Services; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public partial class MtrlTab +{ + private const float ColorTableScalarSize = 65.0f; + + private int _colorTableSelectedPair = 0; + + private bool DrawColorTable(ColorTable table, ColorDyeTable? dyeTable, bool disabled) + { + DrawColorTablePairSelector(table, disabled); + return DrawColorTablePairEditor(table, dyeTable, disabled); + } + + private void DrawColorTablePairSelector(ColorTable table, bool disabled) + { + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var alignment = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); + var style = ImGui.GetStyle(); + var itemSpacing = style.ItemSpacing.X; + var itemInnerSpacing = style.ItemInnerSpacing.X; + var framePadding = style.FramePadding; + var buttonWidth = (ImGui.GetContentRegionAvail().X - itemSpacing * 7.0f) * 0.125f; + var frameHeight = ImGui.GetFrameHeight(); + var highlighterSize = ImUtf8.CalcIconSize(FontAwesomeIcon.Crosshairs) + framePadding * 2.0f; + var spaceWidth = ImUtf8.CalcTextSize(" "u8).X; + var spacePadding = (int)MathF.Ceiling((highlighterSize.X + framePadding.X + itemInnerSpacing) / spaceWidth); + for (var i = 0; i < ColorTable.NumRows >> 1; i += 8) + { + for (var j = 0; j < 8; ++j) + { + var pairIndex = i + j; + using (var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.ButtonActive), pairIndex == _colorTableSelectedPair)) + { + if (ImUtf8.Button($"#{pairIndex + 1}".PadLeft(3 + spacePadding), new Vector2(buttonWidth, ImGui.GetFrameHeightWithSpacing() + frameHeight))) + _colorTableSelectedPair = pairIndex; + } + + var rcMin = ImGui.GetItemRectMin() + framePadding; + var rcMax = ImGui.GetItemRectMax() - framePadding; + CtBlendRect( + rcMin with { X = rcMax.X - frameHeight * 3 - itemInnerSpacing * 2 }, + rcMax with { X = rcMax.X - (frameHeight + itemInnerSpacing) * 2 }, + ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[pairIndex << 1].DiffuseColor)), + ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].DiffuseColor)) + ); + CtBlendRect( + rcMin with { X = rcMax.X - frameHeight * 2 - itemInnerSpacing }, + rcMax with { X = rcMax.X - frameHeight - itemInnerSpacing }, + ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[pairIndex << 1].SpecularColor)), + ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].SpecularColor)) + ); + CtBlendRect( + rcMin with { X = rcMax.X - frameHeight }, rcMax, + ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[pairIndex << 1].EmissiveColor)), + ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].EmissiveColor)) + ); + if (j < 7) + ImGui.SameLine(); + + var cursor = ImGui.GetCursorScreenPos(); + ImGui.SetCursorScreenPos(rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 0.5f) - highlighterSize.Y * 0.5f }); + font.Pop(); + ColorTableHighlightButton(pairIndex, disabled); + font.Push(UiBuilder.MonoFont); + ImGui.SetCursorScreenPos(cursor); + } + } + } + + private bool DrawColorTablePairEditor(ColorTable table, ColorDyeTable? dyeTable, bool disabled) + { + var retA = false; + var retB = false; + ref var rowA = ref table[_colorTableSelectedPair << 1]; + ref var rowB = ref table[(_colorTableSelectedPair << 1) | 1]; + var dyeA = dyeTable != null ? dyeTable[_colorTableSelectedPair << 1] : default; + var dyeB = dyeTable != null ? dyeTable[(_colorTableSelectedPair << 1) | 1] : default; + var previewDyeA = _stainService.GetStainCombo(dyeA.Channel).CurrentSelection.Key; + var previewDyeB = _stainService.GetStainCombo(dyeB.Channel).CurrentSelection.Key; + var dyePackA = _stainService.GudStmFile.GetValueOrNull(dyeA.Template, previewDyeA); + var dyePackB = _stainService.GudStmFile.GetValueOrNull(dyeB.Template, previewDyeB); + using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + { + ColorTableCopyClipboardButton(_colorTableSelectedPair << 1); + ImUtf8.SameLineInner(); + retA |= ColorTablePasteFromClipboardButton(_colorTableSelectedPair << 1, disabled); + ImGui.SameLine(); + CenteredTextInRest($"Row {_colorTableSelectedPair + 1}A"); + columns.Next(); + ColorTableCopyClipboardButton((_colorTableSelectedPair << 1) | 1); + ImUtf8.SameLineInner(); + retB |= ColorTablePasteFromClipboardButton((_colorTableSelectedPair << 1) | 1, disabled); + ImGui.SameLine(); + CenteredTextInRest($"Row {_colorTableSelectedPair + 1}B"); + } + + DrawHeader(" Colors"u8); + using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + { + using var dis = ImRaii.Disabled(disabled); + using (var id = ImUtf8.PushId("ColorsA"u8)) + retA |= DrawColors(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + columns.Next(); + using (var id = ImUtf8.PushId("ColorsB"u8)) + retB |= DrawColors(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } + + DrawHeader(" Physical Parameters"u8); + using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + { + using var dis = ImRaii.Disabled(disabled); + using (var id = ImUtf8.PushId("PbrA"u8)) + retA |= DrawPbr(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + columns.Next(); + using (var id = ImUtf8.PushId("PbrB"u8)) + retB |= DrawPbr(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } + + DrawHeader(" Sheen Layer Parameters"u8); + using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + { + using var dis = ImRaii.Disabled(disabled); + using (var id = ImUtf8.PushId("SheenA"u8)) + retA |= DrawSheen(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + columns.Next(); + using (var id = ImUtf8.PushId("SheenB"u8)) + retB |= DrawSheen(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } + + DrawHeader(" Pair Blending"u8); + using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + { + using var dis = ImRaii.Disabled(disabled); + using (var id = ImUtf8.PushId("BlendingA"u8)) + retA |= DrawBlending(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + columns.Next(); + using (var id = ImUtf8.PushId("BlendingB"u8)) + retB |= DrawBlending(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } + + DrawHeader(" Material Template"u8); + using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + { + using var dis = ImRaii.Disabled(disabled); + using (var id = ImUtf8.PushId("TemplateA"u8)) + retA |= DrawTemplate(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + columns.Next(); + using (var id = ImUtf8.PushId("TemplateB"u8)) + retB |= DrawTemplate(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } + + if (dyeTable != null) + { + DrawHeader(" Dye Properties"u8); + using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + { + using var dis = ImRaii.Disabled(disabled); + using (var id = ImUtf8.PushId("DyeA"u8)) + retA |= DrawDye(dyeTable, dyePackA, _colorTableSelectedPair << 1); + columns.Next(); + using (var id = ImUtf8.PushId("DyeB"u8)) + retB |= DrawDye(dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } + } + + DrawHeader(" Further Content"u8); + using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + { + using var dis = ImRaii.Disabled(disabled); + using (var id = ImUtf8.PushId("FurtherA"u8)) + retA |= DrawFurther(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + columns.Next(); + using (var id = ImUtf8.PushId("FurtherB"u8)) + retB |= DrawFurther(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } + + if (retA) + UpdateColorTableRowPreview(_colorTableSelectedPair << 1); + if (retB) + UpdateColorTableRowPreview((_colorTableSelectedPair << 1) | 1); + + return retA | retB; + } + + /// Padding styles do not seem to apply to this component. It is recommended to prepend two spaces. + private static void DrawHeader(ReadOnlySpan label) + { + var headerColor = ImGui.GetColorU32(ImGuiCol.Header); + using var _ = ImRaii.PushColor(ImGuiCol.HeaderHovered, headerColor).Push(ImGuiCol.HeaderActive, headerColor); + ImUtf8.CollapsingHeader(label, ImGuiTreeNodeFlags.Leaf); + } + + private static bool DrawColors(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) + { + var dyeOffset = ImGui.GetContentRegionAvail().X + ImGui.GetStyle().ItemSpacing.X - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() * 2.0f; + + var ret = false; + ref var row = ref table[rowIdx]; + var dye = dyeTable != null ? dyeTable[rowIdx] : default; + + ret |= CtColorPicker("Diffuse Color"u8, default, row.DiffuseColor, + c => table[rowIdx].DiffuseColor = c); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeDiffuseColor"u8, "Apply Diffuse Color on Dye"u8, dye.DiffuseColor, + b => dyeTable[rowIdx].DiffuseColor = b); + ImUtf8.SameLineInner(); + CtColorPicker("##dyePreviewDiffuseColor"u8, "Dye Preview for Diffuse Color"u8, dyePack?.DiffuseColor); + } + + ret |= CtColorPicker("Specular Color"u8, default, row.SpecularColor, + c => table[rowIdx].SpecularColor = c); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeSpecularColor"u8, "Apply Specular Color on Dye"u8, dye.SpecularColor, + b => dyeTable[rowIdx].SpecularColor = b); + ImUtf8.SameLineInner(); + CtColorPicker("##dyePreviewSpecularColor"u8, "Dye Preview for Specular Color"u8, dyePack?.SpecularColor); + } + + ret |= CtColorPicker("Emissive Color"u8, default, row.EmissiveColor, + c => table[rowIdx].EmissiveColor = c); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeEmissiveColor"u8, "Apply Emissive Color on Dye"u8, dye.EmissiveColor, + b => dyeTable[rowIdx].EmissiveColor = b); + ImUtf8.SameLineInner(); + CtColorPicker("##dyePreviewEmissiveColor"u8, "Dye Preview for Emissive Color"u8, dyePack?.EmissiveColor); + } + + return ret; + } + + private static bool DrawBlending(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) + { + var scalarSize = ColorTableScalarSize * UiHelpers.Scale; + var dyeOffset = ImGui.GetContentRegionAvail().X + ImGui.GetStyle().ItemSpacing.X - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize; + + var isRowB = (rowIdx & 1) != 0; + + var ret = false; + ref var row = ref table[rowIdx]; + var dye = dyeTable != null ? dyeTable[rowIdx] : default; + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf(isRowB ? "Field #19"u8 : "Anisotropy Degree"u8, default, row.Anisotropy, "%.2f"u8, 0.0f, HalfMaxValue, 0.1f, + v => table[rowIdx].Anisotropy = v); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeAnisotropy"u8, isRowB ? "Apply Field #19 on Dye"u8 : "Apply Anisotropy Degree on Dye"u8, dye.Anisotropy, + b => dyeTable[rowIdx].Anisotropy = b); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + CtDragHalf("##dyePreviewAnisotropy"u8, isRowB ? "Dye Preview for Field #19"u8 : "Dye Preview for Anisotropy Degree"u8, dyePack?.Anisotropy, "%.2f"u8); + } + + return ret; + } + + private bool DrawTemplate(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) + { + var scalarSize = ColorTableScalarSize * UiHelpers.Scale; + var itemSpacing = ImGui.GetStyle().ItemSpacing.X; + var dyeOffset = ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize - 64.0f; + var subcolWidth = CalculateSubcolumnWidth(2); + + var ret = false; + ref var row = ref table[rowIdx]; + var dye = dyeTable != null ? dyeTable[rowIdx] : default; + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Shader ID"u8, default, row.ShaderId, "%d"u8, (ushort)0, (ushort)255, 0.25f, + v => table[rowIdx].ShaderId = v); + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + + ImGui.SetNextItemWidth(scalarSize + itemSpacing + 64.0f); + ret |= CtSphereMapIndexPicker("###SphereMapIndex"u8, default, row.SphereMapIndex, false, + v => table[rowIdx].SphereMapIndex = v); + ImUtf8.SameLineInner(); + ImUtf8.Text("Sphere Map"u8); + if (dyeTable != null) + { + var textRectMin = ImGui.GetItemRectMin(); + var textRectMax = ImGui.GetItemRectMax(); + ImGui.SameLine(dyeOffset); + var cursor = ImGui.GetCursorScreenPos(); + ImGui.SetCursorScreenPos(cursor with { Y = float.Lerp(textRectMin.Y, textRectMax.Y, 0.5f) - ImGui.GetFrameHeight() * 0.5f }); + ret |= CtApplyStainCheckbox("##dyeSphereMapIndex"u8, "Apply Sphere Map on Dye"u8, dye.SphereMapIndex, + b => dyeTable[rowIdx].SphereMapIndex = b); + ImUtf8.SameLineInner(); + ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() with { Y = cursor.Y }); + ImGui.SetNextItemWidth(scalarSize + itemSpacing + 64.0f); + using var dis = ImRaii.Disabled(); + CtSphereMapIndexPicker("###SphereMapIndexDye"u8, "Dye Preview for Sphere Map"u8, dyePack?.SphereMapIndex ?? ushort.MaxValue, false, Nop); + } + + ImGui.Dummy(new Vector2(64.0f, 0.0f)); + ImGui.SameLine(); + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Sphere Map Intensity"u8, default, (float)row.SphereMapMask * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].SphereMapMask = (Half)(v * 0.01f)); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeSphereMapMask"u8, "Apply Sphere Map Intensity on Dye"u8, dye.SphereMapMask, + b => dyeTable[rowIdx].SphereMapMask = b); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + CtDragScalar("##dyeSphereMapMask"u8, "Dye Preview for Sphere Map Intensity"u8, (float?)dyePack?.SphereMapMask * 100.0f, "%.0f%%"u8); + } + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + + var leftLineHeight = 64.0f + ImGui.GetStyle().FramePadding.Y * 2.0f; + var rightLineHeight = 3.0f * ImGui.GetFrameHeight() + 2.0f * ImGui.GetStyle().ItemSpacing.Y; + var lineHeight = Math.Max(leftLineHeight, rightLineHeight); + var cursorPos = ImGui.GetCursorScreenPos(); + ImGui.SetCursorScreenPos(cursorPos + new Vector2(0.0f, (lineHeight - leftLineHeight) * 0.5f)); + ImGui.SetNextItemWidth(scalarSize + (itemSpacing + 64.0f) * 2.0f); + ret |= CtTileIndexPicker("###TileIndex"u8, default, row.TileIndex, false, + v => table[rowIdx].TileIndex = v); + ImUtf8.SameLineInner(); + ImUtf8.Text("Tile"u8); + + ImGui.SameLine(subcolWidth); + ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() with { Y = cursorPos.Y + (lineHeight - rightLineHeight) * 0.5f, }); + using (var cld = ImUtf8.Child("###TileProperties"u8, new(ImGui.GetContentRegionAvail().X, float.Lerp(rightLineHeight, lineHeight, 0.5f)), false)) + { + ImGui.Dummy(new Vector2(scalarSize, 0.0f)); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Tile Opacity"u8, default, (float)row.TileAlpha * 100.0f, "%.0f%%"u8, 0.0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].TileAlpha = (Half)(v * 0.01f)); + + ret |= CtTileTransformMatrix(row.TileTransform, scalarSize, true, + m => table[rowIdx].TileTransform = m); + ImUtf8.SameLineInner(); + ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() - new Vector2(0.0f, (ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y) * 0.5f)); + ImUtf8.Text("Tile Transform"u8); + } + + return ret; + } + + private static bool DrawPbr(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) + { + var scalarSize = ColorTableScalarSize * UiHelpers.Scale; + var subcolWidth = CalculateSubcolumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; + var dyeOffset = subcolWidth - ImGui.GetStyle().ItemSpacing.X * 2.0f - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize; + + var ret = false; + ref var row = ref table[rowIdx]; + var dye = dyeTable != null ? dyeTable[rowIdx] : default; + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Roughness"u8, default, (float)row.Roughness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].Roughness = (Half)(v * 0.01f)); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeRoughness"u8, "Apply Roughness on Dye"u8, dye.Roughness, + b => dyeTable[rowIdx].Roughness = b); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + CtDragScalar("##dyePreviewRoughness"u8, "Dye Preview for Roughness"u8, (float?)dyePack?.Roughness * 100.0f, "%.0f%%"u8); + } + + ImGui.SameLine(subcolWidth); + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Metalness"u8, default, (float)row.Metalness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].Metalness = (Half)(v * 0.01f)); + if (dyeTable != null) + { + ImGui.SameLine(subcolWidth + dyeOffset); + ret |= CtApplyStainCheckbox("##dyeMetalness"u8, "Apply Metalness on Dye"u8, dye.Metalness, + b => dyeTable[rowIdx].Metalness = b); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + CtDragScalar("##dyePreviewMetalness"u8, "Dye Preview for Metalness"u8, (float?)dyePack?.Metalness * 100.0f, "%.0f%%"u8); + } + + return ret; + } + + private static bool DrawSheen(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) + { + var scalarSize = ColorTableScalarSize * UiHelpers.Scale; + var subcolWidth = CalculateSubcolumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; + var dyeOffset = subcolWidth - ImGui.GetStyle().ItemSpacing.X * 2.0f - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize; + + var ret = false; + ref var row = ref table[rowIdx]; + var dye = dyeTable != null ? dyeTable[rowIdx] : default; + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Sheen"u8, default, (float)row.SheenRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].SheenRate = (Half)(v * 0.01f)); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeSheenRate"u8, "Apply Sheen on Dye"u8, dye.SheenRate, + b => dyeTable[rowIdx].SheenRate = b); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + CtDragScalar("##dyePreviewSheenRate"u8, "Dye Preview for Sheen"u8, (float?)dyePack?.SheenRate * 100.0f, "%.0f%%"u8); + } + + ImGui.SameLine(subcolWidth); + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Sheen Tint"u8, default, (float)row.SheenTintRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].SheenTintRate = (Half)(v * 0.01f)); + if (dyeTable != null) + { + ImGui.SameLine(subcolWidth + dyeOffset); + ret |= CtApplyStainCheckbox("##dyeSheenTintRate"u8, "Apply Sheen Tint on Dye"u8, dye.SheenTintRate, + b => dyeTable[rowIdx].SheenTintRate = b); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + CtDragScalar("##dyePreviewSheenTintRate"u8, "Dye Preview for Sheen Tint"u8, (float?)dyePack?.SheenTintRate * 100.0f, "%.0f%%"u8); + } + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Sheen Roughness"u8, default, 100.0f / (float)row.SheenAperture, "%.0f%%"u8, 100.0f / HalfMaxValue, 100.0f / HalfEpsilon, 1.0f, + v => table[rowIdx].SheenAperture = (Half)(100.0f / v)); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeSheenRoughness"u8, "Apply Sheen Roughness on Dye"u8, dye.SheenAperture, + b => dyeTable[rowIdx].SheenAperture = b); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + CtDragScalar("##dyePreviewSheenRoughness"u8, "Dye Preview for Sheen Roughness"u8, 100.0f / (float?)dyePack?.SheenAperture, "%.0f%%"u8); + } + + return ret; + } + + private static bool DrawFurther(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) + { + var scalarSize = ColorTableScalarSize * UiHelpers.Scale; + var subcolWidth = CalculateSubcolumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; + var dyeOffset = subcolWidth - ImGui.GetStyle().ItemSpacing.X * 2.0f - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize; + + var ret = false; + ref var row = ref table[rowIdx]; + var dye = dyeTable != null ? dyeTable[rowIdx] : default; + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf("Field #11"u8, default, row.Scalar11, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, + v => table[rowIdx].Scalar11 = v); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeScalar11"u8, "Apply Field #11 on Dye"u8, dye.Scalar3, + b => dyeTable[rowIdx].Scalar3 = b); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + CtDragHalf("##dyePreviewScalar11"u8, "Dye Preview for Field #11"u8, dyePack?.Scalar3, "%.2f"u8); + } + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf("Field #3"u8, default, row.Scalar3, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, + v => table[rowIdx].Scalar3 = v); + + ImGui.SameLine(subcolWidth); + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf("Field #7"u8, default, row.Scalar7, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, + v => table[rowIdx].Scalar7 = v); + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf("Field #15"u8, default, row.Scalar15, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, + v => table[rowIdx].Scalar15 = v); + + ImGui.SameLine(subcolWidth); + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf("Field #17"u8, default, row.Scalar17, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, + v => table[rowIdx].Scalar17 = v); + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf("Field #20"u8, default, row.Scalar20, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, + v => table[rowIdx].Scalar20 = v); + + ImGui.SameLine(subcolWidth); + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf("Field #22"u8, default, row.Scalar22, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, + v => table[rowIdx].Scalar22 = v); + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf("Field #23"u8, default, row.Scalar23, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, + v => table[rowIdx].Scalar23 = v); + + return ret; + } + + private bool DrawDye(ColorDyeTable dyeTable, DyePack? dyePack, int rowIdx) + { + var scalarSize = ColorTableScalarSize * UiHelpers.Scale; + var applyButtonWidth = ImUtf8.CalcTextSize("Apply Preview Dye"u8).X + ImGui.GetStyle().FramePadding.X * 2.0f; + var subcolWidth = CalculateSubcolumnWidth(2, applyButtonWidth); + + var ret = false; + ref var dye = ref dyeTable[rowIdx]; + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Dye Channel"u8, default, dye.Channel + 1, "%d"u8, 1, StainService.ChannelCount, 0.1f, + value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1)); + ImGui.SameLine(subcolWidth); + ImGui.SetNextItemWidth(scalarSize); + if (_stainService.GudTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, + scalarSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) + { + dye.Template = _stainService.LegacyTemplateCombo.CurrentSelection; + ret = true; + } + ImUtf8.SameLineInner(); + ImUtf8.Text("Dye Template"u8); + ImGui.SameLine(ImGui.GetContentRegionAvail().X - applyButtonWidth + ImGui.GetStyle().ItemSpacing.X); + using var dis = ImRaii.Disabled(!dyePack.HasValue); + if (ImUtf8.Button("Apply Preview Dye"u8)) + { + ret |= Mtrl.ApplyDyeToRow(_stainService.GudStmFile, [ + _stainService.StainCombo1.CurrentSelection.Key, + _stainService.StainCombo2.CurrentSelection.Key, + ], rowIdx); + } + + return ret; + } + + private static void CenteredTextInRest(string text) + => AlignedTextInRest(text, 0.5f); + + private static void AlignedTextInRest(string text, float alignment) + { + var width = ImGui.CalcTextSize(text).X; + ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() + new Vector2((ImGui.GetContentRegionAvail().X - width) * alignment, 0.0f)); + ImGui.TextUnformatted(text); + } + + private static float CalculateSubcolumnWidth(int numSubcolumns, float reservedSpace = 0.0f) + { + var itemSpacing = ImGui.GetStyle().ItemSpacing.X; + return (ImGui.GetContentRegionAvail().X - reservedSpace - itemSpacing * (numSubcolumns - 1)) / numSubcolumns + itemSpacing; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs index 937614de..2b093e23 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs @@ -52,6 +52,7 @@ public partial class MtrlTab { LegacyColorTable legacyTable => DrawLegacyColorTable(legacyTable, Mtrl.DyeTable as LegacyColorDyeTable, disabled), ColorTable table when Mtrl.ShaderPackage.Name is "characterlegacy.shpk" => DrawLegacyColorTable(table, Mtrl.DyeTable as ColorDyeTable, disabled), + ColorTable table => DrawColorTable(table, Mtrl.DyeTable as ColorDyeTable, disabled), _ => false, }; From 5323add662874d3b0286f8ed35930b620fd99630 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:45:52 +0200 Subject: [PATCH 08/12] Improve ShPk tab --- .../ModEditWindow.ShaderPackages.cs | 287 ++++++++++++++---- .../AdvancedWindow/ModEditWindow.ShpkTab.cs | 287 ++++++++++++++++-- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- 3 files changed, 494 insertions(+), 82 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index 017478a7..8a1c729c 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -1,7 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; using ImGuiNET; -using Lumina.Misc; using OtterGui.Raii; using OtterGui; using OtterGui.Classes; @@ -11,6 +10,9 @@ using Penumbra.GameData.Interop; using Penumbra.String; using static Penumbra.GameData.Files.ShpkFile; using OtterGui.Widgets; +using Penumbra.GameData.Files.ShaderStructs; +using OtterGui.Text; +using Penumbra.GameData.Structs; namespace Penumbra.UI.AdvancedWindow; @@ -24,6 +26,9 @@ public partial class ModEditWindow { DrawShaderPackageSummary(file); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + DrawShaderPackageFilterSection(file); + var ret = false; ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); ret |= DrawShaderPackageShaderArray(file, "Vertex Shader", file.Shpk.VertexShaders, disabled); @@ -50,15 +55,16 @@ public partial class ModEditWindow private static void DrawShaderPackageSummary(ShpkTab tab) { + if (tab.Shpk.IsLegacy) + { + ImUtf8.Text("This legacy shader package will not work in the current version of the game. Do not attempt to load it.", + ImGuiUtil.HalfBlendText(0x80u)); // Half red + } 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."); + ImUtf8.Text("Your system doesn't support disassembling shaders. Some functionality will be missing.", + ImGuiUtil.HalfBlendText(0x80u)); // Half red } } @@ -123,6 +129,7 @@ public partial class ModEditWindow { shaders[idx].UpdateResources(tab.Shpk); tab.Shpk.UpdateResources(); + tab.UpdateFilteredUsed(); } catch (Exception e) { @@ -149,6 +156,97 @@ public partial class ModEditWindow ImGuiInputTextFlags.ReadOnly, null, null); } + private static void DrawShaderUsage(ShpkTab tab, Shader shader) + { + using (var node = ImUtf8.TreeNode("Used with Shader Keys"u8)) + { + if (node) + { + foreach (var (key, keyIdx) in shader.SystemValues!.WithIndex()) + { + ImRaii.TreeNode($"Used with System Key {tab.TryResolveName(tab.Shpk.SystemKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } + + foreach (var (key, keyIdx) in shader.SceneValues!.WithIndex()) + { + ImRaii.TreeNode($"Used with Scene Key {tab.TryResolveName(tab.Shpk.SceneKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } + + foreach (var (key, keyIdx) in shader.MaterialValues!.WithIndex()) + { + ImRaii.TreeNode($"Used with Material Key {tab.TryResolveName(tab.Shpk.MaterialKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } + + foreach (var (key, keyIdx) in shader.SubViewValues!.WithIndex()) + { + ImRaii.TreeNode($"Used with Sub-View Key #{keyIdx} \u2208 {{ {tab.NameSetToString(key)} }}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } + } + } + + ImRaii.TreeNode($"Used in Passes: {tab.NameSetToString(shader.Passes)}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } + + private static void DrawShaderPackageFilterSection(ShpkTab tab) + { + if (!ImUtf8.CollapsingHeader(tab.FilterPopCount == tab.FilterMaximumPopCount ? "Filters###Filters"u8 : "Filters (ACTIVE)###Filters"u8)) + return; + + foreach (var (key, keyIdx) in tab.Shpk.SystemKeys.WithIndex()) + DrawShaderPackageFilterSet(tab, $"System Key {tab.TryResolveName(key.Id)}", ref tab.FilterSystemValues[keyIdx]); + + foreach (var (key, keyIdx) in tab.Shpk.SceneKeys.WithIndex()) + DrawShaderPackageFilterSet(tab, $"Scene Key {tab.TryResolveName(key.Id)}", ref tab.FilterSceneValues[keyIdx]); + + foreach (var (key, keyIdx) in tab.Shpk.MaterialKeys.WithIndex()) + DrawShaderPackageFilterSet(tab, $"Material Key {tab.TryResolveName(key.Id)}", ref tab.FilterMaterialValues[keyIdx]); + + foreach (var (_, keyIdx) in tab.Shpk.SubViewKeys.WithIndex()) + DrawShaderPackageFilterSet(tab, $"Sub-View Key #{keyIdx}", ref tab.FilterSubViewValues[keyIdx]); + + DrawShaderPackageFilterSet(tab, "Passes", ref tab.FilterPasses); + } + + private static void DrawShaderPackageFilterSet(ShpkTab tab, string label, ref SharedSet values) + { + if (values.PossibleValues == null) + { + ImRaii.TreeNode(label, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + return; + } + + using var node = ImRaii.TreeNode(label); + if (!node) + return; + + foreach (var value in values.PossibleValues) + { + var contains = values.Contains(value); + if (!ImGui.Checkbox($"{tab.TryResolveName(value)}", ref contains)) + continue; + if (contains) + { + if (values.AddExisting(value)) + { + ++tab.FilterPopCount; + tab.UpdateFilteredUsed(); + } + } + else + { + if (values.Remove(value)) + { + --tab.FilterPopCount; + tab.UpdateFilteredUsed(); + } + } + } + } + private static bool DrawShaderPackageShaderArray(ShpkTab tab, string objectName, Shader[] shaders, bool disabled) { if (shaders.Length == 0 || !ImGui.CollapsingHeader($"{objectName}s")) @@ -157,8 +255,11 @@ public partial class ModEditWindow var ret = false; for (var idx = 0; idx < shaders.Length; ++idx) { - var shader = shaders[idx]; - using var t = ImRaii.TreeNode($"{objectName} #{idx}"); + var shader = shaders[idx]; + if (!tab.IsFilterMatch(shader)) + continue; + + using var t = ImRaii.TreeNode($"{objectName} #{idx}"); if (!t) continue; @@ -169,9 +270,11 @@ public partial class ModEditWindow 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, false, true); + ret |= DrawShaderPackageResourceArray("Samplers", "slot", false, shader.Samplers, false, true); + if (!tab.Shpk.IsLegacy) + ret |= DrawShaderPackageResourceArray("Textures", "slot", false, shader.Textures, false, true); + ret |= DrawShaderPackageResourceArray("Unordered Access Views", "slot", true, shader.Uavs, false, true); if (shader.DeclaredInputs != 0) ImRaii.TreeNode($"Declared Inputs: {shader.DeclaredInputs}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); @@ -187,12 +290,14 @@ public partial class ModEditWindow if (tab.Shpk.Disassembled) DrawRawDisassembly(shader); + + DrawShaderUsage(tab, 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 hasFilter, bool disabled) { var ret = false; if (!disabled) @@ -205,16 +310,26 @@ public partial class ModEditWindow if (resource.Used == null) return ret; - var usedString = UsedComponentString(withSize, resource); + var usedString = UsedComponentString(withSize, false, resource); if (usedString.Length > 0) - ImRaii.TreeNode($"Used: {usedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + { + ImRaii.TreeNode(hasFilter ? $"Globally Used: {usedString}" : $"Used: {usedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + if (hasFilter) + { + var filteredUsedString = UsedComponentString(withSize, true, resource); + if (filteredUsedString.Length > 0) + ImRaii.TreeNode($"Used within Filters: {filteredUsedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + else + ImRaii.TreeNode("Unused within Filters", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } + } else - ImRaii.TreeNode("Unused", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImRaii.TreeNode(hasFilter ? "Globally Unused" : "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 hasFilter, bool disabled) { if (resources.Length == 0) return false; @@ -233,7 +348,7 @@ public partial class ModEditWindow 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); + ret |= DrawShaderPackageResource(slotLabel, withSize, ref buf, hasFilter, disabled); } return ret; @@ -268,7 +383,7 @@ public partial class ModEditWindow private static bool DrawShaderPackageMaterialMatrix(ShpkTab tab, bool disabled) { ImGui.TextUnformatted(tab.Shpk.Disassembled - ? "Parameter positions (continuations are grayed out, unused values are red):" + ? "Parameter positions (continuations are grayed out, globally unused values are red, unused values within filters are yellow):" : "Parameter positions (continuations are grayed out):"); using var table = ImRaii.Table("##MaterialParamLayout", 5, @@ -276,17 +391,17 @@ public partial class ModEditWindow 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, 40 * UiHelpers.Scale); + ImGui.TableSetupColumn("x", ImGuiTableColumnFlags.WidthFixed, 250 * UiHelpers.Scale); + ImGui.TableSetupColumn("y", ImGuiTableColumnFlags.WidthFixed, 250 * UiHelpers.Scale); + ImGui.TableSetupColumn("z", ImGuiTableColumnFlags.WidthFixed, 250 * UiHelpers.Scale); + ImGui.TableSetupColumn("w", ImGuiTableColumnFlags.WidthFixed, 250 * 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 textColorCont = ImGuiUtil.HalfTransparent(textColorStart); // Half opacity + var textColorUnusedStart = ImGuiUtil.HalfBlend(textColorStart, 0x80u); // Half red + var textColorUnusedCont = ImGuiUtil.HalfTransparent(textColorUnusedStart); var ret = false; for (var i = 0; i < tab.Matrix.GetLength(0); ++i) @@ -296,14 +411,13 @@ public partial class ModEditWindow for (var j = 0; j < 4; ++j) { var (name, tooltip, idx, colorType) = tab.Matrix[i, j]; - var color = colorType switch - { - ShpkTab.ColorType.Unused => textColorUnusedStart, - ShpkTab.ColorType.Used => textColorStart, - ShpkTab.ColorType.Continuation => textColorUnusedCont, - ShpkTab.ColorType.Continuation | ShpkTab.ColorType.Used => textColorCont, - _ => textColorStart, - }; + var color = textColorStart; + if (!colorType.HasFlag(ShpkTab.ColorType.Used)) + color = ImGuiUtil.HalfBlend(color, 0x80u); // Half red + else if (!colorType.HasFlag(ShpkTab.ColorType.FilteredUsed)) + color = ImGuiUtil.HalfBlend(color, 0x8080u); // Half yellow + if (colorType.HasFlag(ShpkTab.ColorType.Continuation)) + color = ImGuiUtil.HalfTransparent(color); // Half opacity using var _ = ImRaii.PushId(i * 4 + j); var deletable = !disabled && idx >= 0; using (var font = ImRaii.PushFont(UiBuilder.MonoFont, tooltip.Length > 0)) @@ -331,6 +445,35 @@ public partial class ModEditWindow return ret; } + private static void DrawShaderPackageMaterialDevkitExport(ShpkTab tab) + { + if (!ImUtf8.Button("Export globally unused parameters as material dev-kit file"u8)) + return; + + tab.FileDialog.OpenSavePicker("Export material dev-kit file", ".json", $"{Path.GetFileNameWithoutExtension(tab.FilePath)}.json", ".json", DoSave, null, false); + + void DoSave(bool success, string path) + { + if (!success) + return; + + try + { + File.WriteAllText(path, tab.ExportDevkit().ToString()); + } + catch (Exception e) + { + Penumbra.Messager.NotificationMessage(e, $"Could not export dev-kit for {Path.GetFileName(tab.FilePath)} to {path}.", + NotificationType.Error, false); + return; + } + + Penumbra.Messager.NotificationMessage( + $"Material dev-kit file for {Path.GetFileName(tab.FilePath)} exported successfully to {Path.GetFileName(path)}.", + NotificationType.Success, false); + } + } + private static void DrawShaderPackageMisalignedParameters(ShpkTab tab) { using var t = ImRaii.TreeNode("Misaligned / Overflowing Parameters"); @@ -396,23 +539,25 @@ public partial class ModEditWindow DrawShaderPackageEndCombo(tab); ImGui.SetNextItemWidth(UiHelpers.Scale * 400); - if (ImGui.InputText("Name", ref tab.NewMaterialParamName, 63)) - tab.NewMaterialParamId = Crc32.Get(tab.NewMaterialParamName, 0xFFFFFFFFu); + var newName = tab.NewMaterialParamName.Value!; + if (ImGui.InputText("Name", ref newName, 63)) + tab.NewMaterialParamName = newName; - var tooltip = tab.UsedIds.Contains(tab.NewMaterialParamId) + var tooltip = tab.UsedIds.Contains(tab.NewMaterialParamName.Crc32) ? "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()), + if (!ImGuiUtil.DrawDisabledButton($"Add {tab.NewMaterialParamName} (0x{tab.NewMaterialParamName.Crc32:X8})", new Vector2(400 * UiHelpers.Scale, ImGui.GetFrameHeight()), tooltip, tooltip.Length > 0)) return false; tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.AddItem(new MaterialParam { - Id = tab.NewMaterialParamId, + Id = tab.NewMaterialParamName.Crc32, ByteOffset = (ushort)(tab.Orphans[tab.NewMaterialParamStart].Index << 2), ByteSize = (ushort)((tab.NewMaterialParamEnd - tab.NewMaterialParamStart + 1) << 2), }); + tab.AddNameToCache(tab.NewMaterialParamName); tab.Update(); return true; } @@ -434,6 +579,9 @@ public partial class ModEditWindow else if (!disabled && sizeWellDefined) ret |= DrawShaderPackageNewParameter(tab); + if (tab.Shpk.Disassembled) + DrawShaderPackageMaterialDevkitExport(tab); + return ret; } @@ -444,14 +592,17 @@ public partial class ModEditWindow 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); + var hasFilters = tab.FilterPopCount != tab.FilterMaximumPopCount; + ret |= DrawShaderPackageResourceArray("Constant Buffers", "type", true, tab.Shpk.Constants, hasFilters, disabled); + ret |= DrawShaderPackageResourceArray("Samplers", "type", false, tab.Shpk.Samplers, hasFilters, disabled); + if (!tab.Shpk.IsLegacy) + ret |= DrawShaderPackageResourceArray("Textures", "type", false, tab.Shpk.Textures, hasFilters, disabled); + ret |= DrawShaderPackageResourceArray("Unordered Access Views", "type", false, tab.Shpk.Uavs, hasFilters, disabled); return ret; } - private static void DrawKeyArray(string arrayName, bool withId, IReadOnlyCollection keys) + private static void DrawKeyArray(ShpkTab tab, string arrayName, bool withId, IReadOnlyCollection keys) { if (keys.Count == 0) return; @@ -463,12 +614,11 @@ public partial class ModEditWindow 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}"); + using var t2 = ImRaii.TreeNode(withId ? $"#{idx}: {tab.TryResolveName(key.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: {tab.TryResolveName(key.DefaultValue)} (0x{key.DefaultValue:X8})", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImRaii.TreeNode($"Known Values: {tab.NameSetToString(key.Values, true)}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } } } @@ -482,39 +632,46 @@ public partial class ModEditWindow if (!t) return; + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + 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 (!tab.IsFilterMatch(node)) + continue; + + 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}", + ImRaii.TreeNode($"System Key {tab.TryResolveName(tab.Shpk.SystemKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SystemValues![keyIdx])} }}", 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}", + ImRaii.TreeNode($"Scene Key {tab.TryResolveName(tab.Shpk.SceneKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SceneValues![keyIdx])} }}", 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}", + ImRaii.TreeNode($"Material Key {tab.TryResolveName(tab.Shpk.MaterialKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.MaterialValues![keyIdx])} }}", 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($"Sub-View Key #{keyIdx} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SubViewValues![keyIdx])} }}", + 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 #{passIdx}: ID: 0x{pass.Id:X8}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", + ImRaii.TreeNode($"Pass #{passIdx}: ID: {tab.TryResolveName(pass.Id)}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet) .Dispose(); } @@ -526,10 +683,10 @@ public partial class ModEditWindow 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(tab, "System Keys", true, tab.Shpk.SystemKeys); + DrawKeyArray(tab, "Scene Keys", true, tab.Shpk.SceneKeys); + DrawKeyArray(tab, "Material Keys", true, tab.Shpk.MaterialKeys); + DrawKeyArray(tab, "Sub-View Keys", false, tab.Shpk.SubViewKeys); DrawShaderPackageNodes(tab); using var t = ImRaii.TreeNode($"Node Selectors ({tab.Shpk.NodeSelectors.Count})###NodeSelectors"); @@ -559,12 +716,14 @@ public partial class ModEditWindow } } - private static string UsedComponentString(bool withSize, in Resource resource) + private static string UsedComponentString(bool withSize, bool filtered, in Resource resource) { + var used = filtered ? resource.FilteredUsed : resource.Used; + var usedDynamically = filtered ? resource.FilteredUsedDynamically : resource.UsedDynamically; var sb = new StringBuilder(256); if (withSize) { - foreach (var (components, i) in (resource.Used ?? Array.Empty()).WithIndex()) + foreach (var (components, i) in (used ?? Array.Empty()).WithIndex()) { switch (components) { @@ -582,7 +741,7 @@ public partial class ModEditWindow } } - switch (resource.UsedDynamically ?? 0) + switch (usedDynamically ?? 0) { case 0: break; case DisassembledShader.VectorComponents.All: @@ -590,7 +749,7 @@ public partial class ModEditWindow break; default: sb.Append("[*]."); - foreach (var c in resource.UsedDynamically!.Value.ToString().Where(char.IsUpper)) + foreach (var c in usedDynamically!.Value.ToString().Where(char.IsUpper)) sb.Append(char.ToLower(c)); sb.Append(", "); @@ -599,7 +758,7 @@ public partial class ModEditWindow } else { - var components = (resource.Used is { Length: > 0 } ? resource.Used[0] : 0) | (resource.UsedDynamically ?? 0); + var components = (used is { Length: > 0 } ? used[0] : 0) | (usedDynamically ?? 0); if ((components & DisassembledShader.VectorComponents.X) != 0) sb.Append("Red, "); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs index 12b8d761..de20aa9f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs @@ -1,9 +1,12 @@ using Dalamud.Utility; -using Lumina.Misc; +using Newtonsoft.Json.Linq; using OtterGui; -using Penumbra.GameData.Data; +using OtterGui.Classes; using Penumbra.GameData.Files; +using Penumbra.GameData.Files.ShaderStructs; using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; +using Penumbra.UI.AdvancedWindow.Materials; namespace Penumbra.UI.AdvancedWindow; @@ -12,18 +15,27 @@ public partial class ModEditWindow private class ShpkTab : IWritable { public readonly ShpkFile Shpk; + public readonly string FilePath; - public string NewMaterialParamName = string.Empty; - public uint NewMaterialParamId = Crc32.Get(string.Empty, 0xFFFFFFFFu); - public short NewMaterialParamStart; - public short NewMaterialParamEnd; + public Name NewMaterialParamName = string.Empty; + public short NewMaterialParamStart; + public short NewMaterialParamEnd; + + public SharedSet[] FilterSystemValues; + public SharedSet[] FilterSceneValues; + public SharedSet[] FilterMaterialValues; + public SharedSet[] FilterSubViewValues; + public SharedSet FilterPasses; + + public readonly int FilterMaximumPopCount; + public int FilterPopCount; public readonly FileDialogService FileDialog; public readonly string Header; public readonly string Extension; - public ShpkTab(FileDialogService fileDialog, byte[] bytes) + public ShpkTab(FileDialogService fileDialog, byte[] bytes, string filePath) { FileDialog = fileDialog; try @@ -34,6 +46,7 @@ public partial class ModEditWindow { Shpk = new ShpkFile(bytes, false); } + FilePath = filePath; Header = $"Shader Package for DirectX {(int)Shpk.DirectXVersion}"; Extension = Shpk.DirectXVersion switch @@ -42,15 +55,36 @@ public partial class ModEditWindow ShpkFile.DxVersion.DirectX11 => ".dxbc", _ => throw new NotImplementedException(), }; + + FilterSystemValues = Array.ConvertAll(Shpk.SystemKeys, key => key.Values.FullSet()); + FilterSceneValues = Array.ConvertAll(Shpk.SceneKeys, key => key.Values.FullSet()); + FilterMaterialValues = Array.ConvertAll(Shpk.MaterialKeys, key => key.Values.FullSet()); + FilterSubViewValues = Array.ConvertAll(Shpk.SubViewKeys, key => key.Values.FullSet()); + FilterPasses = Shpk.Passes.FullSet(); + + FilterMaximumPopCount = FilterPasses.Count; + foreach (var key in Shpk.SystemKeys) + FilterMaximumPopCount += key.Values.Count; + foreach (var key in Shpk.SceneKeys) + FilterMaximumPopCount += key.Values.Count; + foreach (var key in Shpk.MaterialKeys) + FilterMaximumPopCount += key.Values.Count; + foreach (var key in Shpk.SubViewKeys) + FilterMaximumPopCount += key.Values.Count; + + FilterPopCount = FilterMaximumPopCount; + + UpdateNameCache(); + Shpk.UpdateFilteredUsed(IsFilterMatch); Update(); } [Flags] public enum ColorType : byte { - Unused = 0, Used = 1, - Continuation = 2, + FilteredUsed = 2, + Continuation = 4, } public (string Name, string Tooltip, short Index, ColorType Color)[,] Matrix = null!; @@ -58,10 +92,87 @@ public partial class ModEditWindow public readonly HashSet UsedIds = new(16); public readonly List<(string Name, short Index)> Orphans = new(16); + private readonly Dictionary _nameCache = []; + private readonly Dictionary, string> _nameSetCache = []; + private readonly Dictionary, string> _nameSetWithIdsCache = []; + + public void AddNameToCache(Name name) + { + if (name.Value != null) + _nameCache.TryAdd(name.Crc32, name); + + _nameSetCache.Clear(); + _nameSetWithIdsCache.Clear(); + } + + public void UpdateNameCache() + { + static void CollectResourceNames(Dictionary nameCache, ShpkFile.Resource[] resources) + { + foreach (var resource in resources) + nameCache.TryAdd(resource.Id, resource.Name); + } + + static void CollectKeyNames(Dictionary nameCache, ShpkFile.Key[] keys) + { + foreach (var key in keys) + { + var keyName = nameCache.TryResolve(Names.KnownNames, key.Id); + var valueNames = keyName.WithKnownSuffixes(); + foreach (var value in key.Values) + { + var valueName = valueNames.TryResolve(value); + if (valueName.Value != null) + nameCache.TryAdd(value, valueName); + } + } + } + + CollectResourceNames(_nameCache, Shpk.Constants); + CollectResourceNames(_nameCache, Shpk.Samplers); + CollectResourceNames(_nameCache, Shpk.Textures); + CollectResourceNames(_nameCache, Shpk.Uavs); + + CollectKeyNames(_nameCache, Shpk.SystemKeys); + CollectKeyNames(_nameCache, Shpk.SceneKeys); + CollectKeyNames(_nameCache, Shpk.MaterialKeys); + CollectKeyNames(_nameCache, Shpk.SubViewKeys); + + _nameSetCache.Clear(); + _nameSetWithIdsCache.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Name TryResolveName(uint crc32) + => _nameCache.TryResolve(Names.KnownNames, crc32); + + public string NameSetToString(SharedSet nameSet, bool withIds = false) + { + var cache = withIds ? _nameSetWithIdsCache : _nameSetCache; + if (cache.TryGetValue(nameSet, out var nameSetStr)) + return nameSetStr; + if (withIds) + nameSetStr = string.Join(", ", nameSet.Select(id => $"{TryResolveName(id)} (0x{id:X8})")); + else + nameSetStr = string.Join(", ", nameSet.Select(TryResolveName)); + cache.Add(nameSet, nameSetStr); + return nameSetStr; + } + + public void UpdateFilteredUsed() + { + Shpk.UpdateFilteredUsed(IsFilterMatch); + + var materialParams = Shpk.GetConstantById(ShpkFile.MaterialParamsConstantId); + UpdateColors(materialParams); + } + public void Update() { var materialParams = Shpk.GetConstantById(ShpkFile.MaterialParamsConstantId); var numParameters = ((Shpk.MaterialParamsSize + 0xFu) & ~0xFu) >> 4; + var defaults = Shpk.MaterialParamsDefaults != null ? (ReadOnlySpan)Shpk.MaterialParamsDefaults : []; + var defaultFloats = MemoryMarshal.Cast(defaults); Matrix = new (string Name, string Tooltip, short Index, ColorType Color)[numParameters, 4]; MalformedParameters.Clear(); @@ -75,14 +186,14 @@ public partial class ModEditWindow var jEnd = ((param.ByteOffset + param.ByteSize - 1) >> 2) & 3; if ((param.ByteOffset & 0x3) != 0 || (param.ByteSize & 0x3) != 0) { - MalformedParameters.Add($"ID: 0x{param.Id:X8}, offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}"); + MalformedParameters.Add($"ID: {TryResolveName(param.Id)} (0x{param.Id:X8}), offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}"); continue; } if (iEnd >= numParameters) { MalformedParameters.Add( - $"{MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2)} (ID: 0x{param.Id:X8})"); + $"{MtrlTab.MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2)} ({TryResolveName(param.Id)}, 0x{param.Id:X8})"); continue; } @@ -91,9 +202,12 @@ public partial class ModEditWindow var end = i == iEnd ? jEnd : 3; for (var j = i == iStart ? jStart : 0; j <= end; ++j) { + var component = (i << 2) | j; var tt = - $"{MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2).Item1} (ID: 0x{param.Id:X8})"; - Matrix[i, j] = ($"0x{param.Id:X8}", tt, (short)idx, 0); + $"{MtrlTab.MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2).Item1} ({TryResolveName(param.Id)}, 0x{param.Id:X8})"; + if (component < defaultFloats.Length) + tt += $"\n\nDefault value: {defaultFloats[component]} ({defaults[component << 2]:X2} {defaults[(component << 2) | 1]:X2} {defaults[(component << 2) | 2]:X2} {defaults[(component << 2) | 3]:X2})"; + Matrix[i, j] = (TryResolveName(param.Id).ToString(), tt, (short)idx, 0); } } } @@ -151,7 +265,7 @@ public partial class ModEditWindow if (oldStart == linear) newMaterialParamStart = (short)Orphans.Count; - Orphans.Add(($"{materialParams?.Name ?? string.Empty}{MaterialParamName(false, linear)}", linear)); + Orphans.Add(($"{materialParams?.Name ?? ShpkFile.MaterialParamsConstantName}{MtrlTab.MaterialParamName(false, linear)}", linear)); } } @@ -168,11 +282,15 @@ public partial class ModEditWindow { var usedComponents = (materialParams?.Used?[i] ?? DisassembledShader.VectorComponents.All) | (materialParams?.UsedDynamically ?? 0); + var filteredUsedComponents = (materialParams?.FilteredUsed?[i] ?? DisassembledShader.VectorComponents.All) + | (materialParams?.FilteredUsedDynamically ?? 0); for (var j = 0; j < 4; ++j) { - var color = ((byte)usedComponents & (1 << j)) != 0 - ? ColorType.Used - : 0; + ColorType color = 0; + if (((byte)usedComponents & (1 << j)) != 0) + color |= ColorType.Used; + if (((byte)filteredUsedComponents & (1 << j)) != 0) + color |= ColorType.FilteredUsed; if (Matrix[i, j].Index == lastIndex || Matrix[i, j].Index < 0) color |= ColorType.Continuation; @@ -182,6 +300,141 @@ public partial class ModEditWindow } } + public bool IsFilterMatch(ShpkFile.Shader shader) + { + if (!FilterPasses.Overlaps(shader.Passes)) + return false; + + for (var i = 0; i < shader.SystemValues!.Length; ++i) + { + if (!FilterSystemValues[i].Overlaps(shader.SystemValues[i])) + return false; + } + + for (var i = 0; i < shader.SceneValues!.Length; ++i) + { + if (!FilterSceneValues[i].Overlaps(shader.SceneValues[i])) + return false; + } + + for (var i = 0; i < shader.MaterialValues!.Length; ++i) + { + if (!FilterMaterialValues[i].Overlaps(shader.MaterialValues[i])) + return false; + } + + for (var i = 0; i < shader.SubViewValues!.Length; ++i) + { + if (!FilterSubViewValues[i].Overlaps(shader.SubViewValues[i])) + return false; + } + + return true; + } + + public bool IsFilterMatch(ShpkFile.Node node) + { + if (!node.Passes.Any(pass => FilterPasses.Contains(pass.Id))) + return false; + + for (var i = 0; i < node.SystemValues!.Length; ++i) + { + if (!FilterSystemValues[i].Overlaps(node.SystemValues[i])) + return false; + } + + for (var i = 0; i < node.SceneValues!.Length; ++i) + { + if (!FilterSceneValues[i].Overlaps(node.SceneValues[i])) + return false; + } + + for (var i = 0; i < node.MaterialValues!.Length; ++i) + { + if (!FilterMaterialValues[i].Overlaps(node.MaterialValues[i])) + return false; + } + + for (var i = 0; i < node.SubViewValues!.Length; ++i) + { + if (!FilterSubViewValues[i].Overlaps(node.SubViewValues[i])) + return false; + } + + return true; + } + + /// + /// Generates a minimal material dev-kit file for the given shader package. + /// + /// This file currently only hides globally unused material constants. + /// + public JObject ExportDevkit() + { + var devkit = new JObject(); + + var maybeMaterialParameter = Shpk.GetConstantById(ShpkFile.MaterialParamsConstantId); + if (maybeMaterialParameter.HasValue) + { + var materialParameter = maybeMaterialParameter.Value; + var materialParameterUsage = new IndexSet(materialParameter.Size << 2, true); + + var used = materialParameter.Used ?? []; + var usedDynamically = materialParameter.UsedDynamically ?? 0; + for (var i = 0; i < used.Length; ++i) + { + for (var j = 0; j < 4; ++j) + { + if (!(used[i] | usedDynamically).HasFlag((DisassembledShader.VectorComponents)(1 << j))) + materialParameterUsage[(i << 2) | j] = false; + } + } + + var dkConstants = new JObject(); + foreach (var param in Shpk.MaterialParams) + { + // Don't handle misaligned parameters. + if ((param.ByteOffset & 0x3) != 0 || (param.ByteSize & 0x3) != 0) + continue; + + var start = param.ByteOffset >> 2; + var length = param.ByteSize >> 2; + + // If the parameter is fully used, don't include it. + if (!materialParameterUsage.Indices(start, length, true).Any()) + continue; + + var unusedSlices = new JArray(); + + if (materialParameterUsage.Indices(start, length).Any()) + { + foreach (var (rgStart, rgEnd) in materialParameterUsage.Ranges(start, length, true)) + { + unusedSlices.Add(new JObject + { + ["Type"] = "Hidden", + ["Offset"] = rgStart, + ["Length"] = rgEnd - rgStart, + }); + } + } + else + { + unusedSlices.Add(new JObject + { + ["Type"] = "Hidden", + }); + } + + dkConstants[param.Id.ToString()] = unusedSlices; + } + + devkit["Constants"] = dkConstants; + } + + return devkit; + } + public bool Valid => Shpk.Valid; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index f28cb632..13458252 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -615,7 +615,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _shaderPackageTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Shaders", ".shpk", () => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel, () => Mod?.ModPath.FullName ?? string.Empty, - (bytes, _, _) => new ShpkTab(_fileDialog, bytes)); + (bytes, path, _) => new ShpkTab(_fileDialog, bytes, path)); _center = new CombinedTexture(_left, _right); _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex)); _resourceTreeFactory = resourceTreeFactory; From c01aa000fb94afcbfc13fcfe87c4ab6bd5b5f86d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:46:29 +0200 Subject: [PATCH 09/12] Optimize I/O of ShPk for ResourceTree generation --- Penumbra/Interop/ResourceTree/ResolveContext.cs | 11 ++++++++--- Penumbra/Interop/ResourceTree/TreeBuildCache.cs | 14 +++++++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 3fc1ae3c..41485d75 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -230,8 +230,8 @@ internal unsafe partial record ResolveContext( node.Children.Add(shpkNode); } - var shpkFile = Global.WithUiData && shpkNode != null ? Global.TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null; - var shpk = Global.WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; + var shpkNames = Global.WithUiData && shpkNode != null ? Global.TreeBuildCache.ReadShaderPackageNames(shpkNode.FullPath) : null; + var shpk = Global.WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; var alreadyProcessedSamplerIds = new HashSet(); for (var i = 0; i < resource->TextureCount; i++) @@ -255,7 +255,12 @@ internal unsafe partial record ResolveContext( alreadyProcessedSamplerIds.Add(samplerId.Value); var samplerCrc = GetSamplerCrcById(shpk, samplerId.Value); if (samplerCrc.HasValue) - name = shpkFile?.GetSamplerById(samplerCrc.Value)?.Name ?? $"Texture 0x{samplerCrc.Value:X8}"; + { + if (shpkNames != null && shpkNames.TryGetValue(samplerCrc.Value, out var samplerName)) + name = samplerName.Value; + else + name = $"Texture 0x{samplerCrc.Value:X8}"; + } } } diff --git a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs index ca5ff736..49e00547 100644 --- a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs +++ b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs @@ -1,8 +1,11 @@ +using System.IO.MemoryMappedFiles; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; +using Penumbra.GameData.Files.ShaderStructs; +using Penumbra.GameData.Files.Utility; using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; using Penumbra.String.Classes; @@ -11,7 +14,7 @@ namespace Penumbra.Interop.ResourceTree; internal readonly struct TreeBuildCache(ObjectManager objects, IDataManager dataManager, ActorManager actors) { - private readonly Dictionary _shaderPackages = []; + private readonly Dictionary?> _shaderPackageNames = []; public unsafe bool IsLocalPlayerRelated(ICharacter character) { @@ -68,10 +71,10 @@ internal readonly struct TreeBuildCache(ObjectManager objects, IDataManager data } /// Try to read a shpk file from the given path and cache it on success. - public ShpkFile? ReadShaderPackage(FullPath path) - => ReadFile(dataManager, path, _shaderPackages, bytes => new ShpkFile(bytes)); + public IReadOnlyDictionary? ReadShaderPackageNames(FullPath path) + => ReadFile(dataManager, path, _shaderPackageNames, bytes => ShpkFile.FastExtractNames(bytes.Span)); - private static T? ReadFile(IDataManager dataManager, FullPath path, Dictionary cache, Func parseFile) + private static T? ReadFile(IDataManager dataManager, FullPath path, Dictionary cache, Func, T> parseFile) where T : class { if (path.FullName.Length == 0) @@ -86,7 +89,8 @@ internal readonly struct TreeBuildCache(ObjectManager objects, IDataManager data { if (path.IsRooted) { - parsed = parseFile(File.ReadAllBytes(pathStr)); + using var mmFile = MmioMemoryManager.CreateFromFile(pathStr, access: MemoryMappedFileAccess.Read); + parsed = parseFile(mmFile.Memory); } else { From ee086e3e7698a846cefa20c23dc09408b76caad8 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 4 Aug 2024 00:57:39 +0200 Subject: [PATCH 10/12] Update GameData --- Penumbra.GameData | 2 +- Penumbra/Services/StainService.cs | 4 ++-- Penumbra/UI/Tabs/Debug/DebugTab.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index ee6c6faa..1ec903d5 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ee6c6faa1e4a3e96279cb6c89df96e351f112c6a +Subproject commit 1ec903d53747fc16f62139e2ed3541f224ee3403 diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index 50713968..ba5c3e63 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -16,7 +16,7 @@ namespace Penumbra.Services; public class StainService : IService { public sealed class StainTemplateCombo(FilterComboColors[] stainCombos, StmFile stmFile) - : FilterComboCache(stmFile.Entries.Keys.Prepend((ushort)0), MouseWheelType.None, Penumbra.Log) where TDyePack : unmanaged, IDyePack + : FilterComboCache(stmFile.Entries.Keys.Prepend((ushort)0), MouseWheelType.None, Penumbra.Log) where TDyePack : unmanaged, IDyePack { // FIXME There might be a better way to handle that. public int CurrentDyeChannel = 0; @@ -102,7 +102,7 @@ public class StainService : IService }; /// Loads a STM file. Opportunistically attempts to re-use the file already read by the game, with Lumina fallback. - private static unsafe StmFile LoadStmFile(ResourceHandle* stmResourceHandle, IDataManager dataManager) where TDyePack : unmanaged, IDyePack + private static unsafe StmFile LoadStmFile(ResourceHandle* stmResourceHandle, IDataManager dataManager) where TDyePack : unmanaged, IDyePack { if (stmResourceHandle != null) { diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index ead02874..7dae19c8 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -713,7 +713,7 @@ public class DebugTab : Window, ITab, IUiService } } - private static void DrawStainTemplatesFile(StmFile stmFile) where TDyePack : unmanaged, IDyePack + private static void DrawStainTemplatesFile(StmFile stmFile) where TDyePack : unmanaged, IDyePack { foreach (var (key, data) in stmFile.Entries) { From c8e859ae05ebb9d9b7f0fbce17d3223c19c88be3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 4 Aug 2024 15:41:40 +0200 Subject: [PATCH 11/12] Fixups. --- .../Materials/MtrlTab.LegacyColorTable.cs | 38 +- .../Materials/MtrlTab.LivePreview.cs | 6 +- .../ModEditWindow.Materials.ColorTable.cs | 538 ------------ .../ModEditWindow.Materials.MtrlTab.cs | 783 ------------------ 4 files changed, 28 insertions(+), 1337 deletions(-) delete mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs delete mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs index f3ec5307..a2165760 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs @@ -33,6 +33,7 @@ public partial class MtrlTab UpdateColorTableRowPreview(i); ret = true; } + ImGui.TableNextRow(); } @@ -56,6 +57,7 @@ public partial class MtrlTab UpdateColorTableRowPreview(i); ret = true; } + ImGui.TableNextRow(); } @@ -108,8 +110,10 @@ public partial class MtrlTab } ImGui.TableNextColumn(); - using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) - ImUtf8.Text($"{(rowIdx >> 1) + 1,2:D}{"AB"[rowIdx & 1]}"); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + ImUtf8.Text($"{(rowIdx >> 1) + 1,2:D}{"AB"[rowIdx & 1]}"); + } ImGui.TableNextColumn(); using var dis = ImRaii.Disabled(disabled); @@ -131,9 +135,11 @@ public partial class MtrlTab ret |= CtApplyStainCheckbox("##dyeSpecular"u8, "Apply Specular Color on Dye"u8, dye.SpecularColor, b => dyeTable[rowIdx].SpecularColor = b); } + ImGui.SameLine(); ImGui.SetNextItemWidth(pctSize); - ret |= CtDragScalar("##SpecularMask"u8, "Specular Strength"u8, (float)row.SpecularMask * 100.0f, "%.0f%%"u8, 0f, HalfMaxValue * 100.0f, 1.0f, + ret |= CtDragScalar("##SpecularMask"u8, "Specular Strength"u8, (float)row.SpecularMask * 100.0f, "%.0f%%"u8, 0f, HalfMaxValue * 100.0f, + 1.0f, v => table[rowIdx].SpecularMask = (Half)(v * 0.01f)); if (dyeTable != null) { @@ -155,7 +161,8 @@ public partial class MtrlTab ImGui.TableNextColumn(); ImGui.SetNextItemWidth(floatSize); var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon; - ret |= CtDragHalf("##Shininess"u8, "Gloss Strength"u8, row.Shininess, "%.1f"u8, glossStrengthMin, HalfMaxValue, Math.Max(0.1f, (float)row.Shininess * 0.025f), + ret |= CtDragHalf("##Shininess"u8, "Gloss Strength"u8, row.Shininess, "%.1f"u8, glossStrengthMin, HalfMaxValue, + Math.Max(0.1f, (float)row.Shininess * 0.025f), v => table[rowIdx].Shininess = v); if (dyeTable != null) @@ -197,7 +204,7 @@ public partial class MtrlTab { using var id = ImRaii.PushId(rowIdx); ref var row = ref table[rowIdx]; - var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var dye = dyeTable?[rowIdx] ?? default; var floatSize = LegacyColorTableFloatSize * UiHelpers.Scale; var pctSize = LegacyColorTablePercentageSize * UiHelpers.Scale; var intSize = LegacyColorTableIntegerSize * UiHelpers.Scale; @@ -213,8 +220,10 @@ public partial class MtrlTab } ImGui.TableNextColumn(); - using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { ImUtf8.Text($"{(rowIdx >> 1) + 1,2:D}{"AB"[rowIdx & 1]}"); + } ImGui.TableNextColumn(); using var dis = ImRaii.Disabled(disabled); @@ -236,6 +245,7 @@ public partial class MtrlTab ret |= CtApplyStainCheckbox("##dyeSpecular"u8, "Apply Specular Color on Dye"u8, dye.SpecularColor, b => dyeTable[rowIdx].SpecularColor = b); } + ImGui.SameLine(); ImGui.SetNextItemWidth(pctSize); ret |= CtDragScalar("##SpecularMask"u8, "Specular Strength"u8, (float)row.Scalar7 * 100.0f, "%.0f%%"u8, 0f, HalfMaxValue * 100.0f, 1.0f, @@ -260,7 +270,8 @@ public partial class MtrlTab ImGui.TableNextColumn(); ImGui.SetNextItemWidth(floatSize); var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon; - ret |= CtDragHalf("##Shininess"u8, "Gloss Strength"u8, row.Scalar3, "%.1f"u8, glossStrengthMin, HalfMaxValue, Math.Max(0.1f, (float)row.Scalar3 * 0.025f), + ret |= CtDragHalf("##Shininess"u8, "Gloss Strength"u8, row.Scalar3, "%.1f"u8, glossStrengthMin, HalfMaxValue, + Math.Max(0.1f, (float)row.Scalar3 * 0.025f), v => table[rowIdx].Scalar3 = v); if (dyeTable != null) @@ -307,7 +318,7 @@ public partial class MtrlTab return ret; } - private bool DrawLegacyDyePreview(int rowIdx, bool disabled, LegacyColorDyeTable.Row dye, float floatSize) + private bool DrawLegacyDyePreview(int rowIdx, bool disabled, LegacyColorDyeTableRow dye, float floatSize) { var stain = _stainService.StainCombo1.CurrentSelection.Key; if (stain == 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values)) @@ -326,7 +337,7 @@ public partial class MtrlTab return ret; } - private bool DrawLegacyDyePreview(int rowIdx, bool disabled, ColorDyeTable.Row dye, float floatSize) + private bool DrawLegacyDyePreview(int rowIdx, bool disabled, ColorDyeTableRow dye, float floatSize) { var stain = _stainService.GetStainCombo(dye.Channel).CurrentSelection.Key; if (stain == 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values)) @@ -337,10 +348,11 @@ public partial class MtrlTab var ret = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2(ImGui.GetFrameHeight()), "Apply the selected dye to this row.", disabled, true); - ret = ret && Mtrl.ApplyDyeToRow(_stainService.LegacyStmFile, [ - _stainService.StainCombo1.CurrentSelection.Key, - _stainService.StainCombo2.CurrentSelection.Key, - ], rowIdx); + ret = ret + && Mtrl.ApplyDyeToRow(_stainService.LegacyStmFile, [ + _stainService.StainCombo1.CurrentSelection.Key, + _stainService.StainCombo2.CurrentSelection.Key, + ], rowIdx); ImGui.SameLine(); DrawLegacyDyePreview(values, floatSize); diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs index bb346534..3482e581 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs @@ -191,7 +191,7 @@ public partial class MtrlTab var row = Mtrl.Table switch { - LegacyColorTable legacyTable => new ColorTable.Row(legacyTable[rowIdx]), + LegacyColorTable legacyTable => new ColorTableRow(legacyTable[rowIdx]), ColorTable table => table[rowIdx], _ => throw new InvalidOperationException($"Unsupported color table type {Mtrl.Table.GetType()}"), }; @@ -199,7 +199,7 @@ public partial class MtrlTab { var dyeRow = Mtrl.DyeTable switch { - LegacyColorDyeTable legacyDyeTable => new ColorDyeTable.Row(legacyDyeTable[rowIdx]), + LegacyColorDyeTable legacyDyeTable => new ColorDyeTableRow(legacyDyeTable[rowIdx]), ColorDyeTable dyeTable => dyeTable[rowIdx], _ => throw new InvalidOperationException($"Unsupported color dye table type {Mtrl.DyeTable.GetType()}"), }; @@ -258,7 +258,7 @@ public partial class MtrlTab } } - private static void ApplyHighlight(ref ColorTable.Row row, ColorId colorId, float time) + private static void ApplyHighlight(ref ColorTableRow row, ColorId colorId, float time) { var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; var baseColor = colorId.Value(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs deleted file mode 100644 index cb04dc0a..00000000 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs +++ /dev/null @@ -1,538 +0,0 @@ -using Dalamud.Interface; -using Dalamud.Interface.Utility; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.GameData.Files; -using Penumbra.GameData.Files.MaterialStructs; -using Penumbra.String.Functions; - -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 DrawMaterialColorTableChange(MtrlTab tab, bool disabled) - { - if (!tab.SamplerIds.Contains(ShpkFile.TableSamplerId) || !tab.Mtrl.HasTable) - return false; - - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if (!ImGui.CollapsingHeader("Color Table", ImGuiTreeNodeFlags.DefaultOpen)) - return false; - - ColorTableCopyAllClipboardButton(tab.Mtrl); - ImGui.SameLine(); - var ret = ColorTablePasteAllClipboardButton(tab, disabled); - if (!disabled) - { - ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); - ImGui.SameLine(); - ret |= ColorTableDyeableCheckbox(tab); - } - - var hasDyeTable = tab.Mtrl.HasDyeTable; - if (hasDyeTable) - { - ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); - ImGui.SameLine(); - ret |= DrawPreviewDye(tab, disabled); - } - - using var table = ImRaii.Table("##ColorTable", hasDyeTable ? 11 : 9, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV); - if (!table) - return false; - - ImGui.TableNextColumn(); - ImGui.TableHeader(string.Empty); - ImGui.TableNextColumn(); - ImGui.TableHeader("Row"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Diffuse"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Specular"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Emissive"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Gloss"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Tile"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Repeat"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Skew"); - if (hasDyeTable) - { - ImGui.TableNextColumn(); - ImGui.TableHeader("Dye"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Dye Preview"); - } - - for (var i = 0; i < ColorTable.NumRows; ++i) - { - ret |= DrawColorTableRow(tab, i, disabled); - ImGui.TableNextRow(); - } - - return ret; - } - - - private static void ColorTableCopyAllClipboardButton(MtrlFile file) - { - if (!ImGui.Button("Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2(200, 0))) - return; - - try - { - var data1 = file.Table.AsBytes(); - var data2 = file.HasDyeTable ? file.DyeTable.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); - } - catch - { - // ignored - } - } - - 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; - if (tab.Mtrl.HasDyeTable) - for (var i = 0; i < LegacyColorTable.NumUsedRows; ++i) - ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, i, dyeId, 0); - - tab.UpdateColorTablePreview(); - - return ret; - } - - ImGui.SameLine(); - var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye"; - if (_stainService.StainCombo.Draw(label, dyeColor, string.Empty, true, gloss)) - tab.UpdateColorTablePreview(); - return false; - } - - private static unsafe bool ColorTablePasteAllClipboardButton(MtrlTab tab, bool disabled) - { - if (!ImGuiUtil.DrawDisabledButton("Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2(200, 0), string.Empty, disabled) - || !tab.Mtrl.HasTable) - return false; - - try - { - var text = ImGui.GetClipboardText(); - var data = Convert.FromBase64String(text); - if (data.Length < Marshal.SizeOf()) - return false; - - ref var rows = ref tab.Mtrl.Table; - fixed (void* ptr = data, output = &rows) - { - MemoryUtility.MemCpyUnchecked(output, ptr, Marshal.SizeOf()); - if (data.Length >= Marshal.SizeOf() + Marshal.SizeOf() - && tab.Mtrl.HasDyeTable) - { - ref var dyeRows = ref tab.Mtrl.DyeTable; - fixed (void* output2 = &dyeRows) - { - MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf(), - Marshal.SizeOf()); - } - } - } - - tab.UpdateColorTablePreview(); - - return true; - } - catch - { - return false; - } - } - - [SkipLocalsInit] - private static unsafe void ColorTableCopyClipboardButton(ColorTableRow row, ColorDyeTableRow dye) - { - if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Export this row to your clipboard.", false, true)) - return; - - try - { - Span data = stackalloc byte[ColorTableRow.Size + ColorDyeTableRow.Size]; - fixed (byte* ptr = data) - { - MemoryUtility.MemCpyUnchecked(ptr, &row, ColorTableRow.Size); - MemoryUtility.MemCpyUnchecked(ptr + ColorTableRow.Size, &dye, ColorDyeTableRow.Size); - } - - var text = Convert.ToBase64String(data); - ImGui.SetClipboardText(text); - } - catch - { - // ignored - } - } - - private static bool ColorTableDyeableCheckbox(MtrlTab tab) - { - var dyeable = tab.Mtrl.HasDyeTable; - var ret = ImGui.Checkbox("Dyeable", ref dyeable); - - if (ret) - { - tab.Mtrl.HasDyeTable = dyeable; - tab.UpdateColorTablePreview(); - } - - return ret; - } - - private static unsafe bool ColorTablePasteFromClipboardButton(MtrlTab tab, 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)) - return false; - - try - { - var text = ImGui.GetClipboardText(); - var data = Convert.FromBase64String(text); - if (data.Length != ColorTableRow.Size + ColorDyeTableRow.Size - || !tab.Mtrl.HasTable) - return false; - - fixed (byte* ptr = data) - { - tab.Mtrl.Table[rowIdx] = *(ColorTableRow*)ptr; - if (tab.Mtrl.HasDyeTable) - tab.Mtrl.DyeTable[rowIdx] = *(ColorDyeTableRow*)(ptr + ColorTableRow.Size); - } - - tab.UpdateColorTableRowPreview(rowIdx); - - return true; - } - catch - { - return false; - } - } - - private static void ColorTableHighlightButton(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.ColorTablePreviewers.Count == 0, true); - - if (ImGui.IsItemHovered()) - tab.HighlightColorTableRow(rowIdx); - else if (tab.HighlightedColorTableRow == rowIdx) - tab.CancelColorTableHighlight(); - } - - private bool DrawColorTableRow(MtrlTab tab, int rowIdx, bool disabled) - { - static bool FixFloat(ref float val, float current) - { - val = (float)(Half)val; - return val != current; - } - - using var id = ImRaii.PushId(rowIdx); - ref var row = ref tab.Mtrl.Table[rowIdx]; - var hasDye = tab.Mtrl.HasDyeTable; - ref var dye = ref tab.Mtrl.DyeTable[rowIdx]; - var floatSize = 70 * UiHelpers.Scale; - var intSize = 45 * UiHelpers.Scale; - ImGui.TableNextColumn(); - ColorTableCopyClipboardButton(row, dye); - ImGui.SameLine(); - var ret = ColorTablePasteFromClipboardButton(tab, rowIdx, disabled); - ImGui.SameLine(); - ColorTableHighlightButton(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 => - { - tab.Mtrl.Table[rowIdx].Diffuse = c; - tab.UpdateColorTableRowPreview(rowIdx); - }); - if (hasDye) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse, - b => - { - tab.Mtrl.DyeTable[rowIdx].Diffuse = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - } - - ImGui.TableNextColumn(); - ret |= ColorPicker("##Specular", "Specular Color", row.Specular, c => - { - tab.Mtrl.Table[rowIdx].Specular = c; - tab.UpdateColorTableRowPreview(rowIdx); - }); - ImGui.SameLine(); - var tmpFloat = row.SpecularStrength; - ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##SpecularStrength", ref tmpFloat, 0.01f, 0f, HalfMaxValue, "%.2f") - && FixFloat(ref tmpFloat, row.SpecularStrength)) - { - row.SpecularStrength = tmpFloat; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled); - - if (hasDye) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeSpecular", "Apply Specular Color on Dye", dye.Specular, - b => - { - tab.Mtrl.DyeTable[rowIdx].Specular = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength, - b => - { - tab.Mtrl.DyeTable[rowIdx].SpecularStrength = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - } - - ImGui.TableNextColumn(); - ret |= ColorPicker("##Emissive", "Emissive Color", row.Emissive, c => - { - tab.Mtrl.Table[rowIdx].Emissive = c; - tab.UpdateColorTableRowPreview(rowIdx); - }); - if (hasDye) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive, - b => - { - tab.Mtrl.DyeTable[rowIdx].Emissive = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - } - - ImGui.TableNextColumn(); - tmpFloat = row.GlossStrength; - ImGui.SetNextItemWidth(floatSize); - var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon; - if (ImGui.DragFloat("##GlossStrength", ref tmpFloat, Math.Max(0.1f, tmpFloat * 0.025f), glossStrengthMin, HalfMaxValue, "%.1f") - && FixFloat(ref tmpFloat, row.GlossStrength)) - { - row.GlossStrength = Math.Max(tmpFloat, glossStrengthMin); - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled); - if (hasDye) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss, - b => - { - tab.Mtrl.DyeTable[rowIdx].Gloss = b; - tab.UpdateColorTableRowPreview(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) - { - row.TileSet = (ushort)Math.Clamp(tmpInt, 0, 63); - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - 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)) - { - row.MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - 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)) - { - row.MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - 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)) - { - row.MaterialSkew = row.MaterialSkew with { X = tmpFloat }; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - 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)) - { - row.MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Skew Y", ImGuiHoveredFlags.AllowWhenDisabled); - - if (hasDye) - { - ImGui.TableNextColumn(); - if (_stainService.TemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize - + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) - { - dye.Template = _stainService.TemplateCombo.CurrentSelection; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Dye Template", ImGuiHoveredFlags.AllowWhenDisabled); - - ImGui.TableNextColumn(); - ret |= DrawDyePreview(tab, rowIdx, disabled, dye, floatSize); - } - - - return ret; - } - - private bool DrawDyePreview(MtrlTab tab, int rowIdx, bool disabled, ColorDyeTableRow dye, float floatSize) - { - var stain = _stainService.StainCombo.CurrentSelection.Key; - 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 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, rowIdx, stain, 0); - if (ret) - tab.UpdateColorTableRowPreview(rowIdx); - - ImGui.SameLine(); - ColorPicker("##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D"); - ImGui.SameLine(); - ColorPicker("##specularPreview", string.Empty, values.Specular, _ => { }, "S"); - ImGui.SameLine(); - 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.SameLine(); - 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 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)); - ret = true; - } - - if (letter.Length > 0 && ImGui.IsItemVisible()) - { - 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); - } - - ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled); - - 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); - - internal 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); -} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs deleted file mode 100644 index b95eca9d..00000000 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ /dev/null @@ -1,783 +0,0 @@ -using Dalamud.Interface; -using Dalamud.Interface.ImGuiNotification; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using ImGuiNET; -using Newtonsoft.Json.Linq; -using OtterGui; -using OtterGui.Classes; -using OtterGui.Raii; -using Penumbra.GameData.Data; -using Penumbra.GameData.Files; -using Penumbra.GameData.Files.MaterialStructs; -using Penumbra.GameData.Structs; -using Penumbra.Interop.Hooks.Objects; -using Penumbra.Interop.MaterialPreview; -using Penumbra.String; -using Penumbra.String.Classes; -using Penumbra.UI.Classes; -using static Penumbra.GameData.Files.ShpkFile; - -namespace Penumbra.UI.AdvancedWindow; - -public partial class ModEditWindow -{ - private sealed class MtrlTab : IWritable, IDisposable - { - private const int ShpkPrefixLength = 16; - - private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true); - - private readonly ModEditWindow _edit; - public readonly MtrlFile Mtrl; - public readonly string FilePath; - public readonly bool Writable; - - 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 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 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 HashSet UnfoldedTextures = new(4); - public readonly HashSet SamplerIds = new(16); - public float TextureLabelWidth; - - // Material Constants - 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); - public readonly List ColorTablePreviewers = new(4); - public int HighlightedColorTableRow = -1; - public readonly Stopwatch HighlightTime = new(); - - public FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath) - { - defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name); - if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath)) - return FullPath.Empty; - - 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; - var data = LoadedShpkPath.IsRooted - ? File.ReadAllBytes(LoadedShpkPath.FullName) - : _edit._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) - { - LoadedShpkPath = FullPath.Empty; - LoadedShpkPathName = string.Empty; - AssociatedShpk = null; - Penumbra.Messager.NotificationMessage(e, $"Could not load {LoadedShpkPath.ToPath()}.", NotificationType.Error, false); - } - - if (LoadedShpkPath.InternalName.IsEmpty) - { - AssociatedShpkDevkit = null; - LoadedShpkDevkitPathName = string.Empty; - } - else - { - AssociatedShpkDevkit = - TryLoadShpkDevkit(Path.GetFileNameWithoutExtension(Mtrl.ShaderPackage.Name), out LoadedShpkDevkitPathName); - } - - UpdateShaderKeys(); - Update(); - } - - private JObject? TryLoadShpkDevkit(string shpkBaseName, out string devkitPathName) - { - try - { - 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; - } - } - - private T? TryGetShpkDevkitData(string category, uint? id, bool mayVary) where T : class - => 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 - { - if (devkit == null) - return null; - - try - { - 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]; - } - - return data?.ToObject(typeof(T)) as T; - } - catch (Exception e) - { - // 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; - } - } - - private void UpdateShaderKeys() - { - ShaderKeys.Clear(); - if (AssociatedShpk != null) - foreach (var key in AssociatedShpk.MaterialKeys) - { - 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); - - 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 string.Compare(x.Label, y.Label, StringComparison.Ordinal); - }); - 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)>())); - } - - 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)); - foreach (var systemKeySelector in systemKeySelectors) - { - foreach (var sceneKeySelector in sceneKeySelectors) - { - foreach (var subViewKeySelector in subViewKeySelectors) - { - 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; - } - } - } - } - - 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; - } - - private void UpdateTextures() - { - Textures.Clear(); - SamplerIds.Clear(); - if (AssociatedShpk == null) - { - SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); - if (Mtrl.HasTable) - SamplerIds.Add(TableSamplerId); - - foreach (var (sampler, index) in Mtrl.ShaderPackage.Samplers.WithIndex()) - Textures.Add(($"0x{sampler.SamplerId:X8}", sampler.TextureIndex, index, string.Empty, true)); - } - else - { - 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) - { - SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); - if (Mtrl.HasTable) - SamplerIds.Add(TableSamplerId); - } - - foreach (var samplerId in SamplerIds) - { - var shpkSampler = AssociatedShpk.GetSamplerById(samplerId); - if (shpkSampler is not { Slot: 2 }) - continue; - - 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)); - } - - if (SamplerIds.Contains(TableSamplerId)) - Mtrl.HasTable = true; - } - - 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 = TextureLabelWidth / UiHelpers.Scale + 4; - } - - 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)); - return newGroup; - } - - Constants.Clear(); - if (AssociatedShpk == null) - { - var fcGroup = FindOrAddGroup(Constants, "Further Constants"); - foreach (var (constant, index) in Mtrl.ShaderPackage.Constants.WithIndex()) - { - 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) - 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); - } - - var fcGroup = FindOrAddGroup(Constants, "Further Constants"); - foreach (var (start, end) in handledElements.Ranges(complement: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() - { - UnbindFromMaterialInstances(); - - var instances = MaterialInfo.FindMaterials(_edit._resourceTreeFactory.GetLocalPlayerRelatedCharacters().Select(ch => ch.Address), - FilePath); - - var foundMaterials = new HashSet(); - foreach (var materialInfo in instances) - { - var material = materialInfo.GetDrawObjectMaterial(_edit._objects); - if (foundMaterials.Contains((nint)material)) - continue; - - try - { - MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._objects, materialInfo)); - foundMaterials.Add((nint)material); - } - catch (InvalidOperationException) - { - // Carry on without that previewer. - } - } - - UpdateMaterialPreview(); - - if (!Mtrl.HasTable) - return; - - foreach (var materialInfo in instances) - { - try - { - ColorTablePreviewers.Add(new LiveColorTablePreviewer(_edit._objects, _edit._framework, materialInfo)); - } - catch (InvalidOperationException) - { - // Carry on without that previewer. - } - } - - UpdateColorTablePreview(); - } - - private void UnbindFromMaterialInstances() - { - foreach (var previewer in MaterialPreviewers) - previewer.Dispose(); - MaterialPreviewers.Clear(); - - foreach (var previewer in ColorTablePreviewers) - previewer.Dispose(); - ColorTablePreviewers.Clear(); - } - - private unsafe void UnbindFromDrawObjectMaterialInstances(CharacterBase* characterBase) - { - for (var i = MaterialPreviewers.Count; i-- > 0;) - { - var previewer = MaterialPreviewers[i]; - if (previewer.DrawObject != characterBase) - continue; - - previewer.Dispose(); - MaterialPreviewers.RemoveAt(i); - } - - for (var i = ColorTablePreviewers.Count; i-- > 0;) - { - var previewer = ColorTablePreviewers[i]; - if (previewer.DrawObject != characterBase) - continue; - - previewer.Dispose(); - ColorTablePreviewers.RemoveAt(i); - } - } - - 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); - } - - private 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 HighlightColorTableRow(int rowIdx) - { - var oldRowIdx = HighlightedColorTableRow; - - if (HighlightedColorTableRow != rowIdx) - { - HighlightedColorTableRow = rowIdx; - HighlightTime.Restart(); - } - - if (oldRowIdx >= 0) - UpdateColorTableRowPreview(oldRowIdx); - if (rowIdx >= 0) - UpdateColorTableRowPreview(rowIdx); - } - - public void CancelColorTableHighlight() - { - var rowIdx = HighlightedColorTableRow; - - HighlightedColorTableRow = -1; - HighlightTime.Reset(); - - if (rowIdx >= 0) - UpdateColorTableRowPreview(rowIdx); - } - - public void UpdateColorTableRowPreview(int rowIdx) - { - if (ColorTablePreviewers.Count == 0) - return; - - if (!Mtrl.HasTable) - return; - - var row = new LegacyColorTableRow(Mtrl.Table[rowIdx]); - if (Mtrl.HasDyeTable) - { - var stm = _edit._stainService.StmFile; - var dye = new LegacyColorDyeTableRow(Mtrl.DyeTable[rowIdx]); - if (stm.TryGetValue(dye.Template, _edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) - row.ApplyDyeTemplate(dye, dyes); - } - - if (HighlightedColorTableRow == rowIdx) - ApplyHighlight(ref row, (float)HighlightTime.Elapsed.TotalSeconds); - - foreach (var previewer in ColorTablePreviewers) - { - row.AsHalves().CopyTo(previewer.ColorTable.AsSpan() - .Slice(LiveColorTablePreviewer.TextureWidth * 4 * rowIdx, LiveColorTablePreviewer.TextureWidth * 4)); - previewer.ScheduleUpdate(); - } - } - - public void UpdateColorTablePreview() - { - if (ColorTablePreviewers.Count == 0) - return; - - if (!Mtrl.HasTable) - return; - - var rows = new LegacyColorTable(Mtrl.Table); - var dyeRows = new LegacyColorDyeTable(Mtrl.DyeTable); - if (Mtrl.HasDyeTable) - { - var stm = _edit._stainService.StmFile; - var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; - for (var i = 0; i < LegacyColorTable.NumUsedRows; ++i) - { - ref var row = ref rows[i]; - var dye = dyeRows[i]; - if (stm.TryGetValue(dye.Template, stainId, out var dyes)) - row.ApplyDyeTemplate(dye, dyes); - } - } - - if (HighlightedColorTableRow >= 0) - ApplyHighlight(ref rows[HighlightedColorTableRow], (float)HighlightTime.Elapsed.TotalSeconds); - - foreach (var previewer in ColorTablePreviewers) - { - // TODO: Dawntrail - rows.AsHalves().CopyTo(previewer.ColorTable); - previewer.ScheduleUpdate(); - } - } - - private static void ApplyHighlight(ref LegacyColorTableRow row, float time) - { - var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; - var baseColor = ColorId.InGameHighlight.Value(); - var color = level * new Vector3(baseColor & 0xFF, (baseColor >> 8) & 0xFF, (baseColor >> 16) & 0xFF); - - row.Diffuse = Vector3.Zero; - row.Specular = Vector3.Zero; - row.Emissive = color * color; - } - - public void Update() - { - UpdateShaders(); - UpdateTextures(); - UpdateConstants(); - } - - public unsafe MtrlTab(ModEditWindow edit, MtrlFile file, string filePath, bool writable) - { - _edit = edit; - Mtrl = file; - FilePath = filePath; - Writable = writable; - AssociatedBaseDevkit = TryLoadShpkDevkit("_base", out LoadedBaseDevkitPathName); - LoadShpk(FindAssociatedShpk(out _, out _)); - if (writable) - { - _edit._characterBaseDestructor.Subscribe(UnbindFromDrawObjectMaterialInstances, CharacterBaseDestructor.Priority.MtrlTab); - BindToMaterialInstances(); - } - } - - public unsafe void Dispose() - { - UnbindFromMaterialInstances(); - if (Writable) - _edit._characterBaseDestructor.Unsubscribe(UnbindFromDrawObjectMaterialInstances); - } - - // TODO Readd ShadersKnown - public bool Valid - => (true || ShadersKnown) && Mtrl.Valid; - - public byte[] Write() - { - var output = Mtrl.Clone(); - output.GarbageCollect(AssociatedShpk, SamplerIds); - - 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; - } - - 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() - => Type switch - { - 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; - } - } -} From f8b034c42d14c038ed79183a37bee8262b9c8a7b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 4 Aug 2024 22:48:15 +0200 Subject: [PATCH 12/12] Auto-formatting, generous application of ImUtf8, minor cleanups. --- .../Materials/MtrlTab.ColorTable.cs | 246 +++++++++++------- .../Materials/MtrlTab.CommonColorTable.cs | 142 ++++++---- .../Materials/MtrlTab.Constants.cs | 29 ++- .../Materials/MtrlTab.Devkit.cs | 50 ++-- .../Materials/MtrlTab.LegacyColorTable.cs | 6 +- .../Materials/MtrlTab.LivePreview.cs | 129 ++++----- .../Materials/MtrlTab.ShaderPackage.cs | 218 ++++++++-------- .../Materials/MtrlTab.Textures.cs | 29 ++- .../UI/AdvancedWindow/Materials/MtrlTab.cs | 40 +-- .../Materials/MtrlTabFactory.cs | 13 +- .../AdvancedWindow/ModEditWindow.Materials.cs | 21 +- .../ModEditWindow.ShaderPackages.cs | 179 +++++++------ .../AdvancedWindow/ModEditWindow.ShpkTab.cs | 62 ++--- 13 files changed, 647 insertions(+), 517 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs index dc87ec41..352681bb 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs @@ -13,7 +13,7 @@ public partial class MtrlTab { private const float ColorTableScalarSize = 65.0f; - private int _colorTableSelectedPair = 0; + private int _colorTableSelectedPair; private bool DrawColorTable(ColorTable table, ColorDyeTable? dyeTable, bool disabled) { @@ -23,25 +23,27 @@ public partial class MtrlTab private void DrawColorTablePairSelector(ColorTable table, bool disabled) { + var style = ImGui.GetStyle(); + var itemSpacing = style.ItemSpacing.X; + var itemInnerSpacing = style.ItemInnerSpacing.X; + var framePadding = style.FramePadding; + var buttonWidth = (ImGui.GetContentRegionAvail().X - itemSpacing * 7.0f) * 0.125f; + var frameHeight = ImGui.GetFrameHeight(); + var highlighterSize = ImUtf8.CalcIconSize(FontAwesomeIcon.Crosshairs) + framePadding * 2.0f; + var spaceWidth = ImUtf8.CalcTextSize(" "u8).X; + var spacePadding = (int)MathF.Ceiling((highlighterSize.X + framePadding.X + itemInnerSpacing) / spaceWidth); + using var font = ImRaii.PushFont(UiBuilder.MonoFont); using var alignment = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); - var style = ImGui.GetStyle(); - var itemSpacing = style.ItemSpacing.X; - var itemInnerSpacing = style.ItemInnerSpacing.X; - var framePadding = style.FramePadding; - var buttonWidth = (ImGui.GetContentRegionAvail().X - itemSpacing * 7.0f) * 0.125f; - var frameHeight = ImGui.GetFrameHeight(); - var highlighterSize = ImUtf8.CalcIconSize(FontAwesomeIcon.Crosshairs) + framePadding * 2.0f; - var spaceWidth = ImUtf8.CalcTextSize(" "u8).X; - var spacePadding = (int)MathF.Ceiling((highlighterSize.X + framePadding.X + itemInnerSpacing) / spaceWidth); for (var i = 0; i < ColorTable.NumRows >> 1; i += 8) { for (var j = 0; j < 8; ++j) { var pairIndex = i + j; - using (var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.ButtonActive), pairIndex == _colorTableSelectedPair)) + using (ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.ButtonActive), pairIndex == _colorTableSelectedPair)) { - if (ImUtf8.Button($"#{pairIndex + 1}".PadLeft(3 + spacePadding), new Vector2(buttonWidth, ImGui.GetFrameHeightWithSpacing() + frameHeight))) + if (ImUtf8.Button($"#{pairIndex + 1}".PadLeft(3 + spacePadding), + new Vector2(buttonWidth, ImGui.GetFrameHeightWithSpacing() + frameHeight))) _colorTableSelectedPair = pairIndex; } @@ -79,12 +81,10 @@ public partial class MtrlTab private bool DrawColorTablePairEditor(ColorTable table, ColorDyeTable? dyeTable, bool disabled) { - var retA = false; - var retB = false; - ref var rowA = ref table[_colorTableSelectedPair << 1]; - ref var rowB = ref table[(_colorTableSelectedPair << 1) | 1]; - var dyeA = dyeTable != null ? dyeTable[_colorTableSelectedPair << 1] : default; - var dyeB = dyeTable != null ? dyeTable[(_colorTableSelectedPair << 1) | 1] : default; + var retA = false; + var retB = false; + var dyeA = dyeTable?[_colorTableSelectedPair << 1] ?? default; + var dyeB = dyeTable?[(_colorTableSelectedPair << 1) | 1] ?? default; var previewDyeA = _stainService.GetStainCombo(dyeA.Channel).CurrentSelection.Key; var previewDyeB = _stainService.GetStainCombo(dyeB.Channel).CurrentSelection.Key; var dyePackA = _stainService.GudStmFile.GetValueOrNull(dyeA.Template, previewDyeA); @@ -108,68 +108,96 @@ public partial class MtrlTab using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) { using var dis = ImRaii.Disabled(disabled); - using (var id = ImUtf8.PushId("ColorsA"u8)) + using (ImUtf8.PushId("ColorsA"u8)) + { retA |= DrawColors(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + } + columns.Next(); - using (var id = ImUtf8.PushId("ColorsB"u8)) + using (ImUtf8.PushId("ColorsB"u8)) + { retB |= DrawColors(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } } DrawHeader(" Physical Parameters"u8); using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) { using var dis = ImRaii.Disabled(disabled); - using (var id = ImUtf8.PushId("PbrA"u8)) + using (ImUtf8.PushId("PbrA"u8)) + { retA |= DrawPbr(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + } + columns.Next(); - using (var id = ImUtf8.PushId("PbrB"u8)) + using (ImUtf8.PushId("PbrB"u8)) + { retB |= DrawPbr(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } } DrawHeader(" Sheen Layer Parameters"u8); using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) { using var dis = ImRaii.Disabled(disabled); - using (var id = ImUtf8.PushId("SheenA"u8)) + using (ImUtf8.PushId("SheenA"u8)) + { retA |= DrawSheen(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + } + columns.Next(); - using (var id = ImUtf8.PushId("SheenB"u8)) + using (ImUtf8.PushId("SheenB"u8)) + { retB |= DrawSheen(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } } DrawHeader(" Pair Blending"u8); using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) { using var dis = ImRaii.Disabled(disabled); - using (var id = ImUtf8.PushId("BlendingA"u8)) + using (ImUtf8.PushId("BlendingA"u8)) + { retA |= DrawBlending(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + } + columns.Next(); - using (var id = ImUtf8.PushId("BlendingB"u8)) + using (ImUtf8.PushId("BlendingB"u8)) + { retB |= DrawBlending(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } } DrawHeader(" Material Template"u8); using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) { using var dis = ImRaii.Disabled(disabled); - using (var id = ImUtf8.PushId("TemplateA"u8)) + using (ImUtf8.PushId("TemplateA"u8)) + { retA |= DrawTemplate(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + } + columns.Next(); - using (var id = ImUtf8.PushId("TemplateB"u8)) + using (ImUtf8.PushId("TemplateB"u8)) + { retB |= DrawTemplate(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } } if (dyeTable != null) { DrawHeader(" Dye Properties"u8); - using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + using var columns = ImUtf8.Columns(2, "ColorTable"u8); + using var dis = ImRaii.Disabled(disabled); + using (ImUtf8.PushId("DyeA"u8)) { - using var dis = ImRaii.Disabled(disabled); - using (var id = ImUtf8.PushId("DyeA"u8)) - retA |= DrawDye(dyeTable, dyePackA, _colorTableSelectedPair << 1); - columns.Next(); - using (var id = ImUtf8.PushId("DyeB"u8)) - retB |= DrawDye(dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + retA |= DrawDye(dyeTable, dyePackA, _colorTableSelectedPair << 1); + } + + columns.Next(); + using (ImUtf8.PushId("DyeB"u8)) + { + retB |= DrawDye(dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); } } @@ -177,11 +205,16 @@ public partial class MtrlTab using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) { using var dis = ImRaii.Disabled(disabled); - using (var id = ImUtf8.PushId("FurtherA"u8)) + using (ImUtf8.PushId("FurtherA"u8)) + { retA |= DrawFurther(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + } + columns.Next(); - using (var id = ImUtf8.PushId("FurtherB"u8)) + using (ImUtf8.PushId("FurtherB"u8)) + { retB |= DrawFurther(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } } if (retA) @@ -195,18 +228,21 @@ public partial class MtrlTab /// Padding styles do not seem to apply to this component. It is recommended to prepend two spaces. private static void DrawHeader(ReadOnlySpan label) { - var headerColor = ImGui.GetColorU32(ImGuiCol.Header); - using var _ = ImRaii.PushColor(ImGuiCol.HeaderHovered, headerColor).Push(ImGuiCol.HeaderActive, headerColor); + var headerColor = ImGui.GetColorU32(ImGuiCol.Header); + using var _ = ImRaii.PushColor(ImGuiCol.HeaderHovered, headerColor).Push(ImGuiCol.HeaderActive, headerColor); ImUtf8.CollapsingHeader(label, ImGuiTreeNodeFlags.Leaf); } private static bool DrawColors(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) { - var dyeOffset = ImGui.GetContentRegionAvail().X + ImGui.GetStyle().ItemSpacing.X - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() * 2.0f; + var dyeOffset = ImGui.GetContentRegionAvail().X + + ImGui.GetStyle().ItemSpacing.X + - ImGui.GetStyle().ItemInnerSpacing.X + - ImGui.GetFrameHeight() * 2.0f; - var ret = false; + var ret = false; ref var row = ref table[rowIdx]; - var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var dye = dyeTable?[rowIdx] ?? default; ret |= CtColorPicker("Diffuse Color"u8, default, row.DiffuseColor, c => table[rowIdx].DiffuseColor = c); @@ -247,13 +283,17 @@ public partial class MtrlTab private static bool DrawBlending(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) { var scalarSize = ColorTableScalarSize * UiHelpers.Scale; - var dyeOffset = ImGui.GetContentRegionAvail().X + ImGui.GetStyle().ItemSpacing.X - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize; + var dyeOffset = ImGui.GetContentRegionAvail().X + + ImGui.GetStyle().ItemSpacing.X + - ImGui.GetStyle().ItemInnerSpacing.X + - ImGui.GetFrameHeight() + - scalarSize; var isRowB = (rowIdx & 1) != 0; - var ret = false; + var ret = false; ref var row = ref table[rowIdx]; - var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var dye = dyeTable?[rowIdx] ?? default; ImGui.SetNextItemWidth(scalarSize); ret |= CtDragHalf(isRowB ? "Field #19"u8 : "Anisotropy Degree"u8, default, row.Anisotropy, "%.2f"u8, 0.0f, HalfMaxValue, 0.1f, @@ -261,11 +301,13 @@ public partial class MtrlTab if (dyeTable != null) { ImGui.SameLine(dyeOffset); - ret |= CtApplyStainCheckbox("##dyeAnisotropy"u8, isRowB ? "Apply Field #19 on Dye"u8 : "Apply Anisotropy Degree on Dye"u8, dye.Anisotropy, + ret |= CtApplyStainCheckbox("##dyeAnisotropy"u8, isRowB ? "Apply Field #19 on Dye"u8 : "Apply Anisotropy Degree on Dye"u8, + dye.Anisotropy, b => dyeTable[rowIdx].Anisotropy = b); ImUtf8.SameLineInner(); ImGui.SetNextItemWidth(scalarSize); - CtDragHalf("##dyePreviewAnisotropy"u8, isRowB ? "Dye Preview for Field #19"u8 : "Dye Preview for Anisotropy Degree"u8, dyePack?.Anisotropy, "%.2f"u8); + CtDragHalf("##dyePreviewAnisotropy"u8, isRowB ? "Dye Preview for Field #19"u8 : "Dye Preview for Anisotropy Degree"u8, + dyePack?.Anisotropy, "%.2f"u8); } return ret; @@ -276,11 +318,11 @@ public partial class MtrlTab var scalarSize = ColorTableScalarSize * UiHelpers.Scale; var itemSpacing = ImGui.GetStyle().ItemSpacing.X; var dyeOffset = ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize - 64.0f; - var subcolWidth = CalculateSubcolumnWidth(2); + var subColWidth = CalculateSubColumnWidth(2); - var ret = false; + var ret = false; ref var row = ref table[rowIdx]; - var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var dye = dyeTable?[rowIdx] ?? default; ImGui.SetNextItemWidth(scalarSize); ret |= CtDragScalar("Shader ID"u8, default, row.ShaderId, "%d"u8, (ushort)0, (ushort)255, 0.25f, @@ -306,13 +348,15 @@ public partial class MtrlTab ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() with { Y = cursor.Y }); ImGui.SetNextItemWidth(scalarSize + itemSpacing + 64.0f); using var dis = ImRaii.Disabled(); - CtSphereMapIndexPicker("###SphereMapIndexDye"u8, "Dye Preview for Sphere Map"u8, dyePack?.SphereMapIndex ?? ushort.MaxValue, false, Nop); + CtSphereMapIndexPicker("###SphereMapIndexDye"u8, "Dye Preview for Sphere Map"u8, dyePack?.SphereMapIndex ?? ushort.MaxValue, false, + Nop); } ImGui.Dummy(new Vector2(64.0f, 0.0f)); ImGui.SameLine(); ImGui.SetNextItemWidth(scalarSize); - ret |= CtDragScalar("Sphere Map Intensity"u8, default, (float)row.SphereMapMask * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + ret |= CtDragScalar("Sphere Map Intensity"u8, default, (float)row.SphereMapMask * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, + HalfMaxValue * 100.0f, 1.0f, v => table[rowIdx].SphereMapMask = (Half)(v * 0.01f)); if (dyeTable != null) { @@ -326,10 +370,10 @@ public partial class MtrlTab ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - var leftLineHeight = 64.0f + ImGui.GetStyle().FramePadding.Y * 2.0f; + var leftLineHeight = 64.0f + ImGui.GetStyle().FramePadding.Y * 2.0f; var rightLineHeight = 3.0f * ImGui.GetFrameHeight() + 2.0f * ImGui.GetStyle().ItemSpacing.Y; - var lineHeight = Math.Max(leftLineHeight, rightLineHeight); - var cursorPos = ImGui.GetCursorScreenPos(); + var lineHeight = Math.Max(leftLineHeight, rightLineHeight); + var cursorPos = ImGui.GetCursorScreenPos(); ImGui.SetCursorScreenPos(cursorPos + new Vector2(0.0f, (lineHeight - leftLineHeight) * 0.5f)); ImGui.SetNextItemWidth(scalarSize + (itemSpacing + 64.0f) * 2.0f); ret |= CtTileIndexPicker("###TileIndex"u8, default, row.TileIndex, false, @@ -337,9 +381,10 @@ public partial class MtrlTab ImUtf8.SameLineInner(); ImUtf8.Text("Tile"u8); - ImGui.SameLine(subcolWidth); - ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() with { Y = cursorPos.Y + (lineHeight - rightLineHeight) * 0.5f, }); - using (var cld = ImUtf8.Child("###TileProperties"u8, new(ImGui.GetContentRegionAvail().X, float.Lerp(rightLineHeight, lineHeight, 0.5f)), false)) + ImGui.SameLine(subColWidth); + ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() with { Y = cursorPos.Y + (lineHeight - rightLineHeight) * 0.5f }); + using (ImUtf8.Child("###TileProperties"u8, + new Vector2(ImGui.GetContentRegionAvail().X, float.Lerp(rightLineHeight, lineHeight, 0.5f)))) { ImGui.Dummy(new Vector2(scalarSize, 0.0f)); ImUtf8.SameLineInner(); @@ -350,7 +395,8 @@ public partial class MtrlTab ret |= CtTileTransformMatrix(row.TileTransform, scalarSize, true, m => table[rowIdx].TileTransform = m); ImUtf8.SameLineInner(); - ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() - new Vector2(0.0f, (ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y) * 0.5f)); + ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() + - new Vector2(0.0f, (ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y) * 0.5f)); ImUtf8.Text("Tile Transform"u8); } @@ -360,15 +406,20 @@ public partial class MtrlTab private static bool DrawPbr(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) { var scalarSize = ColorTableScalarSize * UiHelpers.Scale; - var subcolWidth = CalculateSubcolumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; - var dyeOffset = subcolWidth - ImGui.GetStyle().ItemSpacing.X * 2.0f - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize; + var subColWidth = CalculateSubColumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; + var dyeOffset = subColWidth + - ImGui.GetStyle().ItemSpacing.X * 2.0f + - ImGui.GetStyle().ItemInnerSpacing.X + - ImGui.GetFrameHeight() + - scalarSize; - var ret = false; + var ret = false; ref var row = ref table[rowIdx]; - var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var dye = dyeTable?[rowIdx] ?? default; ImGui.SetNextItemWidth(scalarSize); - ret |= CtDragScalar("Roughness"u8, default, (float)row.Roughness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + ret |= CtDragScalar("Roughness"u8, default, (float)row.Roughness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, + 1.0f, v => table[rowIdx].Roughness = (Half)(v * 0.01f)); if (dyeTable != null) { @@ -380,13 +431,14 @@ public partial class MtrlTab CtDragScalar("##dyePreviewRoughness"u8, "Dye Preview for Roughness"u8, (float?)dyePack?.Roughness * 100.0f, "%.0f%%"u8); } - ImGui.SameLine(subcolWidth); + ImGui.SameLine(subColWidth); ImGui.SetNextItemWidth(scalarSize); - ret |= CtDragScalar("Metalness"u8, default, (float)row.Metalness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + ret |= CtDragScalar("Metalness"u8, default, (float)row.Metalness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, + 1.0f, v => table[rowIdx].Metalness = (Half)(v * 0.01f)); if (dyeTable != null) { - ImGui.SameLine(subcolWidth + dyeOffset); + ImGui.SameLine(subColWidth + dyeOffset); ret |= CtApplyStainCheckbox("##dyeMetalness"u8, "Apply Metalness on Dye"u8, dye.Metalness, b => dyeTable[rowIdx].Metalness = b); ImUtf8.SameLineInner(); @@ -400,12 +452,16 @@ public partial class MtrlTab private static bool DrawSheen(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) { var scalarSize = ColorTableScalarSize * UiHelpers.Scale; - var subcolWidth = CalculateSubcolumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; - var dyeOffset = subcolWidth - ImGui.GetStyle().ItemSpacing.X * 2.0f - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize; + var subColWidth = CalculateSubColumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; + var dyeOffset = subColWidth + - ImGui.GetStyle().ItemSpacing.X * 2.0f + - ImGui.GetStyle().ItemInnerSpacing.X + - ImGui.GetFrameHeight() + - scalarSize; - var ret = false; + var ret = false; ref var row = ref table[rowIdx]; - var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var dye = dyeTable?[rowIdx] ?? default; ImGui.SetNextItemWidth(scalarSize); ret |= CtDragScalar("Sheen"u8, default, (float)row.SheenRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, @@ -420,13 +476,14 @@ public partial class MtrlTab CtDragScalar("##dyePreviewSheenRate"u8, "Dye Preview for Sheen"u8, (float?)dyePack?.SheenRate * 100.0f, "%.0f%%"u8); } - ImGui.SameLine(subcolWidth); + ImGui.SameLine(subColWidth); ImGui.SetNextItemWidth(scalarSize); - ret |= CtDragScalar("Sheen Tint"u8, default, (float)row.SheenTintRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + ret |= CtDragScalar("Sheen Tint"u8, default, (float)row.SheenTintRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, + HalfMaxValue * 100.0f, 1.0f, v => table[rowIdx].SheenTintRate = (Half)(v * 0.01f)); if (dyeTable != null) { - ImGui.SameLine(subcolWidth + dyeOffset); + ImGui.SameLine(subColWidth + dyeOffset); ret |= CtApplyStainCheckbox("##dyeSheenTintRate"u8, "Apply Sheen Tint on Dye"u8, dye.SheenTintRate, b => dyeTable[rowIdx].SheenTintRate = b); ImUtf8.SameLineInner(); @@ -435,7 +492,8 @@ public partial class MtrlTab } ImGui.SetNextItemWidth(scalarSize); - ret |= CtDragScalar("Sheen Roughness"u8, default, 100.0f / (float)row.SheenAperture, "%.0f%%"u8, 100.0f / HalfMaxValue, 100.0f / HalfEpsilon, 1.0f, + ret |= CtDragScalar("Sheen Roughness"u8, default, 100.0f / (float)row.SheenAperture, "%.0f%%"u8, 100.0f / HalfMaxValue, + 100.0f / HalfEpsilon, 1.0f, v => table[rowIdx].SheenAperture = (Half)(100.0f / v)); if (dyeTable != null) { @@ -444,7 +502,8 @@ public partial class MtrlTab b => dyeTable[rowIdx].SheenAperture = b); ImUtf8.SameLineInner(); ImGui.SetNextItemWidth(scalarSize); - CtDragScalar("##dyePreviewSheenRoughness"u8, "Dye Preview for Sheen Roughness"u8, 100.0f / (float?)dyePack?.SheenAperture, "%.0f%%"u8); + CtDragScalar("##dyePreviewSheenRoughness"u8, "Dye Preview for Sheen Roughness"u8, 100.0f / (float?)dyePack?.SheenAperture, + "%.0f%%"u8); } return ret; @@ -453,12 +512,16 @@ public partial class MtrlTab private static bool DrawFurther(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) { var scalarSize = ColorTableScalarSize * UiHelpers.Scale; - var subcolWidth = CalculateSubcolumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; - var dyeOffset = subcolWidth - ImGui.GetStyle().ItemSpacing.X * 2.0f - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize; + var subColWidth = CalculateSubColumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; + var dyeOffset = subColWidth + - ImGui.GetStyle().ItemSpacing.X * 2.0f + - ImGui.GetStyle().ItemInnerSpacing.X + - ImGui.GetFrameHeight() + - scalarSize; - var ret = false; + var ret = false; ref var row = ref table[rowIdx]; - var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var dye = dyeTable?[rowIdx] ?? default; ImGui.SetNextItemWidth(scalarSize); ret |= CtDragHalf("Field #11"u8, default, row.Scalar11, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, @@ -479,7 +542,7 @@ public partial class MtrlTab ret |= CtDragHalf("Field #3"u8, default, row.Scalar3, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, v => table[rowIdx].Scalar3 = v); - ImGui.SameLine(subcolWidth); + ImGui.SameLine(subColWidth); ImGui.SetNextItemWidth(scalarSize); ret |= CtDragHalf("Field #7"u8, default, row.Scalar7, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, v => table[rowIdx].Scalar7 = v); @@ -488,7 +551,7 @@ public partial class MtrlTab ret |= CtDragHalf("Field #15"u8, default, row.Scalar15, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, v => table[rowIdx].Scalar15 = v); - ImGui.SameLine(subcolWidth); + ImGui.SameLine(subColWidth); ImGui.SetNextItemWidth(scalarSize); ret |= CtDragHalf("Field #17"u8, default, row.Scalar17, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, v => table[rowIdx].Scalar17 = v); @@ -497,7 +560,7 @@ public partial class MtrlTab ret |= CtDragHalf("Field #20"u8, default, row.Scalar20, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, v => table[rowIdx].Scalar20 = v); - ImGui.SameLine(subcolWidth); + ImGui.SameLine(subColWidth); ImGui.SetNextItemWidth(scalarSize); ret |= CtDragHalf("Field #22"u8, default, row.Scalar22, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, v => table[rowIdx].Scalar22 = v); @@ -513,33 +576,32 @@ public partial class MtrlTab { var scalarSize = ColorTableScalarSize * UiHelpers.Scale; var applyButtonWidth = ImUtf8.CalcTextSize("Apply Preview Dye"u8).X + ImGui.GetStyle().FramePadding.X * 2.0f; - var subcolWidth = CalculateSubcolumnWidth(2, applyButtonWidth); - - var ret = false; + var subColWidth = CalculateSubColumnWidth(2, applyButtonWidth); + + var ret = false; ref var dye = ref dyeTable[rowIdx]; ImGui.SetNextItemWidth(scalarSize); ret |= CtDragScalar("Dye Channel"u8, default, dye.Channel + 1, "%d"u8, 1, StainService.ChannelCount, 0.1f, value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1)); - ImGui.SameLine(subcolWidth); + ImGui.SameLine(subColWidth); ImGui.SetNextItemWidth(scalarSize); if (_stainService.GudTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, - scalarSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) + scalarSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) { dye.Template = _stainService.LegacyTemplateCombo.CurrentSelection; ret = true; } + ImUtf8.SameLineInner(); ImUtf8.Text("Dye Template"u8); ImGui.SameLine(ImGui.GetContentRegionAvail().X - applyButtonWidth + ImGui.GetStyle().ItemSpacing.X); using var dis = ImRaii.Disabled(!dyePack.HasValue); if (ImUtf8.Button("Apply Preview Dye"u8)) - { ret |= Mtrl.ApplyDyeToRow(_stainService.GudStmFile, [ _stainService.StainCombo1.CurrentSelection.Key, _stainService.StainCombo2.CurrentSelection.Key, ], rowIdx); - } return ret; } @@ -554,9 +616,9 @@ public partial class MtrlTab ImGui.TextUnformatted(text); } - private static float CalculateSubcolumnWidth(int numSubcolumns, float reservedSpace = 0.0f) + private static float CalculateSubColumnWidth(int numSubColumns, float reservedSpace = 0.0f) { var itemSpacing = ImGui.GetStyle().ItemSpacing.X; - return (ImGui.GetContentRegionAvail().X - reservedSpace - itemSpacing * (numSubcolumns - 1)) / numSubcolumns + itemSpacing; + return (ImGui.GetContentRegionAvail().X - reservedSpace - itemSpacing * (numSubColumns - 1)) / numSubColumns + itemSpacing; } } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs index 2b093e23..09c8ea61 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs @@ -12,9 +12,9 @@ namespace Penumbra.UI.AdvancedWindow.Materials; public partial class MtrlTab { - 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 static readonly float HalfMinValue = (float)Half.MinValue; + private static readonly float HalfMaxValue = (float)Half.MaxValue; + private static readonly float HalfEpsilon = (float)Half.Epsilon; private static readonly FontAwesomeCheckbox ApplyStainCheckbox = new(FontAwesomeIcon.FillDrip); @@ -22,11 +22,11 @@ public partial class MtrlTab private bool DrawColorTableSection(bool disabled) { - if ((!ShpkLoading && !SamplerIds.Contains(ShpkFile.TableSamplerId)) || Mtrl.Table == null) + if (!_shpkLoading && !SamplerIds.Contains(ShpkFile.TableSamplerId) || Mtrl.Table == null) return false; ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if (!ImGui.CollapsingHeader("Color Table", ImGuiTreeNodeFlags.DefaultOpen)) + if (!ImUtf8.CollapsingHeader("Color Table"u8, ImGuiTreeNodeFlags.DefaultOpen)) return false; ColorTableCopyAllClipboardButton(); @@ -35,7 +35,7 @@ public partial class MtrlTab if (!disabled) { ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImUtf8.IconDummy(); ImGui.SameLine(); ret |= ColorTableDyeableCheckbox(); } @@ -43,17 +43,18 @@ public partial class MtrlTab if (Mtrl.DyeTable != null) { ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImUtf8.IconDummy(); ImGui.SameLine(); ret |= DrawPreviewDye(disabled); } ret |= Mtrl.Table switch { - LegacyColorTable legacyTable => DrawLegacyColorTable(legacyTable, Mtrl.DyeTable as LegacyColorDyeTable, disabled), - ColorTable table when Mtrl.ShaderPackage.Name is "characterlegacy.shpk" => DrawLegacyColorTable(table, Mtrl.DyeTable as ColorDyeTable, disabled), - ColorTable table => DrawColorTable(table, Mtrl.DyeTable as ColorDyeTable, disabled), - _ => false, + LegacyColorTable legacyTable => DrawLegacyColorTable(legacyTable, Mtrl.DyeTable as LegacyColorDyeTable, disabled), + ColorTable table when Mtrl.ShaderPackage.Name is "characterlegacy.shpk" => DrawLegacyColorTable(table, + Mtrl.DyeTable as ColorDyeTable, disabled), + ColorTable table => DrawColorTable(table, Mtrl.DyeTable as ColorDyeTable, disabled), + _ => false, }; return ret; @@ -64,7 +65,7 @@ public partial class MtrlTab if (Mtrl.Table == null) return; - if (!ImGui.Button("Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2(200, 0))) + if (!ImUtf8.Button("Export All Rows to Clipboard"u8, ImGuiHelpers.ScaledVector2(200, 0))) return; try @@ -178,16 +179,18 @@ public partial class MtrlTab private bool ColorTableDyeableCheckbox() { var dyeable = Mtrl.DyeTable != null; - var ret = ImGui.Checkbox("Dyeable", ref dyeable); + var ret = ImUtf8.Checkbox("Dyeable"u8, ref dyeable); if (ret) { - Mtrl.DyeTable = dyeable ? Mtrl.Table switch - { - ColorTable => new ColorDyeTable(), - LegacyColorTable => new LegacyColorDyeTable(), - _ => null, - } : null; + Mtrl.DyeTable = dyeable + ? Mtrl.Table switch + { + ColorTable => new ColorDyeTable(), + LegacyColorTable => new LegacyColorDyeTable(), + _ => null, + } + : null; UpdateColorTablePreview(); } @@ -227,24 +230,27 @@ public partial class MtrlTab private void ColorTableHighlightButton(int pairIdx, bool disabled) { - ImUtf8.IconButton(FontAwesomeIcon.Crosshairs, "Highlight this pair of rows on your character, if possible.\n\nHighlight colors can be configured in Penumbra's settings."u8, - ImGui.GetFrameHeight() * Vector2.One, disabled || ColorTablePreviewers.Count == 0); + ImUtf8.IconButton(FontAwesomeIcon.Crosshairs, + "Highlight this pair of rows on your character, if possible.\n\nHighlight colors can be configured in Penumbra's settings."u8, + ImGui.GetFrameHeight() * Vector2.One, disabled || _colorTablePreviewers.Count == 0); if (ImGui.IsItemHovered()) HighlightColorTablePair(pairIdx); - else if (HighlightedColorTablePair == pairIdx) + else if (_highlightedColorTablePair == pairIdx) CancelColorTableHighlight(); } private static void CtBlendRect(Vector2 rcMin, Vector2 rcMax, uint topColor, uint bottomColor) { - var style = ImGui.GetStyle(); - var frameRounding = style.FrameRounding; + var style = ImGui.GetStyle(); + var frameRounding = style.FrameRounding; var frameThickness = style.FrameBorderSize; - var borderColor = ImGui.GetColorU32(ImGuiCol.Border); - var drawList = ImGui.GetWindowDrawList(); + var borderColor = ImGui.GetColorU32(ImGuiCol.Border); + var drawList = ImGui.GetWindowDrawList(); if (topColor == bottomColor) + { drawList.AddRectFilled(rcMin, rcMax, topColor, frameRounding, ImDrawFlags.RoundCornersDefault); + } else { drawList.AddRectFilled( @@ -258,10 +264,12 @@ public partial class MtrlTab rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 2.0f / 3) }, rcMax, bottomColor, frameRounding, ImDrawFlags.RoundCornersBottomLeft | ImDrawFlags.RoundCornersBottomRight); } + drawList.AddRect(rcMin, rcMax, borderColor, frameRounding, ImDrawFlags.RoundCornersDefault, frameThickness); } - private static bool CtColorPicker(ReadOnlySpan label, ReadOnlySpan description, HalfColor current, Action setter, ReadOnlySpan letter = default) + private static bool CtColorPicker(ReadOnlySpan label, ReadOnlySpan description, HalfColor current, Action setter, + ReadOnlySpan letter = default) { var ret = false; var inputSqrt = PseudoSqrtRgb((Vector3)current); @@ -291,10 +299,13 @@ public partial class MtrlTab return ret; } - private static void CtColorPicker(ReadOnlySpan label, ReadOnlySpan description, HalfColor? current, ReadOnlySpan letter = default) + private static void CtColorPicker(ReadOnlySpan label, ReadOnlySpan description, HalfColor? current, + ReadOnlySpan letter = default) { if (current.HasValue) + { CtColorPicker(label, description, current.Value, Nop, letter); + } else { var tmp = Vector4.Zero; @@ -308,8 +319,8 @@ public partial class MtrlTab if (letter.Length > 0 && ImGui.IsItemVisible()) { - var textSize = ImUtf8.CalcTextSize(letter); - var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2; + var textSize = ImUtf8.CalcTextSize(letter); + var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2; ImGui.GetWindowDrawList().AddText(letter, center, 0x80000000u); } @@ -319,7 +330,7 @@ public partial class MtrlTab private static bool CtApplyStainCheckbox(ReadOnlySpan label, ReadOnlySpan description, bool current, Action setter) { - var tmp = current; + var tmp = current; var result = ApplyStainCheckbox.Draw(label, ref tmp); ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); if (!result || tmp == current) @@ -329,68 +340,79 @@ public partial class MtrlTab return true; } - private static bool CtDragHalf(ReadOnlySpan label, ReadOnlySpan description, Half value, ReadOnlySpan format, float min, float max, float speed, Action setter) + private static bool CtDragHalf(ReadOnlySpan label, ReadOnlySpan description, Half value, ReadOnlySpan format, float min, + float max, float speed, Action setter) { - var tmp = (float)value; + var tmp = (float)value; var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed); ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); if (!result) return false; + var newValue = (Half)tmp; if (newValue == value) return false; + setter(newValue); return true; } - private static bool CtDragHalf(ReadOnlySpan label, ReadOnlySpan description, ref Half value, ReadOnlySpan format, float min, float max, float speed) + private static bool CtDragHalf(ReadOnlySpan label, ReadOnlySpan description, ref Half value, ReadOnlySpan format, + float min, float max, float speed) { - var tmp = (float)value; + var tmp = (float)value; var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed); ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); if (!result) return false; + var newValue = (Half)tmp; if (newValue == value) return false; + value = newValue; return true; } private static void CtDragHalf(ReadOnlySpan label, ReadOnlySpan description, Half? value, ReadOnlySpan format) { - using var _ = ImRaii.Disabled(); - var valueOrDefault = value ?? Half.Zero; - var floatValue = (float)valueOrDefault; + using var _ = ImRaii.Disabled(); + var valueOrDefault = value ?? Half.Zero; + var floatValue = (float)valueOrDefault; CtDragHalf(label, description, valueOrDefault, value.HasValue ? format : "-"u8, floatValue, floatValue, 0.0f, Nop); } - private static bool CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, T value, ReadOnlySpan format, T min, T max, float speed, Action setter) where T : unmanaged, INumber + private static bool CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, T value, ReadOnlySpan format, T min, + T max, float speed, Action setter) where T : unmanaged, INumber { - var tmp = value; + var tmp = value; var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed); ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); if (!result || tmp == value) return false; + setter(tmp); return true; } - private static bool CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, ref T value, ReadOnlySpan format, T min, T max, float speed) where T : unmanaged, INumber + private static bool CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, ref T value, ReadOnlySpan format, T min, + T max, float speed) where T : unmanaged, INumber { - var tmp = value; + var tmp = value; var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed); ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); if (!result || tmp == value) return false; + value = tmp; return true; } - private static void CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, T? value, ReadOnlySpan format) where T : unmanaged, INumber + private static void CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, T? value, ReadOnlySpan format) + where T : unmanaged, INumber { - using var _ = ImRaii.Disabled(); - var valueOrDefault = value ?? T.Zero; + using var _ = ImRaii.Disabled(); + var valueOrDefault = value ?? T.Zero; CtDragScalar(label, description, valueOrDefault, value.HasValue ? format : "-"u8, valueOrDefault, valueOrDefault, 0.0f, Nop); } @@ -398,14 +420,17 @@ public partial class MtrlTab { if (!_materialTemplatePickers.DrawTileIndexPicker(label, description, ref value, compact)) return false; + setter(value); return true; } - private bool CtSphereMapIndexPicker(ReadOnlySpan label, ReadOnlySpan description, ushort value, bool compact, Action setter) + private bool CtSphereMapIndexPicker(ReadOnlySpan label, ReadOnlySpan description, ushort value, bool compact, + Action setter) { if (!_materialTemplatePickers.DrawSphereMapIndexPicker(label, description, ref value, compact)) return false; + setter(value); return true; } @@ -430,33 +455,34 @@ public partial class MtrlTab ret |= CtDragHalf("##TileTransformVU"u8, "Tile Skew V"u8, ref tmp.VU, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); if (!ret || tmp == value) return false; + setter(tmp); } else { value.Decompose(out var scale, out var rotation, out var shear); rotation *= 180.0f / MathF.PI; - shear *= 180.0f / MathF.PI; + shear *= 180.0f / MathF.PI; ImGui.SetNextItemWidth(floatSize); var scaleXChanged = CtDragScalar("##TileScaleU"u8, "Tile Scale U"u8, ref scale.X, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); var activated = ImGui.IsItemActivated(); var deactivated = ImGui.IsItemDeactivated(); ImUtf8.SameLineInner(); ImGui.SetNextItemWidth(floatSize); - var scaleYChanged = CtDragScalar("##TileScaleV"u8, "Tile Scale V"u8, ref scale.Y, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); - activated |= ImGui.IsItemActivated(); - deactivated |= ImGui.IsItemDeactivated(); + var scaleYChanged = CtDragScalar("##TileScaleV"u8, "Tile Scale V"u8, ref scale.Y, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); + activated |= ImGui.IsItemActivated(); + deactivated |= ImGui.IsItemDeactivated(); if (!twoRowLayout) ImUtf8.SameLineInner(); ImGui.SetNextItemWidth(floatSize); - var rotationChanged = CtDragScalar("##TileRotation"u8, "Tile Rotation"u8, ref rotation, "%.0f°"u8, -180.0f, 180.0f, 1.0f); - activated |= ImGui.IsItemActivated(); - deactivated |= ImGui.IsItemDeactivated(); + var rotationChanged = CtDragScalar("##TileRotation"u8, "Tile Rotation"u8, ref rotation, "%.0f°"u8, -180.0f, 180.0f, 1.0f); + activated |= ImGui.IsItemActivated(); + deactivated |= ImGui.IsItemDeactivated(); ImUtf8.SameLineInner(); ImGui.SetNextItemWidth(floatSize); - var shearChanged = CtDragScalar("##TileShear"u8, "Tile Shear"u8, ref shear, "%.0f°"u8, -90.0f, 90.0f, 1.0f); - activated |= ImGui.IsItemActivated(); - deactivated |= ImGui.IsItemDeactivated(); + var shearChanged = CtDragScalar("##TileShear"u8, "Tile Shear"u8, ref shear, "%.0f°"u8, -90.0f, 90.0f, 1.0f); + activated |= ImGui.IsItemActivated(); + deactivated |= ImGui.IsItemDeactivated(); if (deactivated) _pinnedTileTransform = null; else if (activated) @@ -464,6 +490,7 @@ public partial class MtrlTab ret = scaleXChanged | scaleYChanged | rotationChanged | shearChanged; if (!ret) return false; + if (_pinnedTileTransform.HasValue) { var (pinScale, pinRotation, pinShear) = _pinnedTileTransform.Value; @@ -476,11 +503,14 @@ public partial class MtrlTab if (!shearChanged) shear = pinShear; } + var newValue = HalfMatrix2x2.Compose(scale, rotation * MathF.PI / 180.0f, shear * MathF.PI / 180.0f); if (newValue == value) return false; + setter(newValue); } + return true; } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs index 56496005..176ec3f4 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs @@ -35,7 +35,7 @@ public partial class MtrlTab Constants.Clear(); string mpPrefix; - if (AssociatedShpk == null) + if (_associatedShpk == null) { mpPrefix = MaterialParamsConstantName.Value!; var fcGroup = FindOrAddGroup(Constants, "Further Constants"); @@ -51,12 +51,12 @@ public partial class MtrlTab } else { - mpPrefix = AssociatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? MaterialParamsConstantName.Value!; + mpPrefix = _associatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? MaterialParamsConstantName.Value!; var autoNameMaxLength = Math.Max(Names.LongestKnownNameLength, mpPrefix.Length + 8); - foreach (var shpkConstant in AssociatedShpk.MaterialParams) + foreach (var shpkConstant in _associatedShpk.MaterialParams) { var name = Names.KnownNames.TryResolve(shpkConstant.Id); - var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, AssociatedShpk, out var constantIndex); + var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, _associatedShpk, out var constantIndex); var values = Mtrl.GetConstantValue(constant); var handledElements = new IndexSet(values.Length, false); @@ -64,8 +64,8 @@ public partial class MtrlTab if (dkData != null) foreach (var dkConstant in dkData) { - var offset = (int)dkConstant.EffectiveByteOffset; - var length = values.Length - offset; + var offset = (int)dkConstant.EffectiveByteOffset; + var length = values.Length - offset; var constantSize = dkConstant.EffectiveByteSize; if (constantSize.HasValue) length = Math.Min(length, (int)constantSize.Value); @@ -86,7 +86,6 @@ public partial class MtrlTab foreach (var (start, end) in handledElements.Ranges(complement: true)) { if (start == 0 && end == values.Length && end - start <= 16) - { if (name.Value != null) { fcGroup.Add(( @@ -94,7 +93,6 @@ public partial class MtrlTab constantIndex, 0..values.Length, string.Empty, true, DefaultConstantEditorFor(name))); continue; } - } if ((shpkConstant.ByteOffset & 0x3) == 0 && (shpkConstant.ByteSize & 0x3) == 0) { @@ -105,7 +103,8 @@ public partial class MtrlTab var rangeEnd = Math.Min(i + 16, end); if (rangeEnd > rangeStart) { - var autoName = $"{mpPrefix}[{j,2:D}]{VectorSwizzle(((offset + rangeStart) & 0xF) >> 2, ((offset + rangeEnd - 1) & 0xF) >> 2)}"; + var autoName = + $"{mpPrefix}[{j,2:D}]{VectorSwizzle(((offset + rangeStart) & 0xF) >> 2, ((offset + rangeEnd - 1) & 0xF) >> 2)}"; fcGroup.Add(( $"{autoName.PadRight(autoNameMaxLength)} (0x{shpkConstant.Id:X8})", constantIndex, rangeStart..rangeEnd, string.Empty, true, DefaultConstantEditorFor(name))); @@ -116,7 +115,8 @@ public partial class MtrlTab { for (var i = start; i < end; i += 16) { - fcGroup.Add(($"{"???".PadRight(autoNameMaxLength)} (0x{shpkConstant.Id:X8})", constantIndex, i..Math.Min(i + 16, end), string.Empty, true, + fcGroup.Add(($"{"???".PadRight(autoNameMaxLength)} (0x{shpkConstant.Id:X8})", constantIndex, + i..Math.Min(i + 16, end), string.Empty, true, DefaultConstantEditorFor(name))); } } @@ -178,15 +178,16 @@ public partial class MtrlTab ret = true; SetMaterialParameter(constant.Id, slice.Start, buffer[slice]); } - var shpkConstant = AssociatedShpk?.GetMaterialParamById(constant.Id); - var defaultConstantValue = shpkConstant.HasValue ? AssociatedShpk!.GetMaterialParamDefault(shpkConstant.Value) : []; + + var shpkConstant = _associatedShpk?.GetMaterialParamById(constant.Id); + var defaultConstantValue = shpkConstant.HasValue ? _associatedShpk!.GetMaterialParamDefault(shpkConstant.Value) : []; var defaultValue = IsValid(slice, defaultConstantValue.Length) ? defaultConstantValue[slice] : []; - var canReset = AssociatedShpk?.MaterialParamsDefaults != null + var canReset = _associatedShpk?.MaterialParamsDefaults != null ? defaultValue.Length > 0 && !defaultValue.SequenceEqual(buffer[slice]) : buffer[slice].ContainsAnyExcept((byte)0); ImUtf8.SameLineInner(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Backspace.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Reset this constant to its default value.\n\nHold Ctrl to unlock.", !ImGui.GetIO().KeyCtrl || !canReset, true)) + "Reset this constant to its default value.\n\nHold Ctrl to unlock.", !ImGui.GetIO().KeyCtrl || !canReset, true)) { ret = true; if (defaultValue.Length > 0) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs index cd62d58f..26fe3dcb 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs @@ -1,3 +1,4 @@ +using JetBrains.Annotations; using Newtonsoft.Json.Linq; using OtterGui.Text.Widget.Editors; using Penumbra.String.Classes; @@ -29,8 +30,8 @@ public partial class MtrlTab } private T? TryGetShpkDevkitData(string category, uint? id, bool mayVary) where T : class - => 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 { @@ -47,7 +48,7 @@ public partial class MtrlTab { var selector = BuildSelector(data!["Vary"]! .Select(key => (uint)key) - .Select(key => Mtrl.GetShaderKey(key)?.Value ?? AssociatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue)); + .Select(key => Mtrl.GetShaderKey(key)?.Value ?? _associatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue)); var index = (int)data["Selectors"]![selector.ToString()]!; data = data["Items"]![index]; } @@ -62,12 +63,14 @@ public partial class MtrlTab } } + [UsedImplicitly] private sealed class DevkitShaderKeyValue { public string Label = string.Empty; public string Description = string.Empty; } + [UsedImplicitly] private sealed class DevkitShaderKey { public string Label = string.Empty; @@ -75,6 +78,7 @@ public partial class MtrlTab public Dictionary Values = []; } + [UsedImplicitly] private sealed class DevkitSampler { public string Label = string.Empty; @@ -84,14 +88,16 @@ public partial class MtrlTab private enum DevkitConstantType { - Hidden = -1, - Float = 0, + Hidden = -1, + Float = 0, + /// Integer encoded as a float. - Integer = 1, - Color = 2, - Enum = 3, + Integer = 1, + Color = 2, + Enum = 3, + /// Native integer. - Int32 = 4, + Int32 = 4, Int32Enum = 5, Int8 = 6, Int8Enum = 7, @@ -105,6 +111,7 @@ public partial class MtrlTab SphereMapIndex = 15, } + [UsedImplicitly] private sealed class DevkitConstantValue { public string Label = string.Empty; @@ -112,6 +119,7 @@ public partial class MtrlTab public double Value = 0; } + [UsedImplicitly] private sealed class DevkitConstant { public uint Offset = 0; @@ -147,7 +155,7 @@ public partial class MtrlTab => ByteOffset ?? Offset * ValueSize; public uint? EffectiveByteSize - => ByteSize ?? (Length * ValueSize); + => ByteSize ?? Length * ValueSize; public unsafe uint ValueSize => Type switch @@ -198,19 +206,23 @@ public partial class MtrlTab private IEditor CreateIntegerEditor() where T : unmanaged, INumber => ((Drag || Slider) && !Hex - ? (Drag - ? (IEditor)DragEditor.CreateInteger(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed, Unit, 0) - : SliderEditor.CreateInteger(ToInteger(Minimum) ?? default, ToInteger(Maximum) ?? default, Unit, 0)) - : InputEditor.CreateInteger(ToInteger(Minimum), ToInteger(Maximum), ToInteger(Step), ToInteger(StepFast), Hex, Unit, 0)) + ? Drag + ? (IEditor)DragEditor.CreateInteger(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed, + Unit, 0) + : SliderEditor.CreateInteger(ToInteger(Minimum) ?? default, ToInteger(Maximum) ?? default, Unit, 0) + : InputEditor.CreateInteger(ToInteger(Minimum), ToInteger(Maximum), ToInteger(Step), ToInteger(StepFast), + Hex, Unit, 0)) .WithFactorAndBias(ToInteger(Factor), ToInteger(Bias)); private IEditor CreateFloatEditor() where T : unmanaged, INumber, IPowerFunctions - => ((Drag || Slider) - ? (Drag - ? (IEditor)DragEditor.CreateFloat(ToFloat(Minimum), ToFloat(Maximum), Speed ?? 0.1f, RelativeSpeed, Precision, Unit, 0) - : SliderEditor.CreateFloat(ToFloat(Minimum) ?? default, ToFloat(Maximum) ?? default, Precision, Unit, 0)) - : InputEditor.CreateFloat(ToFloat(Minimum), ToFloat(Maximum), T.CreateSaturating(Step), T.CreateSaturating(StepFast), Precision, Unit, 0)) + => (Drag || Slider + ? Drag + ? (IEditor)DragEditor.CreateFloat(ToFloat(Minimum), ToFloat(Maximum), Speed ?? 0.1f, RelativeSpeed, + Precision, Unit, 0) + : SliderEditor.CreateFloat(ToFloat(Minimum) ?? default, ToFloat(Maximum) ?? default, Precision, Unit, 0) + : InputEditor.CreateFloat(ToFloat(Minimum), ToFloat(Maximum), T.CreateSaturating(Step), + T.CreateSaturating(StepFast), Precision, Unit, 0)) .WithExponent(T.CreateSaturating(Exponent)) .WithFactorAndBias(T.CreateSaturating(Factor), T.CreateSaturating(Bias)); diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs index a2165760..0ff2b01f 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs @@ -110,9 +110,9 @@ public partial class MtrlTab } ImGui.TableNextColumn(); - using (ImRaii.PushFont(UiBuilder.MonoFont)) - { - ImUtf8.Text($"{(rowIdx >> 1) + 1,2:D}{"AB"[rowIdx & 1]}"); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + ImUtf8.Text($"{(rowIdx >> 1) + 1,2:D}{"AB"[rowIdx & 1]}"); } ImGui.TableNextColumn(); diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs index 3482e581..6089f2d5 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs @@ -12,20 +12,20 @@ namespace Penumbra.UI.AdvancedWindow.Materials; public partial class MtrlTab { - public readonly List MaterialPreviewers = new(4); - public readonly List ColorTablePreviewers = new(4); - public int HighlightedColorTablePair = -1; - public readonly Stopwatch HighlightTime = new(); + private readonly List _materialPreviewers = new(4); + private readonly List _colorTablePreviewers = new(4); + private int _highlightedColorTablePair = -1; + private readonly Stopwatch _highlightTime = new(); private void DrawMaterialLivePreviewRebind(bool disabled) { if (disabled) return; - if (ImGui.Button("Reload live preview")) + if (ImUtf8.Button("Reload live preview"u8)) BindToMaterialInstances(); - if (MaterialPreviewers.Count != 0 || ColorTablePreviewers.Count != 0) + if (_materialPreviewers.Count != 0 || _colorTablePreviewers.Count != 0) return; ImGui.SameLine(); @@ -34,7 +34,7 @@ public partial class MtrlTab "The current material has not been found on your character. Please check the Import from Screen tab for more information."u8); } - public unsafe void BindToMaterialInstances() + private unsafe void BindToMaterialInstances() { UnbindFromMaterialInstances(); @@ -50,7 +50,7 @@ public partial class MtrlTab try { - MaterialPreviewers.Add(new LiveMaterialPreviewer(_objects, materialInfo)); + _materialPreviewers.Add(new LiveMaterialPreviewer(_objects, materialInfo)); foundMaterials.Add((nint)material); } catch (InvalidOperationException) @@ -68,7 +68,7 @@ public partial class MtrlTab { try { - ColorTablePreviewers.Add(new LiveColorTablePreviewer(_objects, _framework, materialInfo)); + _colorTablePreviewers.Add(new LiveColorTablePreviewer(_objects, _framework, materialInfo)); } catch (InvalidOperationException) { @@ -81,53 +81,53 @@ public partial class MtrlTab private void UnbindFromMaterialInstances() { - foreach (var previewer in MaterialPreviewers) + foreach (var previewer in _materialPreviewers) previewer.Dispose(); - MaterialPreviewers.Clear(); + _materialPreviewers.Clear(); - foreach (var previewer in ColorTablePreviewers) + foreach (var previewer in _colorTablePreviewers) previewer.Dispose(); - ColorTablePreviewers.Clear(); + _colorTablePreviewers.Clear(); } private unsafe void UnbindFromDrawObjectMaterialInstances(CharacterBase* characterBase) { - for (var i = MaterialPreviewers.Count; i-- > 0;) + for (var i = _materialPreviewers.Count; i-- > 0;) { - var previewer = MaterialPreviewers[i]; + var previewer = _materialPreviewers[i]; if (previewer.DrawObject != characterBase) continue; previewer.Dispose(); - MaterialPreviewers.RemoveAt(i); + _materialPreviewers.RemoveAt(i); } - for (var i = ColorTablePreviewers.Count; i-- > 0;) + for (var i = _colorTablePreviewers.Count; i-- > 0;) { - var previewer = ColorTablePreviewers[i]; + var previewer = _colorTablePreviewers[i]; if (previewer.DrawObject != characterBase) continue; previewer.Dispose(); - ColorTablePreviewers.RemoveAt(i); + _colorTablePreviewers.RemoveAt(i); } } - public void SetShaderPackageFlags(uint shPkFlags) + private void SetShaderPackageFlags(uint shPkFlags) { - foreach (var previewer in MaterialPreviewers) + foreach (var previewer in _materialPreviewers) previewer.SetShaderPackageFlags(shPkFlags); } - public void SetMaterialParameter(uint parameterCrc, Index offset, Span value) + private void SetMaterialParameter(uint parameterCrc, Index offset, Span value) { - foreach (var previewer in MaterialPreviewers) + foreach (var previewer in _materialPreviewers) previewer.SetMaterialParameter(parameterCrc, offset, value); } - public void SetSamplerFlags(uint samplerCrc, uint samplerFlags) + private void SetSamplerFlags(uint samplerCrc, uint samplerFlags) { - foreach (var previewer in MaterialPreviewers) + foreach (var previewer in _materialPreviewers) previewer.SetSamplerFlags(samplerCrc, samplerFlags); } @@ -145,14 +145,14 @@ public partial class MtrlTab SetSamplerFlags(sampler.SamplerId, sampler.Flags); } - public void HighlightColorTablePair(int pairIdx) + private void HighlightColorTablePair(int pairIdx) { - var oldPairIdx = HighlightedColorTablePair; + var oldPairIdx = _highlightedColorTablePair; - if (HighlightedColorTablePair != pairIdx) + if (_highlightedColorTablePair != pairIdx) { - HighlightedColorTablePair = pairIdx; - HighlightTime.Restart(); + _highlightedColorTablePair = pairIdx; + _highlightTime.Restart(); } if (oldPairIdx >= 0) @@ -160,19 +160,6 @@ public partial class MtrlTab UpdateColorTableRowPreview(oldPairIdx << 1); UpdateColorTableRowPreview((oldPairIdx << 1) | 1); } - if (pairIdx >= 0) - { - UpdateColorTableRowPreview(pairIdx << 1); - UpdateColorTableRowPreview((pairIdx << 1) | 1); - } - } - - public void CancelColorTableHighlight() - { - var pairIdx = HighlightedColorTablePair; - - HighlightedColorTablePair = -1; - HighlightTime.Reset(); if (pairIdx >= 0) { @@ -181,9 +168,23 @@ public partial class MtrlTab } } - public void UpdateColorTableRowPreview(int rowIdx) + private void CancelColorTableHighlight() { - if (ColorTablePreviewers.Count == 0) + var pairIdx = _highlightedColorTablePair; + + _highlightedColorTablePair = -1; + _highlightTime.Reset(); + + if (pairIdx >= 0) + { + UpdateColorTableRowPreview(pairIdx << 1); + UpdateColorTableRowPreview((pairIdx << 1) | 1); + } + } + + private void UpdateColorTableRowPreview(int rowIdx) + { + if (_colorTablePreviewers.Count == 0) return; if (Mtrl.Table == null) @@ -192,7 +193,7 @@ public partial class MtrlTab var row = Mtrl.Table switch { LegacyColorTable legacyTable => new ColorTableRow(legacyTable[rowIdx]), - ColorTable table => table[rowIdx], + ColorTable table => table[rowIdx], _ => throw new InvalidOperationException($"Unsupported color table type {Mtrl.Table.GetType()}"), }; if (Mtrl.DyeTable != null) @@ -200,8 +201,8 @@ public partial class MtrlTab var dyeRow = Mtrl.DyeTable switch { LegacyColorDyeTable legacyDyeTable => new ColorDyeTableRow(legacyDyeTable[rowIdx]), - ColorDyeTable dyeTable => dyeTable[rowIdx], - _ => throw new InvalidOperationException($"Unsupported color dye table type {Mtrl.DyeTable.GetType()}"), + ColorDyeTable dyeTable => dyeTable[rowIdx], + _ => throw new InvalidOperationException($"Unsupported color dye table type {Mtrl.DyeTable.GetType()}"), }; if (dyeRow.Channel < StainService.ChannelCount) { @@ -213,21 +214,21 @@ public partial class MtrlTab } } - if (HighlightedColorTablePair << 1 == rowIdx) - ApplyHighlight(ref row, ColorId.InGameHighlight, (float)HighlightTime.Elapsed.TotalSeconds); - else if (((HighlightedColorTablePair << 1) | 1) == rowIdx) - ApplyHighlight(ref row, ColorId.InGameHighlight2, (float)HighlightTime.Elapsed.TotalSeconds); + if (_highlightedColorTablePair << 1 == rowIdx) + ApplyHighlight(ref row, ColorId.InGameHighlight, (float)_highlightTime.Elapsed.TotalSeconds); + else if (((_highlightedColorTablePair << 1) | 1) == rowIdx) + ApplyHighlight(ref row, ColorId.InGameHighlight2, (float)_highlightTime.Elapsed.TotalSeconds); - foreach (var previewer in ColorTablePreviewers) + foreach (var previewer in _colorTablePreviewers) { row[..].CopyTo(previewer.GetColorRow(rowIdx)); previewer.ScheduleUpdate(); } } - public void UpdateColorTablePreview() + private void UpdateColorTablePreview() { - if (ColorTablePreviewers.Count == 0) + if (_colorTablePreviewers.Count == 0) return; if (Mtrl.Table == null) @@ -237,7 +238,8 @@ public partial class MtrlTab var dyeRows = Mtrl.DyeTable != null ? ColorDyeTable.CastOrConvert(Mtrl.DyeTable) : null; if (dyeRows != null) { - ReadOnlySpan stainIds = [ + ReadOnlySpan stainIds = + [ _stainService.StainCombo1.CurrentSelection.Key, _stainService.StainCombo2.CurrentSelection.Key, ]; @@ -245,13 +247,14 @@ public partial class MtrlTab rows.ApplyDye(_stainService.GudStmFile, stainIds, dyeRows); } - if (HighlightedColorTablePair >= 0) + if (_highlightedColorTablePair >= 0) { - ApplyHighlight(ref rows[HighlightedColorTablePair << 1], ColorId.InGameHighlight, (float)HighlightTime.Elapsed.TotalSeconds); - ApplyHighlight(ref rows[(HighlightedColorTablePair << 1) | 1], ColorId.InGameHighlight2, (float)HighlightTime.Elapsed.TotalSeconds); + ApplyHighlight(ref rows[_highlightedColorTablePair << 1], ColorId.InGameHighlight, (float)_highlightTime.Elapsed.TotalSeconds); + ApplyHighlight(ref rows[(_highlightedColorTablePair << 1) | 1], ColorId.InGameHighlight2, + (float)_highlightTime.Elapsed.TotalSeconds); } - foreach (var previewer in ColorTablePreviewers) + foreach (var previewer in _colorTablePreviewers) { rows.AsHalves().CopyTo(previewer.ColorTable); previewer.ScheduleUpdate(); @@ -260,11 +263,11 @@ public partial class MtrlTab private static void ApplyHighlight(ref ColorTableRow row, ColorId colorId, float time) { - var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; - var baseColor = colorId.Value(); + var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; + var baseColor = colorId.Value(); var color = level * new Vector3(baseColor & 0xFF, (baseColor >> 8) & 0xFF, (baseColor >> 16) & 0xFF); var halfColor = (HalfColor)(color * color); - + row.DiffuseColor = halfColor; row.SpecularColor = halfColor; row.EmissiveColor = halfColor; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs index 21557939..ae57a122 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs @@ -21,8 +21,8 @@ public partial class MtrlTab // Apricot shader packages are unlisted because // 1. they cause severe performance/memory issues when calculating the effective shader set // 2. they probably aren't intended for use with materials anyway - internal static readonly IReadOnlyList StandardShaderPackages = new[] - { + private static readonly IReadOnlyList StandardShaderPackages = + [ "3dui.shpk", // "apricot_decal_dummy.shpk", // "apricot_decal_ring.shpk", @@ -80,35 +80,35 @@ public partial class MtrlTab "verticalfog.shpk", "water.shpk", "weather.shpk", - }; + ]; - private static readonly byte[] UnknownShadersString = Encoding.UTF8.GetBytes("Vertex Shaders: ???\nPixel Shaders: ???"); + private static readonly byte[] UnknownShadersString = "Vertex Shaders: ???\nPixel Shaders: ???"u8.ToArray(); 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 bool ShpkLoading; - public JObject? AssociatedShpkDevkit; + private string _shaderHeader = "Shader###Shader"; + private FullPath _loadedShpkPath = FullPath.Empty; + private string _loadedShpkPathName = string.Empty; + private string _loadedShpkDevkitPathName = string.Empty; + private string _shaderComment = string.Empty; + private ShpkFile? _associatedShpk; + private bool _shpkLoading; + private JObject? _associatedShpkDevkit; - public readonly string LoadedBaseDevkitPathName; - public readonly JObject? AssociatedBaseDevkit; + private readonly string _loadedBaseDevkitPathName; + private readonly JObject? _associatedBaseDevkit; // Shader Key State - public readonly + private readonly List<(string Label, int Index, string Description, bool MonoFont, IReadOnlyList<(string Label, uint Value, string Description)> - Values)> ShaderKeys = new(16); + Values)> _shaderKeys = new(16); - public readonly HashSet VertexShaders = new(16); - public readonly HashSet PixelShaders = new(16); - public bool ShadersKnown; - public ReadOnlyMemory ShadersString = UnknownShadersString; + private readonly HashSet _vertexShaders = new(16); + private readonly HashSet _pixelShaders = new(16); + private bool _shadersKnown; + private ReadOnlyMemory _shadersString = UnknownShadersString; - public string[] GetShpkNames() + private string[] GetShpkNames() { if (null != _shpkNames) return _shpkNames; @@ -122,7 +122,7 @@ public partial class MtrlTab return _shpkNames; } - public FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath) + private FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath) { defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name); if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath)) @@ -131,45 +131,45 @@ public partial class MtrlTab return _edit.FindBestMatch(defaultGamePath); } - public void LoadShpk(FullPath path) + private void LoadShpk(FullPath path) => Task.Run(() => DoLoadShpk(path)); private async Task DoLoadShpk(FullPath path) { - ShadersKnown = false; - ShaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader"; - ShpkLoading = true; + _shadersKnown = false; + _shaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader"; + _shpkLoading = true; try { var data = path.IsRooted ? await File.ReadAllBytesAsync(path.FullName) : _gameData.GetFile(path.InternalName.ToString())?.Data; - LoadedShpkPath = path; - AssociatedShpk = data?.Length > 0 ? new ShpkFile(data) : throw new Exception("Failure to load file data."); - LoadedShpkPathName = path.ToPath(); + _loadedShpkPath = path; + _associatedShpk = data?.Length > 0 ? new ShpkFile(data) : throw new Exception("Failure to load file data."); + _loadedShpkPathName = path.ToPath(); } catch (Exception e) { - LoadedShpkPath = FullPath.Empty; - LoadedShpkPathName = string.Empty; - AssociatedShpk = null; - Penumbra.Messager.NotificationMessage(e, $"Could not load {LoadedShpkPath.ToPath()}.", NotificationType.Error, false); + _loadedShpkPath = FullPath.Empty; + _loadedShpkPathName = string.Empty; + _associatedShpk = null; + Penumbra.Messager.NotificationMessage(e, $"Could not load {_loadedShpkPath.ToPath()}.", NotificationType.Error, false); } finally { - ShpkLoading = false; + _shpkLoading = false; } - if (LoadedShpkPath.InternalName.IsEmpty) + if (_loadedShpkPath.InternalName.IsEmpty) { - AssociatedShpkDevkit = null; - LoadedShpkDevkitPathName = string.Empty; + _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(); @@ -178,9 +178,9 @@ public partial class MtrlTab private void UpdateShaderKeys() { - ShaderKeys.Clear(); - if (AssociatedShpk != null) - foreach (var key in AssociatedShpk.MaterialKeys) + _shaderKeys.Clear(); + if (_associatedShpk != null) + foreach (var key in _associatedShpk.MaterialKeys) { var keyName = Names.KnownNames.TryResolve(key.Id); var dkData = TryGetShpkDevkitData("ShaderKeys", key.Id, false); @@ -210,7 +210,7 @@ public partial class MtrlTab return string.Compare(x.Label, y.Label, StringComparison.Ordinal); }); - ShaderKeys.Add((hasDkLabel ? dkData!.Label : keyName.ToString(), mtrlKeyIndex, dkData?.Description ?? string.Empty, + _shaderKeys.Add((hasDkLabel ? dkData!.Label : keyName.ToString(), mtrlKeyIndex, dkData?.Description ?? string.Empty, !hasDkLabel, values)); } else @@ -218,7 +218,7 @@ public partial class MtrlTab { var keyName = Names.KnownNames.TryResolve(key.Category); var valueName = keyName.WithKnownSuffixes().TryResolve(Names.KnownNames, key.Value); - ShaderKeys.Add((keyName.ToString(), index, string.Empty, true, [(valueName.ToString(), key.Value, string.Empty)])); + _shaderKeys.Add((keyName.ToString(), index, string.Empty, true, [(valueName.ToString(), key.Value, string.Empty)])); } } @@ -232,27 +232,28 @@ public partial class MtrlTab passSet = []; byPassSets.Add(passId, passSet); } + passSet.Add(shaderIndex); } - VertexShaders.Clear(); - PixelShaders.Clear(); + _vertexShaders.Clear(); + _pixelShaders.Clear(); var vertexShadersByPass = new Dictionary>(); var pixelShadersByPass = new Dictionary>(); - if (AssociatedShpk == null || !AssociatedShpk.IsExhaustiveNodeAnalysisFeasible()) + if (_associatedShpk == null || !_associatedShpk.IsExhaustiveNodeAnalysisFeasible()) { - ShadersKnown = false; + _shadersKnown = false; } else { - ShadersKnown = true; - var systemKeySelectors = AllSelectors(AssociatedShpk.SystemKeys).ToArray(); - var sceneKeySelectors = AllSelectors(AssociatedShpk.SceneKeys).ToArray(); - var subViewKeySelectors = AllSelectors(AssociatedShpk.SubViewKeys).ToArray(); + _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)); + BuildSelector(_associatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value)); foreach (var systemKeySelector in systemKeySelectors) { @@ -261,38 +262,39 @@ public partial class MtrlTab 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) { - AddShader(VertexShaders, vertexShadersByPass, pass.Id, (int)pass.VertexShader); - AddShader(PixelShaders, pixelShadersByPass, pass.Id, (int)pass.PixelShader); + AddShader(_vertexShaders, vertexShadersByPass, pass.Id, (int)pass.VertexShader); + AddShader(_pixelShaders, pixelShadersByPass, pass.Id, (int)pass.PixelShader); } else - ShadersKnown = false; + _shadersKnown = false; } } } } - if (ShadersKnown) + if (_shadersKnown) { var builder = new StringBuilder(); - foreach (var (passId, passVS) in vertexShadersByPass) + foreach (var (passId, passVertexShader) in vertexShadersByPass) { if (builder.Length > 0) builder.Append("\n\n"); var passName = Names.KnownNames.TryResolve(passId); - var shaders = passVS.OrderBy(i => i).Select(i => $"#{i}"); + var shaders = passVertexShader.OrderBy(i => i).Select(i => $"#{i}"); builder.Append($"Vertex Shaders ({passName}): {string.Join(", ", shaders)}"); - if (pixelShadersByPass.TryGetValue(passId, out var passPS)) + if (pixelShadersByPass.TryGetValue(passId, out var passPixelShader)) { - shaders = passPS.OrderBy(i => i).Select(i => $"#{i}"); + shaders = passPixelShader.OrderBy(i => i).Select(i => $"#{i}"); builder.Append($"\nPixel Shaders ({passName}): {string.Join(", ", shaders)}"); } } - foreach (var (passId, passPS) in pixelShadersByPass) + + foreach (var (passId, passPixelShader) in pixelShadersByPass) { if (vertexShadersByPass.ContainsKey(passId)) continue; @@ -301,22 +303,24 @@ public partial class MtrlTab builder.Append("\n\n"); var passName = Names.KnownNames.TryResolve(passId); - var shaders = passPS.OrderBy(i => i).Select(i => $"#{i}"); + var shaders = passPixelShader.OrderBy(i => i).Select(i => $"#{i}"); builder.Append($"Pixel Shaders ({passName}): {string.Join(", ", shaders)}"); } - ShadersString = Encoding.UTF8.GetBytes(builder.ToString()); + _shadersString = Encoding.UTF8.GetBytes(builder.ToString()); } else - ShadersString = UnknownShadersString; + { + _shadersString = UnknownShadersString; + } - ShaderComment = TryGetShpkDevkitData("Comment", null, true) ?? string.Empty; + _shaderComment = TryGetShpkDevkitData("Comment", null, true) ?? string.Empty; } private bool DrawShaderSection(bool disabled) { var ret = false; - if (ImGui.CollapsingHeader(ShaderHeader)) + if (ImGui.CollapsingHeader(_shaderHeader)) { ret |= DrawPackageNameInput(disabled); ret |= DrawShaderFlagsInput(disabled); @@ -325,20 +329,17 @@ public partial class MtrlTab DrawMaterialShaders(); } - if (!ShpkLoading && (AssociatedShpk == null || AssociatedShpkDevkit == null)) + if (!_shpkLoading && (_associatedShpk == null || _associatedShpkDevkit == null)) { ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if (AssociatedShpk == null) - { + if (_associatedShpk == null) ImUtf8.Text("Unable to find a suitable shader (.shpk) file for cross-references. Some functionality will be missing."u8, ImGuiUtil.HalfBlendText(0x80u)); // Half red - } else - { - ImUtf8.Text("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."u8, + ImUtf8.Text( + "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."u8, ImGuiUtil.HalfBlendText(0x8080u)); // Half yellow - } } return ret; @@ -358,14 +359,14 @@ public partial class MtrlTab if (c) foreach (var value in GetShpkNames()) { - if (ImGui.Selectable(value, value == Mtrl.ShaderPackage.Name)) - { - Mtrl.ShaderPackage.Name = value; - ret = true; - AssociatedShpk = null; - LoadedShpkPath = FullPath.Empty; - LoadShpk(FindAssociatedShpk(out _, out _)); - } + if (!ImGui.Selectable(value, value == Mtrl.ShaderPackage.Name)) + continue; + + Mtrl.ShaderPackage.Name = value; + ret = true; + _associatedShpk = null; + _loadedShpkPath = FullPath.Empty; + LoadShpk(FindAssociatedShpk(out _, out _)); } return ret; @@ -391,23 +392,23 @@ public partial class MtrlTab private void DrawCustomAssociations() { const string tooltip = "Click to copy file path to clipboard."; - var text = AssociatedShpk == null + var text = _associatedShpk == null ? "Associated .shpk file: None" - : $"Associated .shpk file: {LoadedShpkPathName}"; - var devkitText = AssociatedShpkDevkit == null + : $"Associated .shpk file: {_loadedShpkPathName}"; + var devkitText = _associatedShpkDevkit == null ? "Associated dev-kit file: None" - : $"Associated dev-kit file: {LoadedShpkDevkitPathName}"; - var baseDevkitText = AssociatedBaseDevkit == null + : $"Associated dev-kit file: {_loadedShpkDevkitPathName}"; + var baseDevkitText = _associatedBaseDevkit == null ? "Base dev-kit file: None" - : $"Base dev-kit file: {LoadedBaseDevkitPathName}"; + : $"Base dev-kit file: {_loadedBaseDevkitPathName}"; ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - ImGuiUtil.CopyOnClickSelectable(text, LoadedShpkPathName, tooltip); - ImGuiUtil.CopyOnClickSelectable(devkitText, LoadedShpkDevkitPathName, tooltip); - ImGuiUtil.CopyOnClickSelectable(baseDevkitText, LoadedBaseDevkitPathName, tooltip); + ImUtf8.CopyOnClickSelectable(text, _loadedShpkPathName, tooltip); + ImUtf8.CopyOnClickSelectable(devkitText, _loadedShpkDevkitPathName, tooltip); + ImUtf8.CopyOnClickSelectable(baseDevkitText, _loadedBaseDevkitPathName, tooltip); - if (ImGui.Button("Associate Custom .shpk File")) + if (ImUtf8.Button("Associate Custom .shpk File"u8)) _fileDialog.OpenFilePicker("Associate Custom .shpk File...", ".shpk", (success, name) => { if (success) @@ -416,15 +417,15 @@ public partial class MtrlTab var moddedPath = FindAssociatedShpk(out var defaultPath, out var gamePath); ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Associate Default .shpk File", Vector2.Zero, moddedPath.ToPath(), - moddedPath.Equals(LoadedShpkPath))) + if (ImUtf8.ButtonEx("Associate Default .shpk File"u8, moddedPath.ToPath(), Vector2.Zero, + moddedPath.Equals(_loadedShpkPath))) LoadShpk(moddedPath); if (!gamePath.Path.Equals(moddedPath.InternalName)) { ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Associate Unmodded .shpk File", Vector2.Zero, defaultPath, - gamePath.Path.Equals(LoadedShpkPath.InternalName))) + if (ImUtf8.ButtonEx("Associate Unmodded .shpk File", defaultPath, Vector2.Zero, + gamePath.Path.Equals(_loadedShpkPath.InternalName))) LoadShpk(new FullPath(gamePath)); } @@ -433,22 +434,23 @@ public partial class MtrlTab private bool DrawMaterialShaderKeys(bool disabled) { - if (ShaderKeys.Count == 0) + if (_shaderKeys.Count == 0) return false; var ret = false; - foreach (var (label, index, description, monoFont, values) in ShaderKeys) + foreach (var (label, index, description, monoFont, values) in _shaderKeys) { using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); ref var key = ref Mtrl.ShaderPackage.ShaderKeys[index]; - var shpkKey = AssociatedShpk?.GetMaterialKeyById(key.Category); + using var id = ImUtf8.PushId((int)key.Category); + var shpkKey = _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)) + using (var c = ImUtf8.Combo(""u8, currentLabel)) { if (c) foreach (var (valueLabel, value, valueDescription) in values) @@ -469,16 +471,16 @@ public partial class MtrlTab if (description.Length > 0) ImGuiUtil.LabeledHelpMarker(label, description); else - ImGui.TextUnformatted(label); + ImUtf8.Text(label); } else if (description.Length > 0 || currentDescription.Length > 0) { - ImGuiUtil.LabeledHelpMarker($"{label}: {currentLabel}", + ImUtf8.LabeledHelpMarker($"{label}: {currentLabel}", description + (description.Length > 0 && currentDescription.Length > 0 ? "\n\n" : string.Empty) + currentDescription); } else { - ImGui.TextUnformatted($"{label}: {currentLabel}"); + ImUtf8.Text($"{label}: {currentLabel}"); } } @@ -487,19 +489,19 @@ public partial class MtrlTab private void DrawMaterialShaders() { - if (AssociatedShpk == null) + if (_associatedShpk == null) return; using (var node = ImUtf8.TreeNode("Candidate Shaders"u8)) { if (node) - ImUtf8.Text(ShadersString.Span); + ImUtf8.Text(_shadersString.Span); } - if (ShaderComment.Length > 0) + if (_shaderComment.Length > 0) { ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - ImGui.TextUnformatted(ShaderComment); + ImUtf8.Text(_shaderComment); } } } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs index 3181dafe..7ab2900d 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs @@ -23,7 +23,7 @@ public partial class MtrlTab { Textures.Clear(); SamplerIds.Clear(); - if (AssociatedShpk == null) + if (_associatedShpk == null) { SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); if (Mtrl.Table != null) @@ -34,11 +34,11 @@ public partial class MtrlTab } else { - 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) + 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) { SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); if (Mtrl.Table != null) @@ -47,11 +47,11 @@ public partial class MtrlTab foreach (var samplerId in SamplerIds) { - var shpkSampler = AssociatedShpk.GetSamplerById(samplerId); + var shpkSampler = _associatedShpk.GetSamplerById(samplerId); 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); @@ -95,9 +95,12 @@ public partial class MtrlTab private static ReadOnlySpan TextureAddressModeTooltip(TextureAddressMode addressMode) => addressMode switch { - TextureAddressMode.Wrap => "Tile the texture at every UV integer junction.\n\nFor example, for U values between 0 and 3, the texture is repeated three times."u8, - TextureAddressMode.Mirror => "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."u8, - TextureAddressMode.Clamp => "Texture coordinates outside the range [0.0, 1.0] are set to the texture color at 0.0 or 1.0, respectively."u8, + TextureAddressMode.Wrap => + "Tile the texture at every UV integer junction.\n\nFor example, for U values between 0 and 3, the texture is repeated three times."u8, + TextureAddressMode.Mirror => + "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."u8, + TextureAddressMode.Clamp => + "Texture coordinates outside the range [0.0, 1.0] are set to the texture color at 0.0 or 1.0, respectively."u8, TextureAddressMode.Border => "Texture coordinates outside the range [0.0, 1.0] are set to the border color (generally black)."u8, _ => ""u8, }; @@ -167,7 +170,7 @@ public partial class MtrlTab return ret; } - + private static bool ComboTextureAddressMode(ReadOnlySpan label, ref TextureAddressMode value) { using var c = ImUtf8.Combo(label, value.ToString()); @@ -202,7 +205,7 @@ public partial class MtrlTab ret = true; } - ref var samplerFlags = ref SamplerFlags.Wrap(ref sampler.Flags); + ref var samplerFlags = ref Wrap(ref sampler.Flags); ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); var addressMode = samplerFlags.UAddressMode; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs index 2d4e93f1..6e16de99 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs @@ -4,7 +4,6 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Text; using OtterGui.Widgets; -using Penumbra.GameData; using Penumbra.GameData.Files; using Penumbra.GameData.Files.MaterialStructs; using Penumbra.GameData.Interop; @@ -21,7 +20,7 @@ public sealed partial class MtrlTab : IWritable, IDisposable private const int ShpkPrefixLength = 16; private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true); - + private readonly IDataManager _gameData; private readonly IFramework _framework; private readonly ObjectManager _objects; @@ -40,7 +39,8 @@ public sealed partial class MtrlTab : IWritable, IDisposable private bool _updateOnNextFrame; public unsafe MtrlTab(IDataManager gameData, IFramework framework, ObjectManager objects, CharacterBaseDestructor characterBaseDestructor, - StainService stainService, ResourceTreeFactory resourceTreeFactory, FileDialogService fileDialog, MaterialTemplatePickers materialTemplatePickers, + StainService stainService, ResourceTreeFactory resourceTreeFactory, FileDialogService fileDialog, + MaterialTemplatePickers materialTemplatePickers, Configuration config, ModEditWindow edit, MtrlFile file, string filePath, bool writable) { _gameData = gameData; @@ -53,11 +53,11 @@ public sealed partial class MtrlTab : IWritable, IDisposable _materialTemplatePickers = materialTemplatePickers; _config = config; - _edit = edit; - Mtrl = file; - FilePath = filePath; - Writable = writable; - AssociatedBaseDevkit = TryLoadShpkDevkit("_base", out LoadedBaseDevkitPathName); + _edit = edit; + Mtrl = file; + FilePath = filePath; + Writable = writable; + _associatedBaseDevkit = TryLoadShpkDevkit("_base", out _loadedBaseDevkitPathName); Update(); LoadShpk(FindAssociatedShpk(out _, out _)); if (writable) @@ -118,7 +118,7 @@ public sealed partial class MtrlTab : IWritable, IDisposable using var dis = ImRaii.Disabled(disabled); var tmp = shaderFlags.EnableTransparency; - if (ImGui.Checkbox("Enable Transparency", ref tmp)) + if (ImUtf8.Checkbox("Enable Transparency"u8, ref tmp)) { shaderFlags.EnableTransparency = tmp; ret = true; @@ -127,14 +127,14 @@ public sealed partial class MtrlTab : IWritable, IDisposable ImGui.SameLine(200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X); tmp = shaderFlags.HideBackfaces; - if (ImGui.Checkbox("Hide Backfaces", ref tmp)) + if (ImUtf8.Checkbox("Hide Backfaces"u8, ref tmp)) { shaderFlags.HideBackfaces = tmp; ret = true; SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); } - if (ShpkLoading) + if (_shpkLoading) { ImGui.SameLine(400 * UiHelpers.Scale + 2 * ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X); @@ -147,32 +147,32 @@ public sealed partial class MtrlTab : IWritable, IDisposable private void DrawOtherMaterialDetails(bool _) { - if (!ImGui.CollapsingHeader("Further Content")) + if (!ImUtf8.CollapsingHeader("Further Content"u8)) return; - using (var sets = ImRaii.TreeNode("UV Sets", ImGuiTreeNodeFlags.DefaultOpen)) + using (var sets = ImUtf8.TreeNode("UV Sets"u8, ImGuiTreeNodeFlags.DefaultOpen)) { if (sets) foreach (var set in Mtrl.UvSets) - ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); + ImUtf8.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); } - using (var sets = ImRaii.TreeNode("Color Sets", ImGuiTreeNodeFlags.DefaultOpen)) + using (var sets = ImUtf8.TreeNode("Color Sets"u8, ImGuiTreeNodeFlags.DefaultOpen)) { if (sets) foreach (var set in Mtrl.ColorSets) - ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); + ImUtf8.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); } if (Mtrl.AdditionalData.Length <= 0) return; - using var t = ImRaii.TreeNode($"Additional Data (Size: {Mtrl.AdditionalData.Length})###AdditionalData"); + using var t = ImUtf8.TreeNode($"Additional Data (Size: {Mtrl.AdditionalData.Length})###AdditionalData"); if (t) Widget.DrawHexViewer(Mtrl.AdditionalData); } - public void Update() + private void Update() { UpdateShaders(); UpdateTextures(); @@ -187,12 +187,12 @@ public sealed partial class MtrlTab : IWritable, IDisposable } public bool Valid - => ShadersKnown && Mtrl.Valid; + => _shadersKnown && Mtrl.Valid; public byte[] Write() { var output = Mtrl.Clone(); - output.GarbageCollect(AssociatedShpk, SamplerIds); + output.GarbageCollect(_associatedShpk, SamplerIds); return output.Write(); } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs index af8b7db2..09db4277 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs @@ -8,9 +8,16 @@ using Penumbra.Services; namespace Penumbra.UI.AdvancedWindow.Materials; -public sealed class MtrlTabFactory(IDataManager gameData, IFramework framework, ObjectManager objects, - CharacterBaseDestructor characterBaseDestructor, StainService stainService, ResourceTreeFactory resourceTreeFactory, - FileDialogService fileDialog, MaterialTemplatePickers materialTemplatePickers, Configuration config) : IUiService +public sealed class MtrlTabFactory( + IDataManager gameData, + IFramework framework, + ObjectManager objects, + CharacterBaseDestructor characterBaseDestructor, + StainService stainService, + ResourceTreeFactory resourceTreeFactory, + FileDialogService fileDialog, + MaterialTemplatePickers materialTemplatePickers, + Configuration config) : IUiService { public MtrlTab Create(ModEditWindow edit, MtrlFile file, string filePath, bool writable) => new(gameData, framework, objects, characterBaseDestructor, stainService, resourceTreeFactory, fileDialog, diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index ee883daf..59b38465 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Text; using Penumbra.UI.AdvancedWindow.Materials; namespace Penumbra.UI.AdvancedWindow; @@ -24,7 +25,7 @@ public partial class ModEditWindow if (_editor.Files.Mdl.Count == 0) return; - using var tab = ImRaii.TabItem("Material Reassignment"); + using var tab = ImUtf8.TabItem("Material Reassignment"u8); if (!tab) return; @@ -32,45 +33,43 @@ public partial class ModEditWindow MaterialSuffix.Draw(_editor, ImGuiHelpers.ScaledVector2(175, 0)); ImGui.NewLine(); - using var child = ImRaii.Child("##mdlFiles", -Vector2.One, true); + using var child = ImUtf8.Child("##mdlFiles"u8, -Vector2.One, true); if (!child) return; - using var table = ImRaii.Table("##files", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One); + using var table = ImUtf8.Table("##files"u8, 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()) { 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 (ImUtf8.IconButton(FontAwesomeIcon.Save, "Save the changed mdl file.\nUse at own risk!"u8, disabled: !info.Changed)) info.Save(_editor.Compactor); ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), iconSize, - "Restore current changes to default.", !info.Changed, true)) + if (ImUtf8.IconButton(FontAwesomeIcon.Recycle, "Restore current changes to default."u8, disabled: !info.Changed)) info.Restore(); ImGui.TableNextColumn(); - ImGui.TextUnformatted(info.Path.FullName[(Mod!.ModPath.FullName.Length + 1)..]); + ImUtf8.Text(info.Path.InternalName.Span[(Mod!.ModPath.FullName.Length + 1)..]); ImGui.TableNextColumn(); ImGui.SetNextItemWidth(400 * UiHelpers.Scale); var tmp = info.CurrentMaterials[0]; - if (ImGui.InputText("##0", ref tmp, 64)) + if (ImUtf8.InputText("##0"u8, ref tmp)) info.SetMaterial(tmp, 0); for (var i = 1; i < info.Count; ++i) { + using var id2 = ImUtf8.PushId(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)) + if (ImUtf8.InputText(""u8, ref tmp)) info.SetMaterial(tmp, i); } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index 8a1c729c..41f1da26 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -10,7 +10,6 @@ using Penumbra.GameData.Interop; using Penumbra.String; using static Penumbra.GameData.Files.ShpkFile; using OtterGui.Widgets; -using Penumbra.GameData.Files.ShaderStructs; using OtterGui.Text; using Penumbra.GameData.Structs; @@ -56,21 +55,17 @@ public partial class ModEditWindow private static void DrawShaderPackageSummary(ShpkTab tab) { if (tab.Shpk.IsLegacy) - { ImUtf8.Text("This legacy shader package will not work in the current version of the game. Do not attempt to load it.", ImGuiUtil.HalfBlendText(0x80u)); // Half red - } - ImGui.TextUnformatted(tab.Header); + ImUtf8.Text(tab.Header); if (!tab.Shpk.Disassembled) - { ImUtf8.Text("Your system doesn't support disassembling shaders. Some functionality will be missing.", ImGuiUtil.HalfBlendText(0x80u)); // Half red - } } private static void DrawShaderExportButton(ShpkTab tab, string objectName, Shader shader, int idx) { - if (!ImGui.Button($"Export Shader Program Blob ({shader.Blob.Length} bytes)")) + if (!ImUtf8.Button($"Export Shader Program Blob ({shader.Blob.Length} bytes)")) return; var defaultName = objectName[0] switch @@ -106,7 +101,7 @@ public partial class ModEditWindow private static void DrawShaderImportButton(ShpkTab tab, string objectName, Shader[] shaders, int idx) { - if (!ImGui.Button("Replace Shader Program Blob")) + if (!ImUtf8.Button("Replace Shader Program Blob"u8)) return; tab.FileDialog.OpenFilePicker($"Replace {objectName} #{idx} Program Blob...", "Shader Program Blobs{.o,.cso,.dxbc,.dxil}", @@ -145,8 +140,8 @@ public partial class ModEditWindow private static unsafe void DrawRawDisassembly(Shader shader) { - using var t2 = ImRaii.TreeNode("Raw Program Disassembly"); - if (!t2) + using var tree = ImUtf8.TreeNode("Raw Program Disassembly"u8); + if (!tree) return; using var font = ImRaii.PushFont(UiBuilder.MonoFont); @@ -164,31 +159,34 @@ public partial class ModEditWindow { foreach (var (key, keyIdx) in shader.SystemValues!.WithIndex()) { - ImRaii.TreeNode($"Used with System Key {tab.TryResolveName(tab.Shpk.SystemKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", + ImUtf8.TreeNode( + $"Used with System Key {tab.TryResolveName(tab.Shpk.SystemKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } foreach (var (key, keyIdx) in shader.SceneValues!.WithIndex()) { - ImRaii.TreeNode($"Used with Scene Key {tab.TryResolveName(tab.Shpk.SceneKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", + ImUtf8.TreeNode( + $"Used with Scene Key {tab.TryResolveName(tab.Shpk.SceneKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } foreach (var (key, keyIdx) in shader.MaterialValues!.WithIndex()) { - ImRaii.TreeNode($"Used with Material Key {tab.TryResolveName(tab.Shpk.MaterialKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", + ImUtf8.TreeNode( + $"Used with Material Key {tab.TryResolveName(tab.Shpk.MaterialKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } foreach (var (key, keyIdx) in shader.SubViewValues!.WithIndex()) { - ImRaii.TreeNode($"Used with Sub-View Key #{keyIdx} \u2208 {{ {tab.NameSetToString(key)} }}", + ImUtf8.TreeNode($"Used with Sub-View Key #{keyIdx} \u2208 {{ {tab.NameSetToString(key)} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } } } - ImRaii.TreeNode($"Used in Passes: {tab.NameSetToString(shader.Passes)}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode($"Used in Passes: {tab.NameSetToString(shader.Passes)}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } private static void DrawShaderPackageFilterSection(ShpkTab tab) @@ -215,19 +213,20 @@ public partial class ModEditWindow { if (values.PossibleValues == null) { - ImRaii.TreeNode(label, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode(label, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); return; } - using var node = ImRaii.TreeNode(label); + using var node = ImUtf8.TreeNode(label); if (!node) return; foreach (var value in values.PossibleValues) { var contains = values.Contains(value); - if (!ImGui.Checkbox($"{tab.TryResolveName(value)}", ref contains)) + if (!ImUtf8.Checkbox($"{tab.TryResolveName(value)}", ref contains)) continue; + if (contains) { if (values.AddExisting(value)) @@ -249,7 +248,7 @@ public partial class ModEditWindow private static bool DrawShaderPackageShaderArray(ShpkTab tab, string objectName, Shader[] shaders, bool disabled) { - if (shaders.Length == 0 || !ImGui.CollapsingHeader($"{objectName}s")) + if (shaders.Length == 0 || !ImUtf8.CollapsingHeader($"{objectName}s")) return false; var ret = false; @@ -259,7 +258,7 @@ public partial class ModEditWindow if (!tab.IsFilterMatch(shader)) continue; - using var t = ImRaii.TreeNode($"{objectName} #{idx}"); + using var t = ImUtf8.TreeNode($"{objectName} #{idx}"); if (!t) continue; @@ -270,20 +269,20 @@ public partial class ModEditWindow DrawShaderImportButton(tab, objectName, shaders, idx); } - ret |= DrawShaderPackageResourceArray("Constant Buffers", "slot", true, shader.Constants, false, true); - ret |= DrawShaderPackageResourceArray("Samplers", "slot", false, shader.Samplers, false, true); + ret |= DrawShaderPackageResourceArray("Constant Buffers", "slot", true, shader.Constants, false, true); + ret |= DrawShaderPackageResourceArray("Samplers", "slot", false, shader.Samplers, false, true); if (!tab.Shpk.IsLegacy) - ret |= DrawShaderPackageResourceArray("Textures", "slot", false, shader.Textures, false, true); - ret |= DrawShaderPackageResourceArray("Unordered Access Views", "slot", true, shader.Uavs, false, true); + ret |= DrawShaderPackageResourceArray("Textures", "slot", false, shader.Textures, false, true); + ret |= DrawShaderPackageResourceArray("Unordered Access Views", "slot", true, shader.Uavs, false, true); if (shader.DeclaredInputs != 0) - ImRaii.TreeNode($"Declared Inputs: {shader.DeclaredInputs}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode($"Declared Inputs: {shader.DeclaredInputs}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); if (shader.UsedInputs != 0) - ImRaii.TreeNode($"Used Inputs: {shader.UsedInputs}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode($"Used Inputs: {shader.UsedInputs}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); if (shader.AdditionalHeader.Length > 8) { - using var t2 = ImRaii.TreeNode($"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader"); + using var t2 = ImUtf8.TreeNode($"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader"); if (t2) Widget.DrawHexViewer(shader.AdditionalHeader); } @@ -313,23 +312,28 @@ public partial class ModEditWindow var usedString = UsedComponentString(withSize, false, resource); if (usedString.Length > 0) { - ImRaii.TreeNode(hasFilter ? $"Globally Used: {usedString}" : $"Used: {usedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode(hasFilter ? $"Globally Used: {usedString}" : $"Used: {usedString}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); if (hasFilter) { var filteredUsedString = UsedComponentString(withSize, true, resource); if (filteredUsedString.Length > 0) - ImRaii.TreeNode($"Used within Filters: {filteredUsedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode($"Used within Filters: {filteredUsedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet) + .Dispose(); else - ImRaii.TreeNode("Unused within Filters", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode("Unused within Filters"u8, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } } else - ImRaii.TreeNode(hasFilter ? "Globally Unused" : "Unused", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + { + ImUtf8.TreeNode(hasFilter ? "Globally Unused"u8 : "Unused"u8, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } return ret; } - private static bool DrawShaderPackageResourceArray(string arrayName, string slotLabel, bool withSize, Resource[] resources, bool hasFilter, bool disabled) + private static bool DrawShaderPackageResourceArray(string arrayName, string slotLabel, bool withSize, Resource[] resources, bool hasFilter, + bool disabled) { if (resources.Length == 0) return false; @@ -345,8 +349,8 @@ public partial class ModEditWindow 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); - font.Dispose(); + using var t2 = ImUtf8.TreeNode(name, !disabled || buf.Used != null ? 0 : ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet); + font.Pop(); if (t2) ret |= DrawShaderPackageResource(slotLabel, withSize, ref buf, hasFilter, disabled); } @@ -361,7 +365,7 @@ public partial class ModEditWindow + new Vector2(ImGui.CalcTextSize(label).X + 3 * ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight(), ImGui.GetStyle().FramePadding.Y); - var ret = ImGui.CollapsingHeader(label); + var ret = ImUtf8.CollapsingHeader(label); ImGui.GetWindowDrawList() .AddText(UiBuilder.DefaultFont, UiBuilder.DefaultFont.FontSize, pos, ImGui.GetColorU32(ImGuiCol.Text), "Layout"); return ret; @@ -374,7 +378,7 @@ public partial class ModEditWindow if (isSizeWellDefined) return true; - ImGui.TextUnformatted(materialParams.HasValue + ImUtf8.Text(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"); return false; @@ -382,7 +386,7 @@ public partial class ModEditWindow private static bool DrawShaderPackageMaterialMatrix(ShpkTab tab, bool disabled) { - ImGui.TextUnformatted(tab.Shpk.Disassembled + ImUtf8.Text(tab.Shpk.Disassembled ? "Parameter positions (continuations are grayed out, globally unused values are red, unused values within filters are yellow):" : "Parameter positions (continuations are grayed out):"); @@ -398,10 +402,7 @@ public partial class ModEditWindow ImGui.TableSetupColumn("w", ImGuiTableColumnFlags.WidthFixed, 250 * UiHelpers.Scale); ImGui.TableHeadersRow(); - var textColorStart = ImGui.GetColorU32(ImGuiCol.Text); - var textColorCont = ImGuiUtil.HalfTransparent(textColorStart); // Half opacity - var textColorUnusedStart = ImGuiUtil.HalfBlend(textColorStart, 0x80u); // Half red - var textColorUnusedCont = ImGuiUtil.HalfTransparent(textColorUnusedStart); + var textColorStart = ImGui.GetColorU32(ImGuiCol.Text); var ret = false; for (var i = 0; i < tab.Matrix.GetLength(0); ++i) @@ -420,12 +421,12 @@ public partial class ModEditWindow color = ImGuiUtil.HalfTransparent(color); // Half opacity using var _ = ImRaii.PushId(i * 4 + j); var deletable = !disabled && idx >= 0; - using (var font = ImRaii.PushFont(UiBuilder.MonoFont, tooltip.Length > 0)) + using (ImRaii.PushFont(UiBuilder.MonoFont, tooltip.Length > 0)) { - using (var c = ImRaii.PushColor(ImGuiCol.Text, color)) + using (ImRaii.PushColor(ImGuiCol.Text, color)) { ImGui.TableNextColumn(); - ImGui.Selectable(name); + ImUtf8.Selectable(name); if (deletable && ImGui.IsItemClicked(ImGuiMouseButton.Right) && ImGui.GetIO().KeyCtrl) { tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.RemoveItems(idx); @@ -434,11 +435,11 @@ public partial class ModEditWindow } } - ImGuiUtil.HoverTooltip(tooltip); + ImUtf8.HoverTooltip(tooltip); } if (deletable) - ImGuiUtil.HoverTooltip("\nControl + Right-Click to remove."); + ImUtf8.HoverTooltip("\nControl + Right-Click to remove."u8); } } @@ -450,7 +451,9 @@ public partial class ModEditWindow if (!ImUtf8.Button("Export globally unused parameters as material dev-kit file"u8)) return; - tab.FileDialog.OpenSavePicker("Export material dev-kit file", ".json", $"{Path.GetFileNameWithoutExtension(tab.FilePath)}.json", ".json", DoSave, null, false); + tab.FileDialog.OpenSavePicker("Export material dev-kit file", ".json", $"{Path.GetFileNameWithoutExtension(tab.FilePath)}.json", + ".json", DoSave, null, false); + return; void DoSave(bool success, string path) { @@ -476,22 +479,22 @@ public partial class ModEditWindow private static void DrawShaderPackageMisalignedParameters(ShpkTab tab) { - using var t = ImRaii.TreeNode("Misaligned / Overflowing Parameters"); + using var t = ImUtf8.TreeNode("Misaligned / Overflowing Parameters"u8); if (!t) return; using var _ = ImRaii.PushFont(UiBuilder.MonoFont); foreach (var name in tab.MalformedParameters) - ImRaii.TreeNode(name, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode(name, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } private static void DrawShaderPackageStartCombo(ShpkTab tab) { using var s = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing); - using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) + using (ImRaii.PushFont(UiBuilder.MonoFont)) { ImGui.SetNextItemWidth(UiHelpers.Scale * 400); - using var c = ImRaii.Combo("##Start", tab.Orphans[tab.NewMaterialParamStart].Name); + using var c = ImUtf8.Combo("##Start", tab.Orphans[tab.NewMaterialParamStart].Name); if (c) foreach (var (start, idx) in tab.Orphans.WithIndex()) { @@ -501,7 +504,7 @@ public partial class ModEditWindow } ImGui.SameLine(); - ImGui.TextUnformatted("Start"); + ImUtf8.Text("Start"u8); } private static void DrawShaderPackageEndCombo(ShpkTab tab) @@ -510,7 +513,7 @@ public partial class ModEditWindow using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) { ImGui.SetNextItemWidth(UiHelpers.Scale * 400); - using var c = ImRaii.Combo("##End", tab.Orphans[tab.NewMaterialParamEnd].Name); + using var c = ImUtf8.Combo("##End", tab.Orphans[tab.NewMaterialParamEnd].Name); if (c) { var current = tab.Orphans[tab.NewMaterialParamStart].Index; @@ -527,7 +530,7 @@ public partial class ModEditWindow } ImGui.SameLine(); - ImGui.TextUnformatted("End"); + ImUtf8.Text("End"u8); } private static bool DrawShaderPackageNewParameter(ShpkTab tab) @@ -540,15 +543,14 @@ public partial class ModEditWindow ImGui.SetNextItemWidth(UiHelpers.Scale * 400); var newName = tab.NewMaterialParamName.Value!; - if (ImGui.InputText("Name", ref newName, 63)) + if (ImUtf8.InputText("Name", ref newName)) tab.NewMaterialParamName = newName; var tooltip = tab.UsedIds.Contains(tab.NewMaterialParamName.Crc32) - ? "The ID is already in use. Please choose a different name." - : string.Empty; - if (!ImGuiUtil.DrawDisabledButton($"Add {tab.NewMaterialParamName} (0x{tab.NewMaterialParamName.Crc32:X8})", new Vector2(400 * UiHelpers.Scale, ImGui.GetFrameHeight()), - tooltip, - tooltip.Length > 0)) + ? "The ID is already in use. Please choose a different name."u8 + : ""u8; + if (!ImUtf8.ButtonEx($"Add {tab.NewMaterialParamName} (0x{tab.NewMaterialParamName.Crc32:X8})", tooltip, + new Vector2(400 * UiHelpers.Scale, ImGui.GetFrameHeight()), tooltip.Length > 0)) return false; tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.AddItem(new MaterialParam @@ -589,15 +591,15 @@ public partial class ModEditWindow { var ret = false; - if (!ImGui.CollapsingHeader("Shader Resources")) + if (!ImUtf8.CollapsingHeader("Shader Resources"u8)) return false; var hasFilters = tab.FilterPopCount != tab.FilterMaximumPopCount; - ret |= DrawShaderPackageResourceArray("Constant Buffers", "type", true, tab.Shpk.Constants, hasFilters, disabled); - ret |= DrawShaderPackageResourceArray("Samplers", "type", false, tab.Shpk.Samplers, hasFilters, disabled); + ret |= DrawShaderPackageResourceArray("Constant Buffers", "type", true, tab.Shpk.Constants, hasFilters, disabled); + ret |= DrawShaderPackageResourceArray("Samplers", "type", false, tab.Shpk.Samplers, hasFilters, disabled); if (!tab.Shpk.IsLegacy) - ret |= DrawShaderPackageResourceArray("Textures", "type", false, tab.Shpk.Textures, hasFilters, disabled); - ret |= DrawShaderPackageResourceArray("Unordered Access Views", "type", false, tab.Shpk.Uavs, hasFilters, disabled); + ret |= DrawShaderPackageResourceArray("Textures", "type", false, tab.Shpk.Textures, hasFilters, disabled); + ret |= DrawShaderPackageResourceArray("Unordered Access Views", "type", false, tab.Shpk.Uavs, hasFilters, disabled); return ret; } @@ -607,18 +609,20 @@ public partial class ModEditWindow if (keys.Count == 0) return; - using var t = ImRaii.TreeNode(arrayName); + using var t = ImUtf8.TreeNode(arrayName); if (!t) return; using var font = ImRaii.PushFont(UiBuilder.MonoFont); foreach (var (key, idx) in keys.WithIndex()) { - using var t2 = ImRaii.TreeNode(withId ? $"#{idx}: {tab.TryResolveName(key.Id)} (0x{key.Id:X8})" : $"#{idx}"); + using var t2 = ImUtf8.TreeNode(withId ? $"#{idx}: {tab.TryResolveName(key.Id)} (0x{key.Id:X8})" : $"#{idx}"); if (t2) { - ImRaii.TreeNode($"Default Value: {tab.TryResolveName(key.DefaultValue)} (0x{key.DefaultValue:X8})", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); - ImRaii.TreeNode($"Known Values: {tab.NameSetToString(key.Values, true)}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode($"Default Value: {tab.TryResolveName(key.DefaultValue)} (0x{key.DefaultValue:X8})", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode($"Known Values: {tab.NameSetToString(key.Values, true)}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet) + .Dispose(); } } } @@ -628,7 +632,7 @@ public partial class ModEditWindow if (tab.Shpk.Nodes.Length <= 0) return; - using var t = ImRaii.TreeNode($"Nodes ({tab.Shpk.Nodes.Length})###Nodes"); + using var t = ImUtf8.TreeNode($"Nodes ({tab.Shpk.Nodes.Length})###Nodes"); if (!t) return; @@ -639,39 +643,44 @@ public partial class ModEditWindow if (!tab.IsFilterMatch(node)) continue; - using var t2 = ImRaii.TreeNode($"#{idx:D4}: Selector: 0x{node.Selector:X8}"); + using var t2 = ImUtf8.TreeNode($"#{idx:D4}: Selector: 0x{node.Selector:X8}"); if (!t2) continue; foreach (var (key, keyIdx) in node.SystemKeys.WithIndex()) { - ImRaii.TreeNode($"System Key {tab.TryResolveName(tab.Shpk.SystemKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SystemValues![keyIdx])} }}", + ImUtf8.TreeNode( + $"System Key {tab.TryResolveName(tab.Shpk.SystemKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SystemValues![keyIdx])} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } foreach (var (key, keyIdx) in node.SceneKeys.WithIndex()) { - ImRaii.TreeNode($"Scene Key {tab.TryResolveName(tab.Shpk.SceneKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SceneValues![keyIdx])} }}", + ImUtf8.TreeNode( + $"Scene Key {tab.TryResolveName(tab.Shpk.SceneKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SceneValues![keyIdx])} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } foreach (var (key, keyIdx) in node.MaterialKeys.WithIndex()) { - ImRaii.TreeNode($"Material Key {tab.TryResolveName(tab.Shpk.MaterialKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.MaterialValues![keyIdx])} }}", + ImUtf8.TreeNode( + $"Material Key {tab.TryResolveName(tab.Shpk.MaterialKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.MaterialValues![keyIdx])} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } foreach (var (key, keyIdx) in node.SubViewKeys.WithIndex()) { - ImRaii.TreeNode($"Sub-View Key #{keyIdx} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SubViewValues![keyIdx])} }}", + ImUtf8.TreeNode( + $"Sub-View Key #{keyIdx} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SubViewValues![keyIdx])} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } - ImRaii.TreeNode($"Pass Indices: {string.Join(' ', node.PassIndices.Select(c => $"{c:X2}"))}", + ImUtf8.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: {tab.TryResolveName(pass.Id)}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", + ImUtf8.TreeNode( + $"Pass #{passIdx}: ID: {tab.TryResolveName(pass.Id)}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet) .Dispose(); } @@ -680,7 +689,7 @@ public partial class ModEditWindow private static void DrawShaderPackageSelection(ShpkTab tab) { - if (!ImGui.CollapsingHeader("Shader Selection")) + if (!ImUtf8.CollapsingHeader("Shader Selection"u8)) return; DrawKeyArray(tab, "System Keys", true, tab.Shpk.SystemKeys); @@ -689,13 +698,13 @@ public partial class ModEditWindow DrawKeyArray(tab, "Sub-View Keys", false, tab.Shpk.SubViewKeys); DrawShaderPackageNodes(tab); - using var t = ImRaii.TreeNode($"Node Selectors ({tab.Shpk.NodeSelectors.Count})###NodeSelectors"); + using var t = ImUtf8.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) + ImUtf8.TreeNode($"#{selector.Value:D4}: Selector: 0x{selector.Key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet) .Dispose(); } } @@ -703,14 +712,14 @@ public partial class ModEditWindow private static void DrawOtherShaderPackageDetails(ShpkTab tab) { - if (!ImGui.CollapsingHeader("Further Content")) + if (!ImUtf8.CollapsingHeader("Further Content"u8)) return; - ImRaii.TreeNode($"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode($"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); if (tab.Shpk.AdditionalData.Length > 0) { - using var t = ImRaii.TreeNode($"Additional Data (Size: {tab.Shpk.AdditionalData.Length})###AdditionalData"); + using var t = ImUtf8.TreeNode($"Additional Data (Size: {tab.Shpk.AdditionalData.Length})###AdditionalData"); if (t) Widget.DrawHexViewer(tab.Shpk.AdditionalData); } @@ -718,9 +727,9 @@ public partial class ModEditWindow private static string UsedComponentString(bool withSize, bool filtered, in Resource resource) { - var used = filtered ? resource.FilteredUsed : resource.Used; + var used = filtered ? resource.FilteredUsed : resource.Used; var usedDynamically = filtered ? resource.FilteredUsedDynamically : resource.UsedDynamically; - var sb = new StringBuilder(256); + var sb = new StringBuilder(256); if (withSize) { foreach (var (components, i) in (used ?? Array.Empty()).WithIndex()) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs index de20aa9f..b5b39e90 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs @@ -21,11 +21,11 @@ public partial class ModEditWindow public short NewMaterialParamStart; public short NewMaterialParamEnd; - public SharedSet[] FilterSystemValues; - public SharedSet[] FilterSceneValues; - public SharedSet[] FilterMaterialValues; - public SharedSet[] FilterSubViewValues; - public SharedSet FilterPasses; + public readonly SharedSet[] FilterSystemValues; + public readonly SharedSet[] FilterSceneValues; + public readonly SharedSet[] FilterMaterialValues; + public readonly SharedSet[] FilterSubViewValues; + public SharedSet FilterPasses; public readonly int FilterMaximumPopCount; public int FilterPopCount; @@ -46,6 +46,7 @@ public partial class ModEditWindow { Shpk = new ShpkFile(bytes, false); } + FilePath = filePath; Header = $"Shader Package for DirectX {(int)Shpk.DirectXVersion}"; @@ -105,13 +106,21 @@ public partial class ModEditWindow _nameSetWithIdsCache.Clear(); } - public void UpdateNameCache() + private void UpdateNameCache() { - static void CollectResourceNames(Dictionary nameCache, ShpkFile.Resource[] resources) - { - foreach (var resource in resources) - nameCache.TryAdd(resource.Id, resource.Name); - } + CollectResourceNames(_nameCache, Shpk.Constants); + CollectResourceNames(_nameCache, Shpk.Samplers); + CollectResourceNames(_nameCache, Shpk.Textures); + CollectResourceNames(_nameCache, Shpk.Uavs); + + CollectKeyNames(_nameCache, Shpk.SystemKeys); + CollectKeyNames(_nameCache, Shpk.SceneKeys); + CollectKeyNames(_nameCache, Shpk.MaterialKeys); + CollectKeyNames(_nameCache, Shpk.SubViewKeys); + + _nameSetCache.Clear(); + _nameSetWithIdsCache.Clear(); + return; static void CollectKeyNames(Dictionary nameCache, ShpkFile.Key[] keys) { @@ -128,18 +137,11 @@ public partial class ModEditWindow } } - CollectResourceNames(_nameCache, Shpk.Constants); - CollectResourceNames(_nameCache, Shpk.Samplers); - CollectResourceNames(_nameCache, Shpk.Textures); - CollectResourceNames(_nameCache, Shpk.Uavs); - - CollectKeyNames(_nameCache, Shpk.SystemKeys); - CollectKeyNames(_nameCache, Shpk.SceneKeys); - CollectKeyNames(_nameCache, Shpk.MaterialKeys); - CollectKeyNames(_nameCache, Shpk.SubViewKeys); - - _nameSetCache.Clear(); - _nameSetWithIdsCache.Clear(); + static void CollectResourceNames(Dictionary nameCache, ShpkFile.Resource[] resources) + { + foreach (var resource in resources) + nameCache.TryAdd(resource.Id, resource.Name); + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -151,6 +153,7 @@ public partial class ModEditWindow var cache = withIds ? _nameSetWithIdsCache : _nameSetCache; if (cache.TryGetValue(nameSet, out var nameSetStr)) return nameSetStr; + if (withIds) nameSetStr = string.Join(", ", nameSet.Select(id => $"{TryResolveName(id)} (0x{id:X8})")); else @@ -186,7 +189,8 @@ public partial class ModEditWindow var jEnd = ((param.ByteOffset + param.ByteSize - 1) >> 2) & 3; if ((param.ByteOffset & 0x3) != 0 || (param.ByteSize & 0x3) != 0) { - MalformedParameters.Add($"ID: {TryResolveName(param.Id)} (0x{param.Id:X8}), offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}"); + MalformedParameters.Add( + $"ID: {TryResolveName(param.Id)} (0x{param.Id:X8}), offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}"); continue; } @@ -206,7 +210,8 @@ public partial class ModEditWindow var tt = $"{MtrlTab.MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2).Item1} ({TryResolveName(param.Id)}, 0x{param.Id:X8})"; if (component < defaultFloats.Length) - tt += $"\n\nDefault value: {defaultFloats[component]} ({defaults[component << 2]:X2} {defaults[(component << 2) | 1]:X2} {defaults[(component << 2) | 2]:X2} {defaults[(component << 2) | 3]:X2})"; + tt += + $"\n\nDefault value: {defaultFloats[component]} ({defaults[component << 2]:X2} {defaults[(component << 2) | 1]:X2} {defaults[(component << 2) | 2]:X2} {defaults[(component << 2) | 3]:X2})"; Matrix[i, j] = (TryResolveName(param.Id).ToString(), tt, (short)idx, 0); } } @@ -265,7 +270,8 @@ public partial class ModEditWindow if (oldStart == linear) newMaterialParamStart = (short)Orphans.Count; - Orphans.Add(($"{materialParams?.Name ?? ShpkFile.MaterialParamsConstantName}{MtrlTab.MaterialParamName(false, linear)}", linear)); + Orphans.Add(($"{materialParams?.Name ?? ShpkFile.MaterialParamsConstantName}{MtrlTab.MaterialParamName(false, linear)}", + linear)); } } @@ -407,7 +413,6 @@ public partial class ModEditWindow var unusedSlices = new JArray(); if (materialParameterUsage.Indices(start, length).Any()) - { foreach (var (rgStart, rgEnd) in materialParameterUsage.Ranges(start, length, true)) { unusedSlices.Add(new JObject @@ -417,14 +422,11 @@ public partial class ModEditWindow ["Length"] = rgEnd - rgStart, }); } - } else - { unusedSlices.Add(new JObject { ["Type"] = "Hidden", }); - } dkConstants[param.Id.ToString()] = unusedSlices; }