diff --git a/Glamourer/Gui/Materials/MaterialDrawer.cs b/Glamourer/Gui/Materials/MaterialDrawer.cs new file mode 100644 index 0000000..1114911 --- /dev/null +++ b/Glamourer/Gui/Materials/MaterialDrawer.cs @@ -0,0 +1,178 @@ +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Glamourer.Interop.Material; +using Glamourer.Interop.Structs; +using ImGuiNET; +using OtterGui.Services; +using Penumbra.GameData.Files; + +namespace Glamourer.Gui.Materials; + +public unsafe class MaterialDrawer : IService +{ + private static readonly IReadOnlyList Types = + [ + MaterialValueIndex.DrawObjectType.Human, + MaterialValueIndex.DrawObjectType.Mainhand, + MaterialValueIndex.DrawObjectType.Offhand, + ]; + + public void DrawPanel(Actor actor) + { + if (!actor.IsCharacter) + return; + + foreach (var type in Types) + { + var index = new MaterialValueIndex(type, 0, 0, 0, 0); + if (index.TryGetModel(actor, out var model)) + DrawModelType(model, index); + } + } + + private void DrawModelType(Model model, MaterialValueIndex sourceIndex) + { + using var tree = ImRaii.TreeNode(sourceIndex.DrawObject.ToString()); + if (!tree) + return; + + var names = model.AsCharacterBase->GetModelType() is CharacterBase.ModelType.Human + ? SlotNamesHuman + : SlotNames; + for (byte i = 0; i < model.AsCharacterBase->SlotCount; ++i) + { + var index = sourceIndex with { SlotIndex = i }; + DrawSlot(model, names, index); + } + } + + private void DrawSlot(Model model, IReadOnlyList names, MaterialValueIndex sourceIndex) + { + using var tree = ImRaii.TreeNode(names[sourceIndex.SlotIndex]); + if (!tree) + return; + + for (byte i = 0; i < MaterialService.MaterialsPerModel; ++i) + { + var index = sourceIndex with { MaterialIndex = i }; + var texture = model.AsCharacterBase->ColorTableTextures + index.SlotIndex * MaterialService.MaterialsPerModel + i; + if (*texture == null) + continue; + + if (!DirectXTextureHelper.TryGetColorTable(*texture, out var table)) + continue; + + DrawMaterial(ref table, texture, index); + } + } + + private void DrawMaterial(ref MtrlFile.ColorTable table, Texture** texture, MaterialValueIndex sourceIndex) + { + using var tree = ImRaii.TreeNode($"Material {sourceIndex.MaterialIndex + 1}"); + if (!tree) + return; + + for (byte i = 0; i < MtrlFile.ColorTable.NumRows; ++i) + { + var index = sourceIndex with { RowIndex = i }; + ref var row = ref table[i]; + DrawRow(ref table, ref row, texture, index); + } + } + + private void DrawRow(ref MtrlFile.ColorTable table, ref MtrlFile.ColorTable.Row row, Texture** texture, MaterialValueIndex sourceIndex) + { + using var id = ImRaii.PushId(sourceIndex.RowIndex); + var diffuse = row.Diffuse; + var specular = row.Specular; + var emissive = row.Emissive; + var glossStrength = row.GlossStrength; + var specularStrength = row.SpecularStrength; + if (ImGui.ColorEdit3("Diffuse", ref diffuse, ImGuiColorEditFlags.NoInputs)) + { + var index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.Diffuse }; + row.Diffuse = diffuse; + MaterialService.ReplaceColorTable(texture, table); + } + ImGui.SameLine(); + if (ImGui.ColorEdit3("Specular", ref specular, ImGuiColorEditFlags.NoInputs)) + { + var index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.Specular }; + row.Specular = specular; + MaterialService.ReplaceColorTable(texture, table); + } + ImGui.SameLine(); + if (ImGui.ColorEdit3("Emissive", ref emissive, ImGuiColorEditFlags.NoInputs)) + { + var index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.Emissive }; + row.Emissive = emissive; + MaterialService.ReplaceColorTable(texture, table); + } + ImGui.SameLine(); + ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); + if (ImGui.DragFloat("Gloss", ref glossStrength, 0.1f)) + { + var index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.GlossStrength }; + row.GlossStrength = glossStrength; + MaterialService.ReplaceColorTable(texture, table); + } + ImGui.SameLine(); + ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); + if (ImGui.DragFloat("Specular Strength", ref specularStrength, 0.1f)) + { + var index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.SpecularStrength }; + row.SpecularStrength = specularStrength; + MaterialService.ReplaceColorTable(texture, table); + } + } + + private static readonly IReadOnlyList SlotNames = + [ + "Slot 1", + "Slot 2", + "Slot 3", + "Slot 4", + "Slot 5", + "Slot 6", + "Slot 7", + "Slot 8", + "Slot 9", + "Slot 10", + "Slot 11", + "Slot 12", + "Slot 13", + "Slot 14", + "Slot 15", + "Slot 16", + "Slot 17", + "Slot 18", + "Slot 19", + "Slot 20", + ]; + + private static readonly IReadOnlyList SlotNamesHuman = + [ + "Head", + "Body", + "Hands", + "Legs", + "Feet", + "Earrings", + "Neck", + "Wrists", + "Right Finger", + "Left Finger", + "Slot 11", + "Slot 12", + "Slot 13", + "Slot 14", + "Slot 15", + "Slot 16", + "Slot 17", + "Slot 18", + "Slot 19", + "Slot 20", + ]; +} diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs index 68eb89e..6ec62f5 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs @@ -7,6 +7,7 @@ using Glamourer.Automation; using Glamourer.Designs; using Glamourer.Gui.Customization; using Glamourer.Gui.Equipment; +using Glamourer.Gui.Materials; using Glamourer.Interop; using Glamourer.Interop.Material; using Glamourer.Interop.Structs; @@ -34,7 +35,8 @@ public class ActorPanel( ImportService _importService, ICondition _conditions, DictModelChara _modelChara, - CustomizeParameterDrawer _parameterDrawer) + CustomizeParameterDrawer _parameterDrawer, + MaterialDrawer _materialDrawer) { private ActorIdentifier _identifier; private string _actorName = string.Empty; @@ -121,16 +123,36 @@ public class ActorPanel( RevertButtons(); + + if (ImGui.CollapsingHeader("Material Shit")) + _materialDrawer.DrawPanel(_actor); ImGui.InputInt("Row", ref _rowId); ImGui.InputInt("Material", ref _materialId); ImGui.InputInt("Slot", ref _slotId); ImGuiUtil.GenericEnumCombo("Value", 300, _index, out _index); var index = new MaterialValueIndex(MaterialValueIndex.DrawObjectType.Human, (byte) _slotId, (byte) _materialId, (byte)_rowId, _index); - index.TryGetValue(_actor, out _test); + index.TryGetValue(_actor, out var current); + _test = current; if (ImGui.ColorPicker3("TestPicker", ref _test) && _actor.Valid) - MaterialService.Test(_actor, index, _test); + _state.Materials.AddOrUpdateValue(index, new MaterialValueState(current, _test, StateSource.Manual)); + if (ImGui.ColorPicker3("TestPicker2", ref _test) && _actor.Valid) + _state.Materials.AddOrUpdateValue(index, new MaterialValueState(current, _test, StateSource.Fixed)); + + foreach (var value in _state.Materials.Values) + { + var id = MaterialValueIndex.FromKey(value.Key); + ImGui.TextUnformatted($"{id.DrawObject} {id.SlotIndex} {id.MaterialIndex} {id.RowIndex} {id.DataIndex} "); + ImGui.SameLine(0, 0); + var game = ImGui.ColorConvertFloat4ToU32(new Vector4(value.Value.Game, 1)); + ImGuiUtil.DrawTextButton(" ", Vector2.Zero, game); + ImGui.SameLine(0, ImGui.GetStyle().ItemSpacing.X); + var model = ImGui.ColorConvertFloat4ToU32(new Vector4(value.Value.Model, 1)); + ImGuiUtil.DrawTextButton(" ", Vector2.Zero, model); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" {value.Value.Source}"); + } using var disabled = ImRaii.Disabled(transformationId != 0); if (_state.ModelData.IsHuman) diff --git a/Glamourer/Interop/Material/MaterialService.cs b/Glamourer/Interop/Material/MaterialService.cs index 91635c5..6b49d3d 100644 --- a/Glamourer/Interop/Material/MaterialService.cs +++ b/Glamourer/Interop/Material/MaterialService.cs @@ -1,44 +1,12 @@ -using Dalamud.Interface.Utility.Raii; -using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using Glamourer.Interop.Structs; -using ImGuiNET; using Lumina.Data.Files; -using OtterGui; -using OtterGui.Services; -using Penumbra.GameData.Actors; -using Penumbra.GameData.Enums; using static Penumbra.GameData.Files.MtrlFile; using Texture = FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Texture; namespace Glamourer.Interop.Material; - -public class MaterialServiceDrawer(ActorManager actors) : IService -{ - private ActorIdentifier _openIdentifier; - private uint _openSlotIndex; - - public unsafe void PopupButton(Actor actor, EquipSlot slot) - { - var slotIndex = slot.ToIndex(); - - var identifier = actor.GetIdentifier(actors); - var buttonActive = actor.Valid - && identifier.IsValid - && actor.Model.IsCharacterBase - && slotIndex < actor.Model.AsCharacterBase->SlotCount - && (actor.Model.AsCharacterBase->HasModelInSlotLoaded & (1 << (int)slotIndex)) != 0; - using var id = ImRaii.PushId((int)slot); - if (ImGuiUtil.DrawDisabledButton("Advanced", Vector2.Zero, "Open advanced window.", !buttonActive)) - { - _openIdentifier = identifier; - _openSlotIndex = slotIndex; - ImGui.OpenPopup($"Popup{slot}"); - } - } -} - public static unsafe class MaterialService { public const int TextureWidth = 4; @@ -49,7 +17,7 @@ public static unsafe class MaterialService /// The original texture that will be replaced with a new one. /// The input color table. /// Success or failure. - public static bool GenerateColorTable(Texture** original, in ColorTable colorTable) + public static bool ReplaceColorTable(Texture** original, in ColorTable colorTable) { if (original == null) return false; @@ -63,7 +31,7 @@ public static unsafe class MaterialService if (texture.IsInvalid) return false; - fixed(ColorTable* ptr = &colorTable) + fixed (ColorTable* ptr = &colorTable) { if (!texture.Texture->InitializeContents(ptr)) return false; @@ -73,6 +41,22 @@ public static unsafe class MaterialService return true; } + public static bool GenerateNewColorTable(in ColorTable colorTable, out Texture* texture) + { + var textureSize = stackalloc int[2]; + textureSize[0] = TextureWidth; + textureSize[1] = TextureHeight; + + texture = Device.Instance()->CreateTexture2D(textureSize, 1, (uint)TexFile.TextureFormat.R16G16B16A16F, + (uint)(TexFile.Attribute.TextureType2D | TexFile.Attribute.Managed | TexFile.Attribute.Immutable), 7); + if (texture == null) + return false; + + fixed (ColorTable* ptr = &colorTable) + { + return texture->InitializeContents(ptr); + } + } /// Obtain a pointer to the models pointer to a specific color table texture. /// @@ -112,19 +96,4 @@ public static unsafe class MaterialService return (ColorTable*)material->ColorTable; } - - public static void Test(Actor actor, MaterialValueIndex index, Vector3 value) - { - if (!index.TryGetColorTable(actor, out var table)) - return; - - ref var row = ref table[index.RowIndex]; - if (!index.DataIndex.SetValue(ref row, value)) - return; - - var texture = GetColorTableTexture(index.TryGetModel(actor, out var model) ? model : Model.Null, index.SlotIndex, - index.MaterialIndex); - if (texture != null) - GenerateColorTable(texture, table); - } } diff --git a/Glamourer/Interop/Material/MaterialValueManager.cs b/Glamourer/Interop/Material/MaterialValueManager.cs index 257dfc8..d4aec65 100644 --- a/Glamourer/Interop/Material/MaterialValueManager.cs +++ b/Glamourer/Interop/Material/MaterialValueManager.cs @@ -1,17 +1,27 @@ -namespace Glamourer.Interop.Material; +global using StateMaterialManager = Glamourer.Interop.Material.MaterialValueManager; +global using DesignMaterialManager = Glamourer.Interop.Material.MaterialValueManager; +using Glamourer.State; -public readonly struct MaterialValueManager + +namespace Glamourer.Interop.Material; + +public record struct MaterialValueState(Vector3 Game, Vector3 Model, StateSource Source); + +public readonly struct MaterialValueManager { - private readonly List<(uint Key, Vector3 Value)> _values = []; + private readonly List<(uint Key, T Value)> _values = []; public MaterialValueManager() { } - public bool TryGetValue(MaterialValueIndex index, out Vector3 value) + public void Clear() + => _values.Clear(); + + public bool TryGetValue(MaterialValueIndex index, out T value) { if (_values.Count == 0) { - value = Vector3.Zero; + value = default!; return false; } @@ -22,11 +32,11 @@ public readonly struct MaterialValueManager return true; } - value = Vector3.Zero; + value = default!; return false; } - public bool TryAddValue(MaterialValueIndex index, in Vector3 value) + public bool TryAddValue(MaterialValueIndex index, in T value) { var key = index.Key; var idx = Search(key); @@ -50,7 +60,7 @@ public readonly struct MaterialValueManager return true; } - public void AddOrUpdateValue(MaterialValueIndex index, in Vector3 value) + public void AddOrUpdateValue(MaterialValueIndex index, in T value) { var key = index.Key; var idx = Search(key); @@ -60,11 +70,11 @@ public readonly struct MaterialValueManager _values[idx] = (key, value); } - public bool UpdateValue(MaterialValueIndex index, in Vector3 value, out Vector3 oldValue) + public bool UpdateValue(MaterialValueIndex index, in T value, out T oldValue) { if (_values.Count == 0) { - oldValue = Vector3.Zero; + oldValue = default!; return false; } @@ -72,7 +82,7 @@ public readonly struct MaterialValueManager var idx = Search(key); if (idx < 0) { - oldValue = Vector3.Zero; + oldValue = default!; return false; } @@ -81,6 +91,9 @@ public readonly struct MaterialValueManager return true; } + public IReadOnlyList<(uint Key, T Value)> Values + => _values; + public int RemoveValues(MaterialValueIndex min, MaterialValueIndex max) { var (minIdx, maxIdx) = GetMinMax(CollectionsMarshal.AsSpan(_values), min.Key, max.Key); @@ -92,21 +105,21 @@ public readonly struct MaterialValueManager return count; } - public ReadOnlySpan<(uint key, Vector3 Value)> GetValues(MaterialValueIndex min, MaterialValueIndex max) + public ReadOnlySpan<(uint key, T Value)> GetValues(MaterialValueIndex min, MaterialValueIndex max) => Filter(CollectionsMarshal.AsSpan(_values), min, max); - public static ReadOnlySpan<(uint Key, Vector3 Value)> Filter(ReadOnlySpan<(uint Key, Vector3 Value)> values, MaterialValueIndex min, + public static ReadOnlySpan<(uint Key, T Value)> Filter(ReadOnlySpan<(uint Key, T Value)> values, MaterialValueIndex min, MaterialValueIndex max) { var (minIdx, maxIdx) = GetMinMax(values, min.Key, max.Key); - return minIdx < 0 ? [] : values[minIdx..(maxIdx - minIdx)]; + return minIdx < 0 ? [] : values[minIdx..(maxIdx - minIdx + 1)]; } /// Obtain the minimum index and maximum index for a minimum and maximum key. - private static (int MinIdx, int MaxIdx) GetMinMax(ReadOnlySpan<(uint Key, Vector3 Value)> values, uint minKey, uint maxKey) + private static (int MinIdx, int MaxIdx) GetMinMax(ReadOnlySpan<(uint Key, T Value)> values, uint minKey, uint maxKey) { // Find the minimum index by binary search. - var idx = values.BinarySearch((minKey, Vector3.Zero), Comparer.Instance); + var idx = values.BinarySearch((minKey, default!), Comparer.Instance); var minIdx = idx; // If the key does not exist, check if it is an invalid range or set it correctly. @@ -131,7 +144,7 @@ public readonly struct MaterialValueManager // Do pretty much the same but in the other direction with the maximum key. - var maxIdx = values[idx..].BinarySearch((maxKey, Vector3.Zero), Comparer.Instance); + var maxIdx = values[idx..].BinarySearch((maxKey, default!), Comparer.Instance); if (maxIdx < 0) { maxIdx = ~maxIdx; @@ -149,14 +162,14 @@ public readonly struct MaterialValueManager [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private int Search(uint key) - => _values.BinarySearch((key, Vector3.Zero), Comparer.Instance); + => _values.BinarySearch((key, default!), Comparer.Instance); - private class Comparer : IComparer<(uint Key, Vector3 Value)> + private class Comparer : IComparer<(uint Key, T Value)> { public static readonly Comparer Instance = new(); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public int Compare((uint Key, Vector3 Value) x, (uint Key, Vector3 Value) y) + int IComparer<(uint Key, T Value)>.Compare((uint Key, T Value) x, (uint Key, T Value) y) => x.Key.CompareTo(y.Key); } } diff --git a/Glamourer/Interop/Material/PrepareColorSet.cs b/Glamourer/Interop/Material/PrepareColorSet.cs index 137228e..7cfbf6c 100644 --- a/Glamourer/Interop/Material/PrepareColorSet.cs +++ b/Glamourer/Interop/Material/PrepareColorSet.cs @@ -9,7 +9,6 @@ using Glamourer.State; using OtterGui.Classes; using OtterGui.Services; using Penumbra.GameData.Actors; -using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.GameData.Structs; @@ -58,28 +57,18 @@ public sealed unsafe class PrepareColorSet return _task.Result.Original(characterBase, material, stainId); } - public static MtrlFile.ColorTable GetColorTable(CharacterBase* characterBase, MaterialResourceHandle* material, StainId stainId) + public static bool TryGetColorTable(CharacterBase* characterBase, MaterialResourceHandle* material, StainId stainId, out MtrlFile.ColorTable table) { - var table = new MtrlFile.ColorTable(); - characterBase->ReadStainingTemplate(material, stainId.Id, (Half*)(&table)); - return table; - } - - public static bool TryGetColorTable(Model model, byte slotIdx, out MtrlFile.ColorTable table) - { - var table2 = new MtrlFile.ColorTable(); - if (!model.IsCharacterBase || slotIdx < model.AsCharacterBase->SlotCount) - return false; - - var resource = (MaterialResourceHandle*)model.AsCharacterBase->Materials[slotIdx]; - var stain = model.AsCharacterBase->GetModelType() switch + if (material->ColorTable == null) { - CharacterBase.ModelType.Human => model.GetArmor(EquipSlotExtensions.ToEquipSlot(slotIdx)).Stain, - CharacterBase.ModelType.Weapon => (StainId)model.AsWeapon->ModelUnknown, - _ => (StainId)0, - }; - model.AsCharacterBase->ReadStainingTemplate(resource, stain.Id, (Half*)(&table2)); - table = table2; + table = default; + return false; + } + + var newTable = *(MtrlFile.ColorTable*)material->ColorTable; + if(stainId.Id != 0) + characterBase->ReadStainingTemplate(material, stainId.Id, (Half*)(&newTable)); + table = newTable; return true; } } @@ -111,10 +100,6 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable var actor = _penumbra.GameObjectFromDrawObject(characterBase); var validType = FindType(characterBase, actor, out var type); var (slotId, materialId) = FindMaterial(characterBase, material); - Glamourer.Log.Information( - $" Triggered with 0x{(nint)characterBase:X} 0x{(nint)material:X} {stain.Id} --- Actor: 0x{actor.Address:X} Slot: {slotId} Material: {materialId} DrawObject: {type}."); - var table = PrepareColorSet.GetColorTable(characterBase, material, stain); - Glamourer.Log.Information($"{table[15].Diffuse}"); if (!validType || slotId == byte.MaxValue @@ -122,12 +107,49 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable || !_stateManager.TryGetValue(identifier, out var state)) return; + var min = MaterialValueIndex.Min(type, slotId, materialId); var max = MaterialValueIndex.Max(type, slotId, materialId); - var manager = new MaterialValueManager(); - var values = manager.GetValues(min, max); - foreach (var (key, value) in values) - ; + var values = state.Materials.GetValues(min, max); + if (values.Length == 0) + return; + + if (!PrepareColorSet.TryGetColorTable(characterBase, material, stain, out var baseColorSet)) + return; + + for (var i = 0; i < values.Length; ++i) + { + var idx = MaterialValueIndex.FromKey(values[i].key); + var (oldGame, model, source) = values[i].Value; + ref var row = ref baseColorSet[idx.RowIndex]; + if (!idx.DataIndex.TryGetValue(row, out var newGame)) + continue; + + if (newGame == oldGame) + { + idx.DataIndex.SetValue(ref row, model); + } + else + { + switch (source) + { + case StateSource.Manual: + case StateSource.Pending: + state.Materials.RemoveValue(idx); + --i; + break; + case StateSource.Ipc: + case StateSource.Fixed: + idx.DataIndex.SetValue(ref row, model); + state.Materials.UpdateValue(idx, new MaterialValueState(newGame, model, source), out _); + break; + + } + } + } + + if (MaterialService.GenerateNewColorTable(baseColorSet, out var texture)) + ret = (nint)texture; } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] diff --git a/Glamourer/State/ActorState.cs b/Glamourer/State/ActorState.cs index b5126da..5d582f6 100644 --- a/Glamourer/State/ActorState.cs +++ b/Glamourer/State/ActorState.cs @@ -30,6 +30,9 @@ public class ActorState /// The territory the draw object was created last. public ushort LastTerritory; + /// State for specific material values. + public readonly StateMaterialManager Materials = new(); + /// Whether the State is locked at all. public bool IsLocked => Combination != 0; @@ -84,4 +87,4 @@ public class ActorState LastTerritory = territory; return true; } -} \ No newline at end of file +} diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs index 2643271..c6658c5 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -240,6 +240,8 @@ public sealed class StateManager( foreach (var flag in CustomizeParameterExtensions.AllFlags) state.Sources[flag] = StateSource.Game; + state.Materials.Clear(); + var actors = ActorData.Invalid; if (source is StateSource.Manual or StateSource.Ipc) actors = Applier.ApplyAll(state, redraw, true);