Change mousewheel to ctrl, current material state.

This commit is contained in:
Ottermandias 2024-02-06 16:42:43 +01:00
parent 42ac507b86
commit b5b9289dc2
20 changed files with 537 additions and 296 deletions

View file

@ -72,6 +72,7 @@ public sealed class Design : DesignBase, ISavable
["Equipment"] = SerializeEquipment(), ["Equipment"] = SerializeEquipment(),
["Customize"] = SerializeCustomize(), ["Customize"] = SerializeCustomize(),
["Parameters"] = SerializeParameters(), ["Parameters"] = SerializeParameters(),
["Materials"] = SerializeMaterials(),
["Mods"] = SerializeMods(), ["Mods"] = SerializeMods(),
["Links"] = Links.Serialize(), ["Links"] = Links.Serialize(),
}; };
@ -136,6 +137,7 @@ public sealed class Design : DesignBase, ISavable
LoadEquip(items, json["Equipment"], design, design.Name, true); LoadEquip(items, json["Equipment"], design, design.Name, true);
LoadMods(json["Mods"], design); LoadMods(json["Mods"], design);
LoadParameters(json["Parameters"], design, design.Name); LoadParameters(json["Parameters"], design, design.Name);
LoadMaterials(json["Materials"], design, design.Name);
LoadLinks(linkLoader, json["Links"], design); LoadLinks(linkLoader, json["Links"], design);
design.Color = json["Color"]?.ToObject<string>() ?? string.Empty; design.Color = json["Color"]?.ToObject<string>() ?? string.Empty;
return design; return design;

View file

@ -14,7 +14,7 @@ public class DesignBase
{ {
public const int FileVersion = 1; public const int FileVersion = 1;
private DesignData _designData = new(); private DesignData _designData = new();
private readonly DesignMaterialManager _materials = new(); private readonly DesignMaterialManager _materials = new();
/// <summary> For read-only information about custom material color changes. </summary> /// <summary> For read-only information about custom material color changes. </summary>
@ -86,9 +86,9 @@ public class DesignBase
internal CustomizeFlag ApplyCustomizeRaw internal CustomizeFlag ApplyCustomizeRaw
=> _applyCustomize; => _applyCustomize;
internal EquipFlag ApplyEquip = EquipFlagExtensions.All; internal EquipFlag ApplyEquip = EquipFlagExtensions.All;
internal CrestFlag ApplyCrest = CrestExtensions.AllRelevant; internal CrestFlag ApplyCrest = CrestExtensions.AllRelevant;
internal MetaFlag ApplyMeta = MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState; internal MetaFlag ApplyMeta = MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState;
private bool _writeProtected; private bool _writeProtected;
public bool SetCustomize(CustomizeService customizeService, CustomizeArray customize) public bool SetCustomize(CustomizeService customizeService, CustomizeArray customize)
@ -124,7 +124,6 @@ public class DesignBase
_writeProtected = value; _writeProtected = value;
return true; return true;
} }
public bool DoApplyEquip(EquipSlot slot) public bool DoApplyEquip(EquipSlot slot)
@ -244,6 +243,7 @@ public class DesignBase
["Equipment"] = SerializeEquipment(), ["Equipment"] = SerializeEquipment(),
["Customize"] = SerializeCustomize(), ["Customize"] = SerializeCustomize(),
["Parameters"] = SerializeParameters(), ["Parameters"] = SerializeParameters(),
["Materials"] = SerializeMaterials(),
}; };
return ret; return ret;
} }
@ -362,6 +362,45 @@ public class DesignBase
return ret; return ret;
} }
protected JObject SerializeMaterials()
{
var ret = new JObject();
foreach (var (key, value) in Materials)
ret[key.ToString("X16")] = JToken.FromObject(value);
return ret;
}
protected static void LoadMaterials(JToken? materials, DesignBase design, string name)
{
if (materials is not JObject obj)
return;
design.GetMaterialDataRef().Clear();
foreach (var (key, value) in obj.Properties().Zip(obj.PropertyValues()))
{
try
{
var k = uint.Parse(key.Name, NumberStyles.HexNumber);
var v = value.ToObject<MaterialValueDesign>();
if (!MaterialValueIndex.FromKey(k, out var idx))
{
Glamourer.Messager.NotificationMessage($"Invalid material value key {k} for design {name}, skipped.",
NotificationType.Warning);
continue;
}
if (!design.GetMaterialDataRef().TryAddValue(MaterialValueIndex.FromKey(k), v))
Glamourer.Messager.NotificationMessage($"Duplicate material value key {k} for design {name}, skipped.",
NotificationType.Warning);
}
catch (Exception ex)
{
Glamourer.Messager.NotificationMessage(ex, $"Error parsing material value for design {name}, skipped",
NotificationType.Warning);
}
}
}
#endregion #endregion
#region Deserialization #region Deserialization
@ -382,6 +421,7 @@ public class DesignBase
LoadCustomize(customizations, json["Customize"], ret, "Temporary Design", false, true); LoadCustomize(customizations, json["Customize"], ret, "Temporary Design", false, true);
LoadEquip(items, json["Equipment"], ret, "Temporary Design", true); LoadEquip(items, json["Equipment"], ret, "Temporary Design", true);
LoadParameters(json["Parameters"], ret, "Temporary Design"); LoadParameters(json["Parameters"], ret, "Temporary Design");
LoadMaterials(json["Materials"], ret, "Temporary Design");
return ret; return ret;
} }

View file

@ -1,4 +1,5 @@
using Glamourer.Designs.Links; using Glamourer.Designs.Links;
using Glamourer.Interop.Material;
using Glamourer.Services; using Glamourer.Services;
using Glamourer.State; using Glamourer.State;
using Glamourer.Utility; using Glamourer.Utility;
@ -6,6 +7,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Penumbra.GameData.DataContainers; using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
namespace Glamourer.Designs; namespace Glamourer.Designs;
@ -38,22 +40,23 @@ public class DesignConverter(
=> ShareBase64(ShareJObject(design)); => ShareBase64(ShareJObject(design));
public string ShareBase64(ActorState state, in ApplicationRules rules) public string ShareBase64(ActorState state, in ApplicationRules rules)
=> ShareBase64(state.ModelData, rules); => ShareBase64(state.ModelData, state.Materials, rules);
public string ShareBase64(in DesignData data, in ApplicationRules rules) public string ShareBase64(in DesignData data, in StateMaterialManager materials, in ApplicationRules rules)
{ {
var design = Convert(data, rules); var design = Convert(data, materials, rules);
return ShareBase64(ShareJObject(design)); return ShareBase64(ShareJObject(design));
} }
public DesignBase Convert(ActorState state, in ApplicationRules rules) public DesignBase Convert(ActorState state, in ApplicationRules rules)
=> Convert(state.ModelData, rules); => Convert(state.ModelData, state.Materials, rules);
public DesignBase Convert(in DesignData data, in ApplicationRules rules) public DesignBase Convert(in DesignData data, in StateMaterialManager materials, in ApplicationRules rules)
{ {
var design = _designs.CreateTemporary(); var design = _designs.CreateTemporary();
rules.Apply(design); rules.Apply(design);
design.SetDesignData(_customize, data); design.SetDesignData(_customize, data);
ComputeMaterials(design.GetMaterialDataRef(), materials, rules.Equip);
return design; return design;
} }
@ -181,4 +184,29 @@ public class DesignConverter(
yield return (EquipSlot.OffHand, oh, offhand.Stain); yield return (EquipSlot.OffHand, oh, offhand.Stain);
} }
private static void ComputeMaterials(DesignMaterialManager manager, in StateMaterialManager materials,
EquipFlag equipFlags = EquipFlagExtensions.All)
{
foreach (var (key, value) in materials.Values)
{
var idx = MaterialValueIndex.FromKey(key);
if (idx.RowIndex >= MtrlFile.ColorTable.NumRows)
continue;
if (idx.MaterialIndex >= MaterialService.MaterialsPerModel)
continue;
var slot = idx.DrawObject switch
{
MaterialValueIndex.DrawObjectType.Human => idx.SlotIndex < 10 ? ((uint)idx.SlotIndex).ToEquipSlot() : EquipSlot.Unknown,
MaterialValueIndex.DrawObjectType.Mainhand when idx.SlotIndex == 0 => EquipSlot.MainHand,
MaterialValueIndex.DrawObjectType.Offhand when idx.SlotIndex == 0 => EquipSlot.OffHand,
_ => EquipSlot.Unknown,
};
if (slot is EquipSlot.Unknown || (slot.ToBothFlags() & equipFlags) == 0)
continue;
manager.AddOrUpdateValue(idx, value.Convert());
}
}
} }

View file

@ -1,6 +1,7 @@
using Glamourer.Designs.Links; using Glamourer.Designs.Links;
using Glamourer.Events; using Glamourer.Events;
using Glamourer.GameData; using Glamourer.GameData;
using Glamourer.Interop.Material;
using Glamourer.Services; using Glamourer.Services;
using Glamourer.State; using Glamourer.State;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
@ -250,6 +251,14 @@ public class DesignEditor(
foreach (var parameter in CustomizeParameterExtensions.AllFlags.Where(other.DoApplyParameter)) foreach (var parameter in CustomizeParameterExtensions.AllFlags.Where(other.DoApplyParameter))
ChangeCustomizeParameter(design, parameter, other.DesignData.Parameters[parameter]); ChangeCustomizeParameter(design, parameter, other.DesignData.Parameters[parameter]);
foreach (var (key, value) in other.Materials)
{
if (!value.Enabled)
continue;
design.GetMaterialDataRef().AddOrUpdateValue(MaterialValueIndex.FromKey(key), value);
}
} }
/// <summary> Change a mainhand weapon and either fix or apply appropriate offhand and potentially gauntlets. </summary> /// <summary> Change a mainhand weapon and either fix or apply appropriate offhand and potentially gauntlets. </summary>

View file

@ -62,4 +62,11 @@ public static class VectorExtensions
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
public static bool NearEqual(this CustomizeParameterValue lhs, CustomizeParameterValue rhs, float eps = 1e-9f) public static bool NearEqual(this CustomizeParameterValue lhs, CustomizeParameterValue rhs, float eps = 1e-9f)
=> NearEqual(lhs.InternalQuadruple, rhs.InternalQuadruple, eps); => NearEqual(lhs.InternalQuadruple, rhs.InternalQuadruple, eps);
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
public static bool NearEqual(this float lhs, float rhs, float eps = 1e-5f)
{
var diff = lhs - rhs;
return diff < 0 ? diff > -eps : diff < eps;
}
} }

View file

@ -207,7 +207,7 @@ public partial class CustomizationDrawer
private static bool CaptureMouseWheel(ref int value, int offset, int cap) private static bool CaptureMouseWheel(ref int value, int offset, int cap)
{ {
if (!ImGui.IsItemHovered()) if (!ImGui.IsItemHovered() || !ImGui.GetIO().KeyCtrl)
return false; return false;
ImGuiInternal.ItemSetUsingMouseWheel(); ImGuiInternal.ItemSetUsingMouseWheel();

View file

@ -10,7 +10,7 @@ using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Equipment; namespace Glamourer.Gui.Equipment;
public sealed class GlamourerColorCombo(float _comboWidth, DictStain _stains, FavoriteManager _favorites) public sealed class GlamourerColorCombo(float _comboWidth, DictStain _stains, FavoriteManager _favorites)
: FilterComboColors(_comboWidth, MouseWheelType.Unmodified, CreateFunc(_stains, _favorites), Glamourer.Log) : FilterComboColors(_comboWidth, MouseWheelType.Control, CreateFunc(_stains, _favorites), Glamourer.Log)
{ {
protected override bool DrawSelectable(int globalIdx, bool selected) protected override bool DrawSelectable(int globalIdx, bool selected)
{ {

View file

@ -24,7 +24,7 @@ public sealed class ItemCombo : FilterComboCache<EquipItem>
public Variant CustomVariant { get; private set; } public Variant CustomVariant { get; private set; }
public ItemCombo(IDataManager gameData, ItemManager items, EquipSlot slot, Logger log, FavoriteManager favorites) public ItemCombo(IDataManager gameData, ItemManager items, EquipSlot slot, Logger log, FavoriteManager favorites)
: base(() => GetItems(favorites, items, slot), MouseWheelType.Unmodified, log) : base(() => GetItems(favorites, items, slot), MouseWheelType.Control, log)
{ {
_favorites = favorites; _favorites = favorites;
Label = GetLabel(gameData, slot); Label = GetLabel(gameData, slot);

View file

@ -17,7 +17,7 @@ public sealed class WeaponCombo : FilterComboCache<EquipItem>
private float _innerWidth; private float _innerWidth;
public WeaponCombo(ItemManager items, FullEquipType type, Logger log) public WeaponCombo(ItemManager items, FullEquipType type, Logger log)
: base(() => GetWeapons(items, type), MouseWheelType.Unmodified, log) : base(() => GetWeapons(items, type), MouseWheelType.Control, log)
{ {
Label = GetLabel(type); Label = GetLabel(type);
SearchByParts = true; SearchByParts = true;

View file

@ -1,18 +1,20 @@
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Designs; using Glamourer.Designs;
using Glamourer.Interop.Material; using Glamourer.Interop.Material;
using Glamourer.Interop.Structs; using Glamourer.Interop.Structs;
using Glamourer.State; using Glamourer.State;
using ImGuiNET; using ImGuiNET;
using OtterGui;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Materials; namespace Glamourer.Gui.Materials;
public unsafe class MaterialDrawer(StateManager _stateManager) : IService public unsafe class MaterialDrawer(StateManager _stateManager, DesignManager _designManager) : IService
{ {
private static readonly IReadOnlyList<MaterialValueIndex.DrawObjectType> Types = private static readonly IReadOnlyList<MaterialValueIndex.DrawObjectType> Types =
[ [
@ -23,135 +25,94 @@ public unsafe class MaterialDrawer(StateManager _stateManager) : IService
private ActorState? _state; private ActorState? _state;
public void DrawPanel(Actor actor) public void DrawActorPanel(Actor actor)
{ {
if (!actor.IsCharacter || !_stateManager.GetOrCreate(actor, out _state)) if (!actor.IsCharacter || !_stateManager.GetOrCreate(actor, out _state))
return; return;
foreach (var type in Types) var model = actor.Model;
{ if (!model.IsHuman)
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; return;
var names = model.AsCharacterBase->GetModelType() is CharacterBase.ModelType.Human if (model.AsCharacterBase->SlotCount < 10)
? 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<string> names, MaterialValueIndex sourceIndex)
{
using var tree = ImRaii.TreeNode(names[sourceIndex.SlotIndex]);
if (!tree)
return; return;
for (byte i = 0; i < MaterialService.MaterialsPerModel; ++i) // Humans should have at least 10 slots for the equipment types. Technically more.
foreach (var (slot, idx) in EquipSlotExtensions.EqdpSlots.WithIndex())
{ {
var index = sourceIndex with { MaterialIndex = i }; var item = model.GetArmor(slot).ToWeapon(0);
var texture = model.AsCharacterBase->ColorTableTextures + index.SlotIndex * MaterialService.MaterialsPerModel + i; DrawSlotMaterials(model, slot.ToName(), item, new MaterialValueIndex(MaterialValueIndex.DrawObjectType.Human, (byte) idx, 0, 0));
}
var (mainhand, offhand, mh, oh) = actor.Model.GetWeapons(actor);
if (mainhand.IsWeapon && mainhand.AsCharacterBase->SlotCount > 0)
DrawSlotMaterials(mainhand, EquipSlot.MainHand.ToName(), mh, new MaterialValueIndex(MaterialValueIndex.DrawObjectType.Mainhand, 0, 0, 0));
if (offhand.IsWeapon && offhand.AsCharacterBase->SlotCount > 0)
DrawSlotMaterials(offhand, EquipSlot.OffHand.ToName(), oh, new MaterialValueIndex(MaterialValueIndex.DrawObjectType.Offhand, 0, 0, 0));
}
private void DrawSlotMaterials(Model model, string name, CharacterWeapon drawData, MaterialValueIndex index)
{
var drawnMaterial = 1;
for (byte materialIndex = 0; materialIndex < MaterialService.MaterialsPerModel; ++materialIndex)
{
var texture = model.AsCharacterBase->ColorTableTextures + index.SlotIndex * MaterialService.MaterialsPerModel + materialIndex;
if (*texture == null) if (*texture == null)
continue; continue;
if (!DirectXTextureHelper.TryGetColorTable(*texture, out var table)) if (!DirectXTextureHelper.TryGetColorTable(*texture, out var table))
continue; continue;
DrawMaterial(ref table, index); using var tree = ImRaii.TreeNode($"{name} Material #{drawnMaterial++}###{name}{materialIndex}");
if (!tree)
continue;
DrawMaterial(ref table, drawData, index with { MaterialIndex = materialIndex} );
} }
} }
private void DrawMaterial(ref MtrlFile.ColorTable table, MaterialValueIndex sourceIndex) private void DrawMaterial(ref MtrlFile.ColorTable table, CharacterWeapon drawData, MaterialValueIndex sourceIndex)
{ {
using var tree = ImRaii.TreeNode($"Material {sourceIndex.MaterialIndex + 1}");
if (!tree)
return;
for (byte i = 0; i < MtrlFile.ColorTable.NumRows; ++i) for (byte i = 0; i < MtrlFile.ColorTable.NumRows; ++i)
{ {
var index = sourceIndex with { RowIndex = i }; var index = sourceIndex with { RowIndex = i };
ref var row = ref table[i]; ref var row = ref table[i];
DrawRow(ref row, index); DrawRow(ref row, drawData, index);
} }
} }
private void DrawRow(ref MtrlFile.ColorTable.Row row, MaterialValueIndex sourceIndex) private void DrawRow(ref MtrlFile.ColorTable.Row row, CharacterWeapon drawData, MaterialValueIndex index)
{ {
var r = _state!.Materials.GetValues( using var id = ImRaii.PushId(index.RowIndex);
MaterialValueIndex.Min(sourceIndex.DrawObject, sourceIndex.SlotIndex, sourceIndex.MaterialIndex, sourceIndex.RowIndex), var changed = _state!.Materials.TryGetValue(index, out var value);
MaterialValueIndex.Max(sourceIndex.DrawObject, sourceIndex.SlotIndex, sourceIndex.MaterialIndex, sourceIndex.RowIndex)); if (!changed)
var highlightColor = ColorId.FavoriteStarOn.Value();
using var id = ImRaii.PushId(sourceIndex.RowIndex);
var index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.Diffuse };
var (diffuse, diffuseGame, changed) = MaterialValueManager.GetSpecific(r, index, out var d)
? (d.Model, d.Game, true)
: (row.Diffuse, row.Diffuse, false);
using (ImRaii.PushColor(ImGuiCol.Text, highlightColor, changed))
{ {
if (ImGui.ColorEdit3("Diffuse", ref diffuse, ImGuiColorEditFlags.NoInputs)) var internalRow = new ColorRow(row);
_stateManager.ChangeMaterialValue(_state!, index, diffuse, diffuseGame, ApplySettings.Manual); value = new MaterialValueState(internalRow, internalRow, drawData, StateSource.Manual);
} }
var applied = ImGui.ColorEdit3("Diffuse", ref value.Model.Diffuse, ImGuiColorEditFlags.NoInputs);
index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.Specular };
(var specular, var specularGame, changed) = MaterialValueManager.GetSpecific(r, index, out var s)
? (s.Model, s.Game, true)
: (row.Specular, row.Specular, false);
ImGui.SameLine(); ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, highlightColor, changed)) applied |= ImGui.ColorEdit3("Specular", ref value.Model.Specular, ImGuiColorEditFlags.NoInputs);
{
if (ImGui.ColorEdit3("Specular", ref specular, ImGuiColorEditFlags.NoInputs))
_stateManager.ChangeMaterialValue(_state!, index, specular, specularGame, ApplySettings.Manual);
}
index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.Emissive };
(var emissive, var emissiveGame, changed) = MaterialValueManager.GetSpecific(r, index, out var e)
? (e.Model, e.Game, true)
: (row.Emissive, row.Emissive, false);
ImGui.SameLine(); ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, highlightColor, changed)) applied |= ImGui.ColorEdit3("Emissive", ref value.Model.Emissive, ImGuiColorEditFlags.NoInputs);
{
if (ImGui.ColorEdit3("Emissive", ref emissive, ImGuiColorEditFlags.NoInputs))
_stateManager.ChangeMaterialValue(_state!, index, emissive, emissiveGame, ApplySettings.Manual);
}
index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.GlossStrength };
(var glossStrength, var glossStrengthGame, changed) = MaterialValueManager.GetSpecific(r, index, out var g)
? (g.Model.X, g.Game.X, true)
: (row.GlossStrength, row.GlossStrength, false);
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
using (ImRaii.PushColor(ImGuiCol.Text, highlightColor, changed)) applied |= ImGui.DragFloat("Gloss", ref value.Model.GlossStrength, 0.1f);
{
if (ImGui.DragFloat("Gloss", ref glossStrength, 0.1f))
_stateManager.ChangeMaterialValue(_state!, index, new Vector3(glossStrength), new Vector3(glossStrengthGame),
ApplySettings.Manual);
}
index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.SpecularStrength };
(var specularStrength, var specularStrengthGame, changed) = MaterialValueManager.GetSpecific(r, index, out var ss)
? (ss.Model.X, ss.Game.X, true)
: (row.SpecularStrength, row.SpecularStrength, false);
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
using (ImRaii.PushColor(ImGuiCol.Text, highlightColor, changed)) applied |= ImGui.DragFloat("Specular Strength", ref value.Model.SpecularStrength, 0.1f);
if (applied)
_stateManager.ChangeMaterialValue(_state!, index, value, ApplySettings.Manual);
if (changed)
{ {
if (ImGui.DragFloat("Specular Strength", ref specularStrength, 0.1f)) ImGui.SameLine();
_stateManager.ChangeMaterialValue(_state!, index, new Vector3(specularStrength), new Vector3(specularStrengthGame), using (ImRaii.PushFont(UiBuilder.IconFont))
ApplySettings.Manual); {
using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FavoriteStarOn.Value());
ImGui.TextUnformatted(FontAwesomeIcon.UserEdit.ToIconString());
}
} }
} }

View file

@ -119,7 +119,7 @@ public class ActorPanel(
if (ImGui.CollapsingHeader("Material Shit")) if (ImGui.CollapsingHeader("Material Shit"))
_materialDrawer.DrawPanel(_actor); _materialDrawer.DrawActorPanel(_actor);
using var disabled = ImRaii.Disabled(transformationId != 0); using var disabled = ImRaii.Disabled(transformationId != 0);
if (_state.ModelData.IsHuman) if (_state.ModelData.IsHuman)
DrawHumanPanel(); DrawHumanPanel();

View file

@ -10,7 +10,7 @@ public sealed class DesignColorCombo(DesignColors _designColors, bool _skipAutom
FilterComboCache<string>(_skipAutomatic FilterComboCache<string>(_skipAutomatic
? _designColors.Keys.OrderBy(k => k) ? _designColors.Keys.OrderBy(k => k)
: _designColors.Keys.OrderBy(k => k).Prepend(DesignColors.AutomaticName), : _designColors.Keys.OrderBy(k => k).Prepend(DesignColors.AutomaticName),
MouseWheelType.Shift, Glamourer.Log) MouseWheelType.Control, Glamourer.Log)
{ {
protected override void OnMouseWheel(string preview, ref int current, int steps) protected override void OnMouseWheel(string preview, ref int current, int steps)
{ {

View file

@ -174,6 +174,25 @@ public class DesignPanel(
_parameterDrawer.Draw(_manager, _selector.Selected!); _parameterDrawer.Draw(_manager, _selector.Selected!);
} }
private void DrawMaterialValues()
{
if (!_config.UseAdvancedParameters)
return;
using var h = ImRaii.CollapsingHeader("Advanced Dyes");
if (!h)
return;
foreach (var ((key, value), i) in _selector.Selected!.Materials.WithIndex())
{
using var id = ImRaii.PushId(i);
ImGui.TextUnformatted($"{key:X16}");
ImGui.SameLine();
var enabled = value.Enabled;
ImGui.Checkbox("Enabled", ref enabled);
}
}
private void DrawCustomizeApplication() private void DrawCustomizeApplication()
{ {
using var id = ImRaii.PushId("Customizations"); using var id = ImRaii.PushId("Customizations");
@ -365,6 +384,7 @@ public class DesignPanel(
DrawCustomize(); DrawCustomize();
DrawEquipment(); DrawEquipment();
DrawCustomizeParameters(); DrawCustomizeParameters();
DrawMaterialValues();
_designDetails.Draw(); _designDetails.Draw();
DrawApplicationRules(); DrawApplicationRules();
_modAssociations.Draw(); _modAssociations.Draw();

View file

@ -85,7 +85,7 @@ public class NpcPanel(
try try
{ {
var data = ToDesignData(); var data = ToDesignData();
var text = _converter.ShareBase64(data, ApplicationRules.NpcFromModifiers()); var text = _converter.ShareBase64(data, new StateMaterialManager(), ApplicationRules.NpcFromModifiers());
ImGui.SetClipboardText(text); ImGui.SetClipboardText(text);
} }
catch (Exception ex) catch (Exception ex)
@ -101,7 +101,7 @@ public class NpcPanel(
ImGui.OpenPopup("Save as Design"); ImGui.OpenPopup("Save as Design");
_newName = _selector.Selection.Name; _newName = _selector.Selection.Name;
var data = ToDesignData(); var data = ToDesignData();
_newDesign = _converter.Convert(data, ApplicationRules.NpcFromModifiers()); _newDesign = _converter.Convert(data, new StateMaterialManager(), ApplicationRules.NpcFromModifiers());
} }
private void SaveDesignDrawPopup() private void SaveDesignDrawPopup()
@ -195,7 +195,7 @@ public class NpcPanel(
if (_state.GetOrCreate(id, data.Objects[0], out var state)) if (_state.GetOrCreate(id, data.Objects[0], out var state))
{ {
var design = _converter.Convert(ToDesignData(), ApplicationRules.NpcFromModifiers()); var design = _converter.Convert(ToDesignData(), new StateMaterialManager(), ApplicationRules.NpcFromModifiers());
_state.ApplyDesign(state, design, ApplySettings.Manual); _state.ApplyDesign(state, design, ApplySettings.Manual);
} }
} }
@ -213,7 +213,7 @@ public class NpcPanel(
if (_state.GetOrCreate(id, data.Objects[0], out var state)) if (_state.GetOrCreate(id, data.Objects[0], out var state))
{ {
var design = _converter.Convert(ToDesignData(), ApplicationRules.NpcFromModifiers()); var design = _converter.Convert(ToDesignData(), new StateMaterialManager(), ApplicationRules.NpcFromModifiers());
_state.ApplyDesign(state, design, ApplySettings.Manual); _state.ApplyDesign(state, design, ApplySettings.Manual);
} }
} }
@ -247,7 +247,9 @@ public class NpcPanel(
var colorName = color.Length == 0 ? DesignColors.AutomaticName : color; var colorName = color.Length == 0 ? DesignColors.AutomaticName : color;
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (_colorCombo.Draw("##colorCombo", colorName, if (_colorCombo.Draw("##colorCombo", colorName,
"Associate a color with this NPC appearance. Right-Click to revert to automatic coloring.", "Associate a color with this NPC appearance.\n"
+ "Right-Click to revert to automatic coloring.\n"
+ "Hold Control and scroll the mousewheel to scroll.",
width - ImGui.GetStyle().ItemSpacing.X - ImGui.GetFrameHeight(), ImGui.GetTextLineHeight()) width - ImGui.GetStyle().ItemSpacing.X - ImGui.GetFrameHeight(), ImGui.GetTextLineHeight())
&& _colorCombo.CurrentSelection != null) && _colorCombo.CurrentSelection != null)
{ {

View file

@ -6,6 +6,8 @@ using Glamourer.Interop.Structs;
using Glamourer.State; using Glamourer.State;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.GameData.Actors; using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
namespace Glamourer.Interop.Material; namespace Glamourer.Interop.Material;
@ -19,6 +21,8 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable
private int _lastSlot; private int _lastSlot;
private readonly ThreadLocal<List<MaterialValueIndex>> _deleteList = new(() => []);
public MaterialManager(PrepareColorSet prepareColorSet, StateManager stateManager, ActorManager actors, PenumbraService penumbra) public MaterialManager(PrepareColorSet prepareColorSet, StateManager stateManager, ActorManager actors, PenumbraService penumbra)
{ {
_stateManager = stateManager; _stateManager = stateManager;
@ -39,7 +43,8 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable
var (slotId, materialId) = FindMaterial(characterBase, material); var (slotId, materialId) = FindMaterial(characterBase, material);
if (!validType if (!validType
|| slotId == byte.MaxValue || slotId > 9
|| type is not MaterialValueIndex.DrawObjectType.Human && slotId > 0
|| !actor.Identifier(_actors, out var identifier) || !actor.Identifier(_actors, out var identifier)
|| !_stateManager.TryGetValue(identifier, out var state)) || !_stateManager.TryGetValue(identifier, out var state))
return; return;
@ -53,38 +58,60 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable
if (!PrepareColorSet.TryGetColorTable(characterBase, material, stain, out var baseColorSet)) if (!PrepareColorSet.TryGetColorTable(characterBase, material, stain, out var baseColorSet))
return; return;
for (var i = 0; i < values.Length; ++i) var drawData = type switch
{ {
var idx = MaterialValueIndex.FromKey(values[i].key); MaterialValueIndex.DrawObjectType.Human => GetTempSlot((Human*)characterBase, slotId),
var (oldGame, model, source) = values[i].Value; _ => GetTempSlot((Weapon*)characterBase),
ref var row = ref baseColorSet[idx.RowIndex]; };
if (!idx.DataIndex.TryGetValue(row, out var newGame)) UpdateMaterialValues(state, values, drawData, ref baseColorSet);
continue;
if (newGame == oldGame)
{
idx.DataIndex.SetValue(ref row, model);
}
else
{
switch (source.Base())
{
case StateSource.Manual:
_stateManager.ChangeMaterialValue(state, idx, Vector3.Zero, Vector3.Zero, ApplySettings.Game);
--i;
break;
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)) if (MaterialService.GenerateNewColorTable(baseColorSet, out var texture))
ret = (nint)texture; ret = (nint)texture;
} }
/// <summary> Update and apply the glamourer state of an actor according to the application sources when updated by the game. </summary>
private void UpdateMaterialValues(ActorState state, ReadOnlySpan<(uint Key, MaterialValueState Value)> values, CharacterWeapon drawData,
ref MtrlFile.ColorTable colorTable)
{
var deleteList = _deleteList.Value!;
deleteList.Clear();
for (var i = 0; i < values.Length; ++i)
{
var idx = MaterialValueIndex.FromKey(values[i].Key);
var materialValue = values[i].Value;
ref var row = ref colorTable[idx.RowIndex];
var newGame = new ColorRow(row);
if (materialValue.EqualGame(newGame, drawData))
materialValue.Model.Apply(ref row);
else
switch (materialValue.Source)
{
case StateSource.Pending:
materialValue.Model.Apply(ref row);
state.Materials.UpdateValue(idx, new MaterialValueState(newGame, materialValue.Model, drawData, StateSource.Manual),
out _);
break;
case StateSource.IpcManual:
case StateSource.Manual:
deleteList.Add(idx);
break;
case StateSource.Fixed:
case StateSource.IpcFixed:
materialValue.Model.Apply(ref row);
state.Materials.UpdateValue(idx, new MaterialValueState(newGame, materialValue.Model, drawData, materialValue.Source),
out _);
break;
}
}
foreach (var idx in deleteList)
_stateManager.ChangeMaterialValue(state, idx, default, ApplySettings.Game);
}
/// <summary>
/// Find the index of a material by searching through a draw objects pointers.
/// Tries to take shortcuts for consecutive searches like when a character is newly created.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private (byte SlotId, byte MaterialId) FindMaterial(CharacterBase* characterBase, MaterialResourceHandle* material) private (byte SlotId, byte MaterialId) FindMaterial(CharacterBase* characterBase, MaterialResourceHandle* material)
{ {
@ -119,6 +146,7 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable
return (byte.MaxValue, byte.MaxValue); return (byte.MaxValue, byte.MaxValue);
} }
/// <summary> Find the type of the given draw object by checking the actors pointers. </summary>
private static bool FindType(CharacterBase* characterBase, Actor actor, out MaterialValueIndex.DrawObjectType type) private static bool FindType(CharacterBase* characterBase, Actor actor, out MaterialValueIndex.DrawObjectType type)
{ {
type = MaterialValueIndex.DrawObjectType.Human; type = MaterialValueIndex.DrawObjectType.Human;
@ -145,4 +173,26 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable
return false; return false;
} }
/// <summary> We need to get the temporary set, variant and stain that is currently being set if it is available. </summary>
private CharacterWeapon GetTempSlot(Human* human, byte slotId)
{
if (human->ChangedEquipData == null)
return ((Model)human).GetArmor(((uint)slotId).ToEquipSlot()).ToWeapon(0);
return ((CharacterArmor*)human->ChangedEquipData + slotId * 3)->ToWeapon(0);
}
/// <summary>
/// We need to get the temporary set, variant and stain that is currently being set if it is available.
/// Weapons do not change in skeleton id without being reconstructed, so this is not changeable data.
/// </summary>
private CharacterWeapon GetTempSlot(Weapon* weapon)
{
var changedData = *(void**)((byte*)weapon + 0x918);
if (changedData == null)
return new CharacterWeapon(weapon->ModelSetId, weapon->SecondaryId, (Variant)weapon->Variant, (StainId)weapon->ModelUnknown);
return new CharacterWeapon(weapon->ModelSetId, *(SecondaryId*)changedData, ((Variant*)changedData)[2], ((StainId*)changedData)[3]);
}
} }

View file

@ -11,14 +11,13 @@ public readonly record struct MaterialValueIndex(
MaterialValueIndex.DrawObjectType DrawObject, MaterialValueIndex.DrawObjectType DrawObject,
byte SlotIndex, byte SlotIndex,
byte MaterialIndex, byte MaterialIndex,
byte RowIndex, byte RowIndex)
MaterialValueIndex.ColorTableIndex DataIndex)
{ {
public uint Key public uint Key
=> ToKey(DrawObject, SlotIndex, MaterialIndex, RowIndex, DataIndex); => ToKey(DrawObject, SlotIndex, MaterialIndex, RowIndex);
public bool Valid public bool Valid
=> Validate(DrawObject) && ValidateSlot(SlotIndex) && ValidateMaterial(MaterialIndex) && ValidateRow(RowIndex) && Validate(DataIndex); => Validate(DrawObject) && ValidateSlot(SlotIndex) && ValidateMaterial(MaterialIndex) && ValidateRow(RowIndex);
public static bool FromKey(uint key, out MaterialValueIndex index) public static bool FromKey(uint key, out MaterialValueIndex index)
{ {
@ -96,7 +95,7 @@ public readonly record struct MaterialValueIndex(
public unsafe bool TryGetColorTable(Texture** texture, out MtrlFile.ColorTable table) public unsafe bool TryGetColorTable(Texture** texture, out MtrlFile.ColorTable table)
=> DirectXTextureHelper.TryGetColorTable(*texture, out table); => DirectXTextureHelper.TryGetColorTable(*texture, out table);
public unsafe bool TryGetColorRow(Actor actor, out MtrlFile.ColorTable.Row row) public bool TryGetColorRow(Actor actor, out MtrlFile.ColorTable.Row row)
{ {
if (!TryGetColorTable(actor, out var table)) if (!TryGetColorTable(actor, out var table))
{ {
@ -108,40 +107,16 @@ public readonly record struct MaterialValueIndex(
return true; return true;
} }
public unsafe bool TryGetValue(Actor actor, out Vector3 value)
{
if (!TryGetColorRow(actor, out var row))
{
value = Vector3.Zero;
return false;
}
value = DataIndex switch
{
ColorTableIndex.Diffuse => row.Diffuse,
ColorTableIndex.Specular => row.Specular,
ColorTableIndex.SpecularStrength => new Vector3(row.SpecularStrength, 0, 0),
ColorTableIndex.Emissive => row.Emissive,
ColorTableIndex.GlossStrength => new Vector3(row.GlossStrength, 0, 0),
ColorTableIndex.TileSet => new Vector3(row.TileSet),
ColorTableIndex.MaterialRepeat => new Vector3(row.MaterialRepeat, 0),
ColorTableIndex.MaterialSkew => new Vector3(row.MaterialSkew, 0),
_ => new Vector3(float.NaN),
};
return !float.IsNaN(value.X);
}
public static MaterialValueIndex FromKey(uint key) public static MaterialValueIndex FromKey(uint key)
=> new(key); => new(key);
public static MaterialValueIndex Min(DrawObjectType drawObject = 0, byte slotIndex = 0, byte materialIndex = 0, byte rowIndex = 0, public static MaterialValueIndex Min(DrawObjectType drawObject = 0, byte slotIndex = 0, byte materialIndex = 0, byte rowIndex = 0)
ColorTableIndex dataIndex = 0) => new(drawObject, slotIndex, materialIndex, rowIndex);
=> new(drawObject, slotIndex, materialIndex, rowIndex, dataIndex);
public static MaterialValueIndex Max(DrawObjectType drawObject = (DrawObjectType)byte.MaxValue, byte slotIndex = byte.MaxValue, public static MaterialValueIndex Max(DrawObjectType drawObject = (DrawObjectType)byte.MaxValue, byte slotIndex = byte.MaxValue,
byte materialIndex = byte.MaxValue, byte rowIndex = byte.MaxValue, byte materialIndex = byte.MaxValue, byte rowIndex = byte.MaxValue)
ColorTableIndex dataIndex = (ColorTableIndex)byte.MaxValue) => new(drawObject, slotIndex, materialIndex, rowIndex);
=> new(drawObject, slotIndex, materialIndex, rowIndex, dataIndex);
public enum DrawObjectType : byte public enum DrawObjectType : byte
{ {
@ -150,18 +125,6 @@ public readonly record struct MaterialValueIndex(
Offhand, Offhand,
}; };
public enum ColorTableIndex : byte
{
Diffuse,
Specular,
SpecularStrength,
Emissive,
GlossStrength,
TileSet,
MaterialRepeat,
MaterialSkew,
}
public static bool Validate(DrawObjectType type) public static bool Validate(DrawObjectType type)
=> Enum.IsDefined(type); => Enum.IsDefined(type);
@ -174,22 +137,17 @@ public readonly record struct MaterialValueIndex(
public static bool ValidateRow(byte rowIndex) public static bool ValidateRow(byte rowIndex)
=> rowIndex < MtrlFile.ColorTable.NumRows; => rowIndex < MtrlFile.ColorTable.NumRows;
public static bool Validate(ColorTableIndex dataIndex) private static uint ToKey(DrawObjectType type, byte slotIndex, byte materialIndex, byte rowIndex)
=> Enum.IsDefined(dataIndex);
private static uint ToKey(DrawObjectType type, byte slotIndex, byte materialIndex, byte rowIndex, ColorTableIndex index)
{ {
var result = (uint)index & 0xFF; var result = (uint)rowIndex;
result |= (uint)(rowIndex & 0xFF) << 8; result |= (uint)materialIndex << 8;
result |= (uint)(materialIndex & 0xF) << 16; result |= (uint)slotIndex << 16;
result |= (uint)(slotIndex & 0xFF) << 20; result |= (uint)((byte)type << 24);
result |= (uint)((byte)type & 0xF) << 28;
return result; return result;
} }
private MaterialValueIndex(uint key) private MaterialValueIndex(uint key)
: this((DrawObjectType)((key >> 28) & 0xF), (byte)(key >> 20), (byte)((key >> 16) & 0xF), (byte)(key >> 8), : this((DrawObjectType)(key >> 24), (byte)(key >> 16), (byte)(key >> 8), (byte)key)
(ColorTableIndex)(key & 0xFF))
{ } { }
private class Converter : JsonConverter<MaterialValueIndex> private class Converter : JsonConverter<MaterialValueIndex>
@ -202,81 +160,3 @@ public readonly record struct MaterialValueIndex(
=> FromKey(serializer.Deserialize<uint>(reader), out var value) ? value : throw new Exception($"Invalid material key {value.Key}."); => FromKey(serializer.Deserialize<uint>(reader), out var value) ? value : throw new Exception($"Invalid material key {value.Key}.");
} }
} }
public static class MaterialExtensions
{
public static bool TryGetValue(this MaterialValueIndex.ColorTableIndex index, in MtrlFile.ColorTable.Row row, out Vector3 value)
{
value = index switch
{
MaterialValueIndex.ColorTableIndex.Diffuse => row.Diffuse,
MaterialValueIndex.ColorTableIndex.Specular => row.Specular,
MaterialValueIndex.ColorTableIndex.SpecularStrength => new Vector3(row.SpecularStrength, 0, 0),
MaterialValueIndex.ColorTableIndex.Emissive => row.Emissive,
MaterialValueIndex.ColorTableIndex.GlossStrength => new Vector3(row.GlossStrength, 0, 0),
MaterialValueIndex.ColorTableIndex.TileSet => new Vector3(row.TileSet),
MaterialValueIndex.ColorTableIndex.MaterialRepeat => new Vector3(row.MaterialRepeat, 0),
MaterialValueIndex.ColorTableIndex.MaterialSkew => new Vector3(row.MaterialSkew, 0),
_ => new Vector3(float.NaN),
};
return !float.IsNaN(value.X);
}
public static bool SetValue(this MaterialValueIndex.ColorTableIndex index, ref MtrlFile.ColorTable.Row row, in Vector3 value)
{
switch (index)
{
case MaterialValueIndex.ColorTableIndex.Diffuse:
if (value == row.Diffuse)
return false;
row.Diffuse = value;
return true;
case MaterialValueIndex.ColorTableIndex.Specular:
if (value == row.Specular)
return false;
row.Specular = value;
return true;
case MaterialValueIndex.ColorTableIndex.SpecularStrength:
if (value.X == row.SpecularStrength)
return false;
row.SpecularStrength = value.X;
return true;
case MaterialValueIndex.ColorTableIndex.Emissive:
if (value == row.Emissive)
return false;
row.Emissive = value;
return true;
case MaterialValueIndex.ColorTableIndex.GlossStrength:
if (value.X == row.GlossStrength)
return false;
row.GlossStrength = value.X;
return true;
case MaterialValueIndex.ColorTableIndex.TileSet:
var @ushort = (ushort)(value.X + 0.5f);
if (@ushort == row.TileSet)
return false;
row.TileSet = @ushort;
return true;
case MaterialValueIndex.ColorTableIndex.MaterialRepeat:
if (value.X == row.MaterialRepeat.X && value.Y == row.MaterialRepeat.Y)
return false;
row.MaterialRepeat = new Vector2(value.X, value.Y);
return true;
case MaterialValueIndex.ColorTableIndex.MaterialSkew:
if (value.X == row.MaterialSkew.X && value.Y == row.MaterialSkew.Y)
return false;
row.MaterialSkew = new Vector2(value.X, value.Y);
return true;
default: return false;
}
}
}

View file

@ -1,12 +1,242 @@
global using StateMaterialManager = Glamourer.Interop.Material.MaterialValueManager<Glamourer.Interop.Material.MaterialValueState>; global using StateMaterialManager = Glamourer.Interop.Material.MaterialValueManager<Glamourer.Interop.Material.MaterialValueState>;
global using DesignMaterialManager = Glamourer.Interop.Material.MaterialValueManager<Glamourer.Interop.Material.MaterialValueDesign>; global using DesignMaterialManager = Glamourer.Interop.Material.MaterialValueManager<Glamourer.Interop.Material.MaterialValueDesign>;
using Glamourer.GameData;
using Glamourer.State; using Glamourer.State;
using Penumbra.GameData.Files;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop.Material; namespace Glamourer.Interop.Material;
public record struct MaterialValueDesign(Vector3 Value, bool Enabled); [JsonConverter(typeof(Converter))]
public record struct MaterialValueState(Vector3 Game, Vector3 Model, StateSource Source); public struct ColorRow(Vector3 diffuse, Vector3 specular, Vector3 emissive, float specularStrength, float glossStrength)
{
public static readonly ColorRow Empty = new(Vector3.Zero, Vector3.Zero, Vector3.Zero, 0, 0);
public Vector3 Diffuse = diffuse;
public Vector3 Specular = specular;
public Vector3 Emissive = emissive;
public float SpecularStrength = specularStrength;
public float GlossStrength = glossStrength;
public ColorRow(in MtrlFile.ColorTable.Row row)
: this(row.Diffuse, row.Specular, row.Emissive, row.SpecularStrength, row.GlossStrength)
{ }
public readonly bool NearEqual(in ColorRow rhs)
=> Diffuse.NearEqual(rhs.Diffuse)
&& Specular.NearEqual(rhs.Specular)
&& Emissive.NearEqual(rhs.Emissive)
&& SpecularStrength.NearEqual(rhs.SpecularStrength)
&& GlossStrength.NearEqual(rhs.GlossStrength);
public readonly bool Apply(ref MtrlFile.ColorTable.Row row)
{
var ret = false;
if (!row.Diffuse.NearEqual(Diffuse))
{
row.Diffuse = Diffuse;
ret = true;
}
if (!row.Specular.NearEqual(Specular))
{
row.Specular = Specular;
ret = true;
}
if (!row.Emissive.NearEqual(Emissive))
{
row.Emissive = Emissive;
ret = true;
}
if (!row.SpecularStrength.NearEqual(SpecularStrength))
{
row.SpecularStrength = SpecularStrength;
ret = true;
}
if (!row.GlossStrength.NearEqual(GlossStrength))
{
row.GlossStrength = GlossStrength;
ret = true;
}
return ret;
}
private class Converter : JsonConverter<ColorRow>
{
public override void WriteJson(JsonWriter writer, ColorRow value, JsonSerializer serializer)
{
writer.WriteStartObject();
writer.WritePropertyName("DiffuseR");
writer.WriteValue(value.Diffuse.X);
writer.WritePropertyName("DiffuseG");
writer.WriteValue(value.Diffuse.Y);
writer.WritePropertyName("DiffuseB");
writer.WriteValue(value.Diffuse.Z);
writer.WritePropertyName("SpecularR");
writer.WriteValue(value.Specular.X);
writer.WritePropertyName("SpecularG");
writer.WriteValue(value.Specular.Y);
writer.WritePropertyName("SpecularB");
writer.WriteValue(value.Specular.Z);
writer.WritePropertyName("SpecularA");
writer.WriteValue(value.SpecularStrength);
writer.WritePropertyName("EmissiveR");
writer.WriteValue(value.Emissive.X);
writer.WritePropertyName("EmissiveG");
writer.WriteValue(value.Emissive.Y);
writer.WritePropertyName("EmissiveB");
writer.WriteValue(value.Emissive.Z);
writer.WritePropertyName("Gloss");
writer.WriteValue(value.GlossStrength);
writer.WriteEndObject();
}
public override ColorRow ReadJson(JsonReader reader, Type objectType, ColorRow existingValue, bool hasExistingValue,
JsonSerializer serializer)
{
var obj = JObject.Load(reader);
Set(ref existingValue.Diffuse.X, obj["DiffuseR"]?.Value<float>());
Set(ref existingValue.Diffuse.Y, obj["DiffuseG"]?.Value<float>());
Set(ref existingValue.Diffuse.Z, obj["DiffuseB"]?.Value<float>());
Set(ref existingValue.Specular.X, obj["SpecularR"]?.Value<float>());
Set(ref existingValue.Specular.Y, obj["SpecularG"]?.Value<float>());
Set(ref existingValue.Specular.Z, obj["SpecularB"]?.Value<float>());
Set(ref existingValue.SpecularStrength, obj["SpecularA"]?.Value<float>());
Set(ref existingValue.Emissive.X, obj["EmissiveR"]?.Value<float>());
Set(ref existingValue.Emissive.Y, obj["EmissiveG"]?.Value<float>());
Set(ref existingValue.Emissive.Z, obj["EmissiveB"]?.Value<float>());
Set(ref existingValue.GlossStrength, obj["Gloss"]?.Value<float>());
return existingValue;
static void Set<T>(ref T target, T? value)
where T : struct
{
if (value.HasValue)
target = value.Value;
}
}
}
}
[JsonConverter(typeof(Converter))]
public struct MaterialValueDesign(ColorRow value, bool enabled)
{
public ColorRow Value = value;
public bool Enabled = enabled;
public readonly bool Apply(ref MaterialValueState state)
{
if (!Enabled)
return false;
if (state.Model.NearEqual(Value))
return false;
state.Model = Value;
return true;
}
private class Converter : JsonConverter<MaterialValueDesign>
{
public override void WriteJson(JsonWriter writer, MaterialValueDesign value, JsonSerializer serializer)
{
writer.WriteStartObject();
writer.WritePropertyName("DiffuseR");
writer.WriteValue(value.Value.Diffuse.X);
writer.WritePropertyName("DiffuseG");
writer.WriteValue(value.Value.Diffuse.Y);
writer.WritePropertyName("DiffuseB");
writer.WriteValue(value.Value.Diffuse.Z);
writer.WritePropertyName("SpecularR");
writer.WriteValue(value.Value.Specular.X);
writer.WritePropertyName("SpecularG");
writer.WriteValue(value.Value.Specular.Y);
writer.WritePropertyName("SpecularB");
writer.WriteValue(value.Value.Specular.Z);
writer.WritePropertyName("SpecularA");
writer.WriteValue(value.Value.SpecularStrength);
writer.WritePropertyName("EmissiveR");
writer.WriteValue(value.Value.Emissive.X);
writer.WritePropertyName("EmissiveG");
writer.WriteValue(value.Value.Emissive.Y);
writer.WritePropertyName("EmissiveB");
writer.WriteValue(value.Value.Emissive.Z);
writer.WritePropertyName("Gloss");
writer.WriteValue(value.Value.GlossStrength);
writer.WritePropertyName("Enabled");
writer.WriteValue(value.Enabled);
writer.WriteEndObject();
}
public override MaterialValueDesign ReadJson(JsonReader reader, Type objectType, MaterialValueDesign existingValue,
bool hasExistingValue,
JsonSerializer serializer)
{
var obj = JObject.Load(reader);
Set(ref existingValue.Value.Diffuse.X, obj["DiffuseR"]?.Value<float>());
Set(ref existingValue.Value.Diffuse.Y, obj["DiffuseG"]?.Value<float>());
Set(ref existingValue.Value.Diffuse.Z, obj["DiffuseB"]?.Value<float>());
Set(ref existingValue.Value.Specular.X, obj["SpecularR"]?.Value<float>());
Set(ref existingValue.Value.Specular.Y, obj["SpecularG"]?.Value<float>());
Set(ref existingValue.Value.Specular.Z, obj["SpecularB"]?.Value<float>());
Set(ref existingValue.Value.SpecularStrength, obj["SpecularA"]?.Value<float>());
Set(ref existingValue.Value.Emissive.X, obj["EmissiveR"]?.Value<float>());
Set(ref existingValue.Value.Emissive.Y, obj["EmissiveG"]?.Value<float>());
Set(ref existingValue.Value.Emissive.Z, obj["EmissiveB"]?.Value<float>());
Set(ref existingValue.Value.GlossStrength, obj["Gloss"]?.Value<float>());
existingValue.Enabled = obj["Enabled"]?.Value<bool>() ?? false;
return existingValue;
static void Set<T>(ref T target, T? value)
where T : struct
{
if (value.HasValue)
target = value.Value;
}
}
}
}
[StructLayout(LayoutKind.Explicit)]
public struct MaterialValueState(
in ColorRow game,
in ColorRow model,
CharacterWeapon drawData,
StateSource source)
{
public MaterialValueState(in ColorRow gameRow, in ColorRow modelRow, CharacterArmor armor, StateSource source)
: this(gameRow, modelRow, armor.ToWeapon(0), source)
{ }
[FieldOffset(0)]
public ColorRow Game = game;
[FieldOffset(44)]
public ColorRow Model = model;
[FieldOffset(88)]
public readonly CharacterWeapon DrawData = drawData;
[FieldOffset(95)]
public readonly StateSource Source = source;
public readonly bool EqualGame(in ColorRow rhsRow, CharacterWeapon rhsData)
=> DrawData.Skeleton == rhsData.Skeleton
&& DrawData.Weapon == rhsData.Weapon
&& DrawData.Variant == rhsData.Variant
&& DrawData.Stain == rhsData.Stain
&& Game.NearEqual(rhsRow);
public readonly MaterialValueDesign Convert()
=> new(Model, true);
}
public readonly struct MaterialValueManager<T> public readonly struct MaterialValueManager<T>
{ {
@ -113,7 +343,7 @@ public readonly struct MaterialValueManager<T>
return count; return count;
} }
public ReadOnlySpan<(uint key, T Value)> GetValues(MaterialValueIndex min, MaterialValueIndex max) public ReadOnlySpan<(uint Key, T Value)> GetValues(MaterialValueIndex min, MaterialValueIndex max)
=> MaterialValueManager.Filter<T>(CollectionsMarshal.AsSpan(_values), min, max); => MaterialValueManager.Filter<T>(CollectionsMarshal.AsSpan(_values), min, max);
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
@ -187,6 +417,7 @@ public static class MaterialValueManager
maxIdx = ~maxIdx + idx; maxIdx = ~maxIdx + idx;
return maxIdx > minIdx ? (minIdx, maxIdx - 1) : (-1, -1); return maxIdx > minIdx ? (minIdx, maxIdx - 1) : (-1, -1);
} }
maxIdx += idx; maxIdx += idx;
while (maxIdx < values.Length - 1 && values[maxIdx + 1].Key <= maxKey) while (maxIdx < values.Length - 1 && values[maxIdx + 1].Key <= maxKey)

View file

@ -222,7 +222,7 @@ public class InternalStateEditor(
} }
/// <summary> Change the value of a single material color table entry. </summary> /// <summary> Change the value of a single material color table entry. </summary>
public bool ChangeMaterialValue(ActorState state, MaterialValueIndex index, Vector3 value, Vector3 gameValue, StateSource source, out Vector3 oldValue, public bool ChangeMaterialValue(ActorState state, MaterialValueIndex index, in MaterialValueState newValue, StateSource source, out ColorRow? oldValue,
uint key = 0) uint key = 0)
{ {
// We already have an existing value. // We already have an existing value.
@ -240,18 +240,18 @@ public class InternalStateEditor(
} }
// Update if edited. // Update if edited.
state.Materials.UpdateValue(index, new MaterialValueState(gameValue, value, source), out _); state.Materials.UpdateValue(index, newValue, out _);
return true; return true;
} }
// We do not have an existing value. // We do not have an existing value.
oldValue = gameValue; oldValue = null;
// Do not do anything if locked or if the game value updates, because then we do not need to add an entry. // Do not do anything if locked or if the game value updates, because then we do not need to add an entry.
if (!state.CanUnlock(key) || source is StateSource.Game) if (!state.CanUnlock(key) || source is StateSource.Game)
return false; return false;
// Only add an entry if it is sufficiently different from the game value. // Only add an entry if it is different from the game value.
return !value.NearEqual(gameValue) && state.Materials.TryAddValue(index, new MaterialValueState(gameValue, value, source)); return state.Materials.TryAddValue(index, newValue);
} }
public bool ChangeMetaState(ActorState state, MetaIndex index, bool value, StateSource source, out bool oldValue, public bool ChangeMetaState(ActorState state, MetaIndex index, bool value, StateSource source, out bool oldValue,

View file

@ -276,7 +276,7 @@ public class StateApplier(
return data; return data;
} }
public unsafe void ChangeMaterialValue(ActorData data, MaterialValueIndex index, Vector3? value, bool force) public unsafe void ChangeMaterialValue(ActorData data, MaterialValueIndex index, ColorRow? value, bool force)
{ {
if (!force && !_config.UseAdvancedParameters) if (!force && !_config.UseAdvancedParameters)
return; return;
@ -289,14 +289,11 @@ public class StateApplier(
if (!index.TryGetColorTable(texture, out var table)) if (!index.TryGetColorTable(texture, out var table))
continue; continue;
Vector3 actualValue;
if (value.HasValue) if (value.HasValue)
actualValue = value.Value; value.Value.Apply(ref table[index.RowIndex]);
else if (!PrepareColorSet.TryGetColorTable(actor, index, out var baseTable) else if (PrepareColorSet.TryGetColorTable(actor, index, out var baseTable))
|| !index.DataIndex.TryGetValue(baseTable[index.RowIndex], out actualValue)) table[index.RowIndex] = baseTable[index.RowIndex];
continue; else
if (!index.DataIndex.SetValue(ref table[index.RowIndex], actualValue))
continue; continue;
MaterialService.ReplaceColorTable(texture, table); MaterialService.ReplaceColorTable(texture, table);

View file

@ -165,15 +165,15 @@ public class StateEditor(
StateChanged.Invoke(StateChanged.Type.Parameter, settings.Source, state, actors, (old, @new, flag)); StateChanged.Invoke(StateChanged.Type.Parameter, settings.Source, state, actors, (old, @new, flag));
} }
public void ChangeMaterialValue(object data, MaterialValueIndex index, Vector3 value, Vector3 gameValue, ApplySettings settings) public void ChangeMaterialValue(object data, MaterialValueIndex index, in MaterialValueState newValue, ApplySettings settings)
{ {
var state = (ActorState)data; var state = (ActorState)data;
if (!Editor.ChangeMaterialValue(state, index, value, gameValue, settings.Source, out var oldValue, settings.Key)) if (!Editor.ChangeMaterialValue(state, index, newValue, settings.Source, out var oldValue, settings.Key))
return; return;
var actors = Applier.ChangeMaterialValue(state, index, settings.Source.RequiresChange()); var actors = Applier.ChangeMaterialValue(state, index, settings.Source.RequiresChange());
Glamourer.Log.Verbose($"Set material value in state {state.Identifier.Incognito(null)} from {oldValue} to {value}. [Affecting {actors.ToLazyString("nothing")}.]"); Glamourer.Log.Verbose($"Set material value in state {state.Identifier.Incognito(null)} from {oldValue} to {newValue.Game}. [Affecting {actors.ToLazyString("nothing")}.]");
StateChanged.Invoke(StateChanged.Type.MaterialValue, settings.Source, state, actors, (oldValue, value, index)); StateChanged.Invoke(StateChanged.Type.MaterialValue, settings.Source, state, actors, (oldValue, newValue.Game, index));
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -282,6 +282,20 @@ public class StateEditor(
if (!settings.RespectManual || !state.Sources[meta].IsManual()) if (!settings.RespectManual || !state.Sources[meta].IsManual())
Editor.ChangeMetaState(state, meta, mergedDesign.Design.DesignData.GetMeta(meta), Source(meta), out _, settings.Key); Editor.ChangeMetaState(state, meta, mergedDesign.Design.DesignData.GetMeta(meta), Source(meta), out _, settings.Key);
} }
foreach (var (key, value) in mergedDesign.Design.Materials)
{
if (!value.Enabled)
continue;
var idx = MaterialValueIndex.FromKey(key);
// TODO
//if (state.Materials.TryGetValue(idx, out var materialState))
//{
// if (!settings.RespectManual || materialState.Source.IsManual())
// Editor.ChangeMaterialValue(state, idx, new MaterialValueState(materialState.Game, value.Value, materialState.DrawData));
//}
}
} }
var actors = settings.Source.RequiresChange() var actors = settings.Source.RequiresChange()