diff --git a/Glamourer/Configuration.cs b/Glamourer/Configuration.cs
index 219e8cf..0ceb056 100644
--- a/Glamourer/Configuration.cs
+++ b/Glamourer/Configuration.cs
@@ -82,6 +82,7 @@ public class Configuration : IPluginConfiguration, ISavable
public bool AllowDoubleClickToApply { get; set; } = false;
public bool RespectManualOnAutomationUpdate { get; set; } = false;
public bool PreventRandomRepeats { get; set; } = false;
+ public bool? AlwaysEditAsRoughness { get; set; } = null;
public string PcpFolder { get; set; } = "PCP";
public string PcpColor { get; set; } = "";
diff --git a/Glamourer/Designs/DesignEditor.cs b/Glamourer/Designs/DesignEditor.cs
index e5c0357..c6fbe86 100644
--- a/Glamourer/Designs/DesignEditor.cs
+++ b/Glamourer/Designs/DesignEditor.cs
@@ -256,7 +256,21 @@ public class DesignEditor(
DesignChanged.Invoke(DesignChanged.Type.MaterialRevert, design, new MaterialRevertTransaction(index, !revert, revert));
}
- public void ChangeMaterialValue(Design design, MaterialValueIndex index, ColorRow? row)
+ public void ChangeMaterialMode(Design design, MaterialValueIndex index, ColorRow.Mode mode)
+ {
+ var materials = design.GetMaterialDataRef();
+ if (!materials.TryGetValue(index, out var oldValue))
+ return;
+
+ var oldMode = oldValue.Mode;
+ materials.AddOrUpdateValue(index, oldValue with { Mode = mode });
+ Glamourer.Log.Debug($"Changed advanced dye value for {index} from {oldMode} to {mode} mode.");
+ design.LastEdit = DateTimeOffset.UtcNow;
+ SaveService.QueueSave(design);
+ DesignChanged.Invoke(DesignChanged.Type.MaterialMode, design, new MaterialModeTransaction(index, oldMode, mode));
+ }
+
+ public void ChangeMaterialValue(Design design, MaterialValueIndex index, ColorRow? row, ColorRow.Mode? mode = null)
{
var materials = design.GetMaterialDataRef();
if (materials.TryGetValue(index, out var oldValue))
@@ -268,9 +282,14 @@ public class DesignEditor(
}
else if (!row.Value.NearEqual(oldValue.Value))
{
- materials.UpdateValue(index, new MaterialValueDesign(row.Value, oldValue.Enabled, oldValue.Revert), out _);
+ materials.UpdateValue(index, new MaterialValueDesign(row.Value, oldValue.Enabled, oldValue.Revert, mode ?? oldValue.Mode), out _);
Glamourer.Log.Debug($"Updated advanced dye value for {index} to new value.");
}
+ else if (mode.HasValue && mode.Value != oldValue.Mode)
+ {
+ ChangeMaterialMode(design, index, mode.Value);
+ return;
+ }
else
{
return;
@@ -280,7 +299,7 @@ public class DesignEditor(
{
if (!row.HasValue)
return;
- if (!materials.TryAddValue(index, new MaterialValueDesign(row.Value, true, false)))
+ if (!materials.TryAddValue(index, new MaterialValueDesign(row.Value, true, false, mode ?? ColorRow.Mode.Legacy)))
return;
Glamourer.Log.Debug($"Added new advanced dye value for {index}.");
@@ -288,7 +307,8 @@ public class DesignEditor(
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.DelaySave(design);
- DesignChanged.Invoke(DesignChanged.Type.Material, design, new MaterialTransaction(index, oldValue.Value, row));
+ DesignChanged.Invoke(DesignChanged.Type.Material, design,
+ new MaterialTransaction(index, oldValue.Value, row, mode.HasValue ? oldValue.Mode : null, mode));
}
public void ChangeApplyMaterialValue(Design design, MaterialValueIndex index, bool value)
diff --git a/Glamourer/Designs/History/DesignTransaction.cs b/Glamourer/Designs/History/DesignTransaction.cs
index 65086db..157e71d 100644
--- a/Glamourer/Designs/History/DesignTransaction.cs
+++ b/Glamourer/Designs/History/DesignTransaction.cs
@@ -119,16 +119,18 @@ public readonly record struct ModUpdatedTransaction(Mod Mod, ModSettings Old, Mo
}
/// Only Designs.
-public readonly record struct MaterialTransaction(MaterialValueIndex Index, ColorRow? Old, ColorRow? New)
+public readonly record struct MaterialTransaction(MaterialValueIndex Index, ColorRow? Old, ColorRow? New, ColorRow.Mode? OldMode, ColorRow.Mode? NewMode)
: ITransaction
{
public ITransaction? Merge(ITransaction older)
- => older is MaterialTransaction other && Index == other.Index ? new MaterialTransaction(Index, other.Old, New) : null;
+ => older is MaterialTransaction other && Index == other.Index
+ ? new MaterialTransaction(Index, other.Old, New, other.OldMode ?? OldMode, NewMode ?? other.NewMode)
+ : null;
public void Revert(IDesignEditor editor, object data)
{
if (editor is DesignManager e)
- e.ChangeMaterialValue((Design)data, Index, Old);
+ e.ChangeMaterialValue((Design)data, Index, Old, OldMode);
}
}
@@ -143,6 +145,17 @@ public readonly record struct MaterialRevertTransaction(MaterialValueIndex Index
=> ((DesignManager)editor).ChangeMaterialRevert((Design)data, Index, Old);
}
+/// Only Designs.
+public readonly record struct MaterialModeTransaction(MaterialValueIndex Index, ColorRow.Mode Old, ColorRow.Mode New)
+ : ITransaction
+{
+ public ITransaction? Merge(ITransaction other)
+ => null;
+
+ public void Revert(IDesignEditor editor, object data)
+ => ((DesignManager)editor).ChangeMaterialMode((Design)data, Index, Old);
+}
+
/// Only Designs.
public readonly record struct ApplicationTransaction(object Index, bool Old, bool New)
: ITransaction
diff --git a/Glamourer/Events/DesignChanged.cs b/Glamourer/Events/DesignChanged.cs
index 04bb46a..b6e708e 100644
--- a/Glamourer/Events/DesignChanged.cs
+++ b/Glamourer/Events/DesignChanged.cs
@@ -87,6 +87,9 @@ public sealed class DesignChanged()
/// An existing design had an advanced dye rows Revert state changed.
MaterialRevert,
+ /// An existing design had an advanced dye rows mode changed.
+ MaterialMode,
+
/// An existing design had changed whether it always forces a redraw or not.
ForceRedraw,
diff --git a/Glamourer/Gui/Materials/AdvancedDyePopup.cs b/Glamourer/Gui/Materials/AdvancedDyePopup.cs
index bdbaa16..74a41c4 100644
--- a/Glamourer/Gui/Materials/AdvancedDyePopup.cs
+++ b/Glamourer/Gui/Materials/AdvancedDyePopup.cs
@@ -31,8 +31,9 @@ public sealed unsafe class AdvancedDyePopup(
private bool _anyChanged;
private bool _forceFocus;
- private const int RowsPerPage = 16;
- private int _rowOffset;
+ private const int RowsPerPage = 16;
+ private int _rowOffset;
+ private bool _editSheen;
private bool ShouldBeDrawn()
{
@@ -149,28 +150,51 @@ public sealed unsafe class AdvancedDyePopup(
if ((tab.Success || select is TabItemFlags.SetSelected) && available)
{
_selectedMaterial = i;
- DrawToggle();
+ DrawToggles();
DrawTable(index, table);
}
}
}
- private void DrawToggle()
+ private void DrawToggles()
{
- var buttonWidth = new Vector2(Im.ContentRegion.Available.X / 2, 0);
- using var font = Im.Font.PushMono();
- using var hoverColor = ImGuiColor.ButtonHovered.Push(Im.Style[ImGuiColor.TabHovered]);
+ var layerButtonWidth = _mode is ColorRow.Mode.Dawntrail ? Im.Font.Mono.GetCharacterAdvance(' ') * 5 + Im.Style.FramePadding.X * 2 : 0;
+ var buttonWidth =
+ (Im.ContentRegion.Available.X - (_mode is ColorRow.Mode.Dawntrail ? layerButtonWidth * 2 + Im.Style.ItemSpacing.X : 0)) / 2;
+ var buttonSize = new Vector2(buttonWidth, 0);
+ using var font = Im.Font.PushMono();
+ using var hoverColor = ImGuiColor.ButtonHovered.Push(Im.Style[ImGuiColor.TabHovered]);
hoverColor.Push(ImGuiColor.Button, Im.Style[_rowOffset is 0 ? ImGuiColor.TabSelected : ImGuiColor.Tab]);
- if (ImEx.ButtonCorners("Row Pairs 1-8 "u8, buttonWidth, ButtonFlags.MouseButtonLeft, Corners.Left))
+ if (ImEx.ButtonCorners("Row Pairs 1-8 "u8, buttonSize, ButtonFlags.MouseButtonLeft, Corners.Left))
_rowOffset = 0;
hoverColor.Pop();
Im.Line.NoSpacing();
hoverColor.Push(ImGuiColor.Button, Im.Style[_rowOffset is RowsPerPage ? ImGuiColor.TabSelected : ImGuiColor.Tab]);
- if (ImEx.ButtonCorners("Row Pairs 9-16"u8, buttonWidth, ButtonFlags.MouseButtonLeft, Corners.Right))
+ if (ImEx.ButtonCorners("Row Pairs 9-16"u8, buttonSize, ButtonFlags.MouseButtonLeft, Corners.Right))
_rowOffset = RowsPerPage;
+ hoverColor.Pop();
+
+ if (_mode is ColorRow.Mode.Dawntrail)
+ {
+ Im.Line.Same();
+
+ buttonSize = new Vector2(layerButtonWidth, 0);
+
+ hoverColor.Push(ImGuiColor.Button, Im.Style[!_editSheen ? ImGuiColor.TabSelected : ImGuiColor.Tab]);
+ if (ImEx.ButtonCorners("Base"u8, buttonSize, ButtonFlags.MouseButtonLeft, Corners.Left))
+ _editSheen = false;
+ hoverColor.Pop();
+
+ Im.Line.NoSpacing();
+
+ hoverColor.Push(ImGuiColor.Button, Im.Style[_editSheen ? ImGuiColor.TabSelected : ImGuiColor.Tab]);
+ if (ImEx.ButtonCorners("Sheen"u8, buttonSize, ButtonFlags.MouseButtonLeft, Corners.Right))
+ _editSheen = true;
+ hoverColor.Pop();
+ }
}
private void DrawContent(ReadOnlySpan> textures,
@@ -323,7 +347,8 @@ public sealed unsafe class AdvancedDyePopup(
Im.Line.Same(Im.Window.Size.X - 3 * Im.Style.FrameHeight - 2 * spacing - Im.Style.WindowPadding.X);
if (ImEx.Icon.Button(LunaStyle.ToClipboardIcon, "Export this table to your clipboard."u8))
{
- ColorRowClipboard.Table = table;
+ ColorRowClipboard.Table = table;
+ ColorRowClipboard.TableMode = _mode;
CopyToClipboard(table);
}
@@ -333,7 +358,7 @@ public sealed unsafe class AdvancedDyePopup(
for (var idx = 0; idx < ColorTable.NumRows; ++idx)
{
var row = newTable[idx];
- var internalRow = new ColorRow(row);
+ var internalRow = ColorRow.From(row, _mode);
var slot = materialIndex.ToEquipSlot();
var weapon = slot is EquipSlot.MainHand or EquipSlot.OffHand
? _state.ModelData.Weapon(slot)
@@ -354,7 +379,7 @@ public sealed unsafe class AdvancedDyePopup(
var changed = _state.Materials.TryGetValue(index, out var value);
if (!changed)
{
- var internalRow = new ColorRow(row);
+ var internalRow = ColorRow.From(row, _mode);
var slot = index.ToEquipSlot();
var weapon = slot switch
{
@@ -369,7 +394,9 @@ public sealed unsafe class AdvancedDyePopup(
else
{
_anyChanged = true;
- value = new MaterialValueState(value.Game, value.Model, value.DrawData, StateSource.Manual);
+ value = new MaterialValueState(value.Game,
+ value.Model.IsPartial(_mode) ? value.Model.MergeOnto(ColorRow.From(row, _mode)) : value.Model,
+ value.DrawData, StateSource.Manual);
}
ImEx.Icon.Button(LunaStyle.OnHoverIcon, "Highlight the affected colors on the character."u8);
@@ -385,45 +412,79 @@ public sealed unsafe class AdvancedDyePopup(
}
Im.Line.Same(0, Im.Style.ItemSpacing.X * 2);
- var applied = ImEx.ColorPickerButton("##diffuse"u8, "Change the diffuse value for this row."u8, value.Model.Diffuse,
+ var applied = ImEx.ColorPickerButton("##diffuse"u8, "Change the diffuse color for this row."u8, value.Model.Diffuse,
out value.Model.Diffuse, 'D');
var spacing = Im.Style.ItemInnerSpacing;
Im.Line.Same(0, spacing.X);
- applied |= ImEx.ColorPickerButton("##specular"u8, "Change the specular value for this row."u8, value.Model.Specular,
+ applied |= ImEx.ColorPickerButton("##specular"u8, "Change the specular color for this row."u8, value.Model.Specular,
out value.Model.Specular, 'S');
Im.Line.Same(0, spacing.X);
- applied |= ImEx.ColorPickerButton("##emissive"u8, "Change the emissive value for this row."u8, value.Model.Emissive,
+ applied |= ImEx.ColorPickerButton("##emissive"u8, "Change the emissive color for this row."u8, value.Model.Emissive,
out value.Model.Emissive, 'E');
-
+
Im.Line.Same(0, spacing.X);
- if (_mode is not ColorRow.Mode.Dawntrail)
+
+ if (_mode is ColorRow.Mode.Dawntrail && _editSheen)
{
- Im.Item.SetNextWidthScaled(100);
- applied |= DragGloss(ref value.Model.GlossStrength);
- Im.Tooltip.OnHover("Change the gloss strength for this row."u8);
+ // The other layout has 2 items of width 100*sc and one spacing, for a total of 200*sc + 1*sp.
+ // This layout has 3 items and two spacings: 3*w + 2*sp = 200*sc + 1*sp.
+ var allItemsWidth = 200 * Im.Style.GlobalScale - spacing.X;
+ var itemWidth = MathF.Floor(allItemsWidth / 3);
+
+ Im.Item.SetNextWidth(allItemsWidth - itemWidth * 2);
+ applied |= DragSheen(ref value.Model.Sheen, false);
+ Im.Tooltip.OnHover("Change the sheen strength for this row."u8);
+
+ Im.Line.Same(0, spacing.X);
+
+ Im.Item.SetNextWidth(itemWidth);
+ applied |= DragSheenTint(ref value.Model.SheenTint, false);
+ Im.Tooltip.OnHover("Change the sheen tint for this row."u8);
+
+ Im.Line.Same(0, spacing.X);
+
+ Im.Item.SetNextWidth(itemWidth);
+ applied |= DragSheenRoughness(ref value.Model.SheenAperture, false);
+ Im.Tooltip.OnHover("Change the sheen roughness for this row."u8);
}
else
- {
- Im.Dummy(new Vector2(100 * Im.Style.GlobalScale, 0));
- }
-
- Im.Line.Same(0, spacing.X);
- if (_mode is not ColorRow.Mode.Dawntrail)
{
Im.Item.SetNextWidthScaled(100);
- applied |= DragSpecularStrength(ref value.Model.SpecularStrength);
- Im.Tooltip.OnHover("Change the specular strength for this row."u8);
- }
- else
- {
- Im.Dummy(new Vector2(100 * Im.Style.GlobalScale, 0));
+ var editAsRoughness = config.AlwaysEditAsRoughness ?? _mode is ColorRow.Mode.Dawntrail;
+ applied |= (_mode, editAsRoughness) switch
+ {
+ (ColorRow.Mode.Legacy, false) => DragGloss(ref value.Model.GlossStrength, false),
+ (ColorRow.Mode.Legacy, true) => DragGlossAsRoughness(ref value.Model.GlossStrength, false),
+ (ColorRow.Mode.Dawntrail, false) => DragRoughnessAsGloss(ref value.Model.Roughness, false),
+ (ColorRow.Mode.Dawntrail, true) => DragRoughness(ref value.Model.Roughness, false),
+ _ => throw new NotImplementedException(),
+ };
+ Im.Tooltip.OnHover(editAsRoughness ? "Change the roughness for this row."u8 : "Change the gloss strength for this row."u8);
+
+ Im.Line.Same(0, spacing.X);
+ if (_mode is not ColorRow.Mode.Dawntrail)
+ {
+ Im.Item.SetNextWidthScaled(100);
+ applied |= DragSpecularStrength(ref value.Model.SpecularStrength, false);
+ Im.Tooltip.OnHover("Change the specular strength for this row."u8);
+ }
+ else
+ {
+ Im.Item.SetNextWidthScaled(100);
+ applied |= DragMetalness(ref value.Model.Metalness, false);
+ Im.Tooltip.OnHover("Change the metalness for this row."u8);
+ }
}
Im.Line.Same(0, spacing.X);
if (ImEx.Icon.Button(LunaStyle.ToClipboardIcon, "Export this row to your clipboard."u8))
- ColorRowClipboard.Row = value.Model;
+ {
+ ColorRowClipboard.Row = value.Model;
+ ColorRowClipboard.RowMode = _mode;
+ }
+
Im.Line.Same(0, spacing.X);
if (ImEx.Icon.Button(LunaStyle.FromClipboardIcon, "Import an exported row from your clipboard onto this row."u8,
!ColorRowClipboard.IsSet))
@@ -440,12 +501,12 @@ public sealed unsafe class AdvancedDyePopup(
stateManager.ChangeMaterialValue(_state, index, value, ApplySettings.Manual);
}
- public static bool DragGloss(ref float value)
+ public static bool DragGloss(ref float value, bool canUnset)
{
- var tmp = value;
+ var tmp = float.IsNaN(value) ? ColorRow.DefaultGlossStrength : value;
var minValue = Im.Io.KeyControl ? 0f : (float)Half.Epsilon;
- if (!Im.Drag("##Gloss"u8, ref tmp, "%.1f G"u8, 0.001f, minValue, Math.Max(0.01f, 0.005f * value), SliderFlags.AlwaysClamp))
- return false;
+ if (!Im.Drag("##Gloss"u8, ref tmp, float.IsNaN(value) ? "\u2014 G"u8 : "%.1f G"u8, 0.001f, minValue, Math.Max(0.01f, 0.005f * value), SliderFlags.AlwaysClamp))
+ return UnsetBehavior(ref value, canUnset);
var tmp2 = Math.Clamp(tmp, minValue, (float)Half.MaxValue);
if (tmp2 == value)
@@ -455,12 +516,27 @@ public sealed unsafe class AdvancedDyePopup(
return true;
}
- public static bool DragSpecularStrength(ref float value)
+ public static bool DragGlossAsRoughness(ref float value, bool canUnset)
{
- var tmp = value * 100f;
- if (!Im.Drag("##SpecularStrength"u8, ref tmp, "%.0f%% SS"u8, 0f, (float)Half.MaxValue * 100f, 0.05f, SliderFlags.AlwaysClamp))
+ var roughness = ColorTableRow.RoughnessFromShininess(float.IsNaN(value) ? ColorRow.DefaultGlossStrength : value);
+ var tmp = roughness * 100f;
+ if (!Im.Drag("##Gloss"u8, ref tmp, float.IsNaN(value) ? "\u2014 Rg"u8 : "%.0f%% Rg"u8, 0f, 100f, 0.25f, SliderFlags.AlwaysClamp))
+ return UnsetBehavior(ref value, canUnset);
+
+ var tmp2 = Math.Clamp(tmp, 0f, 100f) / 100f;
+ if (tmp2 == roughness)
return false;
+ value = ColorTableRow.ShininessFromRoughness(tmp2);
+ return true;
+ }
+
+ public static bool DragSpecularStrength(ref float value, bool canUnset)
+ {
+ var tmp = (float.IsNaN(value) ? ColorRow.DefaultSpecularStrength : value) * 100f;
+ if (!Im.Drag("##SpecularStrength"u8, ref tmp, float.IsNaN(value) ? "\u2014 SS"u8 : "%.0f%% SS"u8, 0f, (float)Half.MaxValue * 100f, 0.05f, SliderFlags.AlwaysClamp))
+ return UnsetBehavior(ref value, canUnset);
+
var tmp2 = Math.Clamp(tmp, 0f, (float)Half.MaxValue * 100f) / 100f;
if (tmp2 == value)
return false;
@@ -469,6 +545,102 @@ public sealed unsafe class AdvancedDyePopup(
return true;
}
+ public static bool DragRoughness(ref float value, bool canUnset)
+ {
+ var tmp = (float.IsNaN(value) ? ColorRow.DefaultRoughness : value) * 100f;
+ if (!Im.Drag("##Roughness"u8, ref tmp, float.IsNaN(value) ? "\u2014 Rg"u8 : "%.0f%% Rg"u8, 0f, 100f, 0.25f, SliderFlags.AlwaysClamp))
+ return UnsetBehavior(ref value, canUnset);
+
+ var tmp2 = Math.Clamp(tmp, 0f, 100f) / 100f;
+ if (tmp2 == value)
+ return false;
+
+ value = tmp2;
+ return true;
+ }
+
+ public static bool DragRoughnessAsGloss(ref float value, bool canUnset)
+ {
+ var gloss = ColorTableRow.ShininessFromRoughness(float.IsNaN(value) ? ColorRow.DefaultRoughness : value);
+ var tmp = gloss;
+ if (!Im.Drag("##Roughness"u8, ref tmp, float.IsNaN(value) ? "\u2014 G"u8 : "%.1f G"u8, 0.001f, (float)Half.Epsilon, Math.Max(0.01f, 0.005f * gloss), SliderFlags.AlwaysClamp))
+ return UnsetBehavior(ref value, canUnset);
+
+ var tmp2 = Math.Clamp(tmp, (float)Half.Epsilon, (float)Half.MaxValue);
+ if (tmp2 == gloss)
+ return false;
+
+ value = ColorTableRow.RoughnessFromShininess(tmp2);
+ return true;
+ }
+
+ public static bool DragMetalness(ref float value, bool canUnset)
+ {
+ var tmp = (float.IsNaN(value) ? ColorRow.DefaultMetalness : value) * 100f;
+ if (!Im.Drag("##Metalness"u8, ref tmp, float.IsNaN(value) ? "\u2014 Mt"u8 : "%.0f%% Mt"u8, 0f, 100f, 0.25f, SliderFlags.AlwaysClamp))
+ return UnsetBehavior(ref value, canUnset);
+
+ var tmp2 = Math.Clamp(tmp, 0f, 100f) / 100f;
+ if (tmp2 == value)
+ return false;
+
+ value = tmp2;
+ return true;
+ }
+
+ public static bool DragSheen(ref float value, bool canUnset)
+ {
+ var tmp = (float.IsNaN(value) ? ColorRow.DefaultSheen : value) * 100f;
+ if (!Im.Drag("##Sheen"u8, ref tmp, float.IsNaN(value) ? "\u2014 Sh"u8 : "%.0f%% Sh"u8, 0f, 100f * (float)Half.MaxValue, 0.25f, SliderFlags.AlwaysClamp))
+ return UnsetBehavior(ref value, canUnset);
+
+ var tmp2 = Math.Clamp(tmp, 0f, 100f * (float)Half.MaxValue) / 100f;
+ if (tmp2 == value)
+ return false;
+
+ value = tmp2;
+ return true;
+ }
+
+ public static bool DragSheenTint(ref float value, bool canUnset)
+ {
+ var tmp = (float.IsNaN(value) ? ColorRow.DefaultSheenTint : value) * 100f;
+ if (!Im.Drag("##SheenTint"u8, ref tmp, float.IsNaN(value) ? "\u2014 ST"u8 : "%.0f%% ST"u8, -100f * (float)Half.MaxValue, 100f * (float)Half.MaxValue, 0.25f,
+ SliderFlags.AlwaysClamp))
+ return UnsetBehavior(ref value, canUnset);
+
+ var tmp2 = Math.Clamp(tmp, -100f * (float)Half.MaxValue, 100f * (float)Half.MaxValue) / 100f;
+ if (tmp2 == value)
+ return false;
+
+ value = tmp2;
+ return true;
+ }
+
+ public static bool DragSheenRoughness(ref float value, bool canUnset)
+ {
+ var tmp = 100f / (float.IsNaN(value) ? ColorRow.DefaultSheenAperture : value);
+ if (!Im.Drag("##SheenAperture"u8, ref tmp, float.IsNaN(value) ? "\u2014 SR"u8 : "%.0f%% SR"u8, 100f / (float)Half.MaxValue, 100f / (float)Half.Epsilon, 0.25f,
+ SliderFlags.AlwaysClamp))
+ return UnsetBehavior(ref value, canUnset);
+
+ var tmp2 = Math.Clamp(100f / tmp, (float)Half.Epsilon, (float)Half.MaxValue);
+ if (tmp2 == value)
+ return false;
+
+ value = tmp2;
+ return true;
+ }
+
+ private static bool UnsetBehavior(ref float value, bool canUnset)
+ {
+ if (!(canUnset && Im.Item.RightClicked() && Im.Io.KeyControl))
+ return false;
+
+ value = float.NaN;
+ return true;
+ }
+
private LabelStruct _label = new();
private struct LabelStruct
diff --git a/Glamourer/Gui/Materials/ColorRowClipboard.cs b/Glamourer/Gui/Materials/ColorRowClipboard.cs
index 329211b..a4cb674 100644
--- a/Glamourer/Gui/Materials/ColorRowClipboard.cs
+++ b/Glamourer/Gui/Materials/ColorRowClipboard.cs
@@ -18,6 +18,8 @@ public static class ColorRowClipboard
field = value;
}
}
+
+ public static ColorRow.Mode TableMode { get; set; }
public static ColorRow Row
{
@@ -28,4 +30,6 @@ public static class ColorRowClipboard
field = value;
}
}
+
+ public static ColorRow.Mode RowMode { get; set; }
}
diff --git a/Glamourer/Gui/Materials/MaterialDrawer.cs b/Glamourer/Gui/Materials/MaterialDrawer.cs
index 766ff20..9187649 100644
--- a/Glamourer/Gui/Materials/MaterialDrawer.cs
+++ b/Glamourer/Gui/Materials/MaterialDrawer.cs
@@ -9,8 +9,9 @@ namespace Glamourer.Gui.Materials;
public class MaterialDrawer(DesignManager designManager, Configuration config) : IService
{
- public const float GlossWidth = 100;
- public const float SpecularStrengthWidth = 125;
+ public const float SliderWidth = 90;
+ public const float ModeWidth = 45;
+ public const float SheenSliderWidth = 75; // Should satisfy 2*Slider + Mode = 3*SheenSlider
private int _newMaterialIdx;
private int _newRowIdx;
@@ -25,17 +26,17 @@ public class MaterialDrawer(DesignManager designManager, Configuration config) :
_spacing = Im.Style.ItemInnerSpacing.X;
_buttonSize = new Vector2(Im.Style.FrameHeight);
var colorWidth = 4 * _buttonSize.X
- + (GlossWidth + SpecularStrengthWidth) * Im.Style.GlobalScale
- + 6 * _spacing
+ + (SliderWidth * 2 + ModeWidth) * Im.Style.GlobalScale
+ + 7 * _spacing
+ Im.Font.CalculateSize("Revert"u8).X;
DrawMultiButtons(design);
Im.Dummy(0);
Im.Separator();
Im.Dummy(0);
- if (available > 1.95 * colorWidth)
+ if (available > 2.6f * colorWidth)
DrawSingleRow(design);
else
- DrawTwoRow(design);
+ DrawMultipleRow(design);
DrawNew(design);
}
@@ -75,7 +76,7 @@ public class MaterialDrawer(DesignManager designManager, Configuration config) :
private void DrawName(MaterialValueIndex index)
{
using var style = ImStyleDouble.ButtonTextAlign.Push(new Vector2(0.05f, 0.5f));
- ImEx.TextFramed($"{index}", new Vector2((GlossWidth + SpecularStrengthWidth) * Im.Style.GlobalScale + _spacing, 0),
+ ImEx.TextFramed($"{index}", new Vector2((SliderWidth * 2 + ModeWidth) * Im.Style.GlobalScale + _spacing * 2, 0),
borderColor: ImGuiColor.Text.Get());
}
@@ -91,20 +92,21 @@ public class MaterialDrawer(DesignManager designManager, Configuration config) :
Im.Line.Same(0, _spacing);
DeleteButton(design, key, ref i);
Im.Line.Same(0, _spacing);
- CopyButton(value.Value);
+ CopyButton(value.Value, value.Mode);
Im.Line.Same(0, _spacing);
PasteButton(design, key);
Im.Line.Same(0, _spacing);
using var disabled = Im.Disabled(design.WriteProtected());
EnabledToggle(design, key, value.Enabled);
Im.Line.Same(0, _spacing);
- DrawRow(design, key, value.Value, value.Revert);
+ DrawRow(design, key, value.Value, value.Revert, value.Mode);
+ DrawRowExtra(design, key, value.Value, value.Revert, value.Mode, true);
Im.Line.Same(0, _spacing);
RevertToggle(design, key, value.Revert);
}
}
- private void DrawTwoRow(Design design)
+ private void DrawMultipleRow(Design design)
{
for (var i = 0; i < design.Materials.Count; ++i)
{
@@ -116,16 +118,18 @@ public class MaterialDrawer(DesignManager designManager, Configuration config) :
Im.Line.Same(0, _spacing);
DeleteButton(design, key, ref i);
Im.Line.Same(0, _spacing);
- CopyButton(value.Value);
+ CopyButton(value.Value, value.Mode);
Im.Line.Same(0, _spacing);
PasteButton(design, key);
Im.Line.Same(0, _spacing);
+ using var disabled = Im.Disabled(design.WriteProtected());
EnabledToggle(design, key, value.Enabled);
- DrawRow(design, key, value.Value, value.Revert);
+ DrawRow(design, key, value.Value, value.Revert, value.Mode);
Im.Line.Same(0, _spacing);
RevertToggle(design, key, value.Revert);
+ DrawRowExtra(design, key, value.Value, value.Revert, value.Mode, false);
Im.Separator();
}
}
@@ -142,17 +146,20 @@ public class MaterialDrawer(DesignManager designManager, Configuration config) :
--idx;
}
- private void CopyButton(in ColorRow row)
+ private void CopyButton(in ColorRow row, ColorRow.Mode mode)
{
if (ImEx.Icon.Button(LunaStyle.ToClipboardIcon, "Export this row to your clipboard."u8))
- ColorRowClipboard.Row = row;
+ {
+ ColorRowClipboard.Row = row;
+ ColorRowClipboard.RowMode = mode;
+ }
}
private void PasteButton(Design design, MaterialValueIndex index)
{
if (ImEx.Icon.Button(LunaStyle.FromClipboardIcon, "Import an exported row from your clipboard onto this row."u8,
!ColorRowClipboard.IsSet || design.WriteProtected()))
- designManager.ChangeMaterialValue(design, index, ColorRowClipboard.Row);
+ designManager.ChangeMaterialValue(design, index, ColorRowClipboard.Row, ColorRowClipboard.RowMode);
}
private void EnabledToggle(Design design, MaterialValueIndex index, bool enabled)
@@ -169,6 +176,39 @@ public class MaterialDrawer(DesignManager designManager, Configuration config) :
"If this is checked, Glamourer will try to revert the advanced dye row to its game state instead of applying a specific row."u8);
}
+ private void ModeToggle(Design design, MaterialValueIndex index, ColorRow.Mode mode)
+ {
+ if (Im.Button(ToCallsignString(mode), new Vector2(ModeWidth * Im.Style.GlobalScale, 0)))
+ designManager.ChangeMaterialMode(design, index, GetNextMode(mode));
+ Im.Tooltip.OnHover(ToTooltipString(mode));
+
+ return;
+
+ static ReadOnlySpan ToCallsignString(ColorRow.Mode mode)
+ => mode switch
+ {
+ ColorRow.Mode.Legacy => "Lgc###mode"u8,
+ ColorRow.Mode.Dawntrail => "DT###mode"u8,
+ _ => throw new NotImplementedException(),
+ };
+
+ static ColorRow.Mode GetNextMode(ColorRow.Mode mode)
+ => mode switch
+ {
+ ColorRow.Mode.Legacy => ColorRow.Mode.Dawntrail,
+ ColorRow.Mode.Dawntrail => ColorRow.Mode.Legacy,
+ _ => throw new NotImplementedException(),
+ };
+
+ static ReadOnlySpan ToTooltipString(ColorRow.Mode mode)
+ => mode switch
+ {
+ ColorRow.Mode.Legacy => "This color row currently contains Legacy material parameters.\nClick this button to switch it to Dawntrail parameters."u8,
+ ColorRow.Mode.Dawntrail => "This color row currently contains Dawntrail material parameters.\nClick this button to switch it to Legacy parameters."u8,
+ _ => throw new NotImplementedException(),
+ };
+ }
+
public sealed class MaterialSlotCombo;
private void DrawSlotCombo()
@@ -245,23 +285,76 @@ public class MaterialDrawer(DesignManager designManager, Configuration config) :
Im.Tooltip.OnHover("Drag this to the left or right to change its value."u8);
}
- private void DrawRow(Design design, MaterialValueIndex index, in ColorRow row, bool disabled)
+ private void DrawRow(Design design, MaterialValueIndex index, in ColorRow row, bool disabled, ColorRow.Mode mode)
{
var tmp = row;
using var _ = Im.Disabled(disabled);
- var applied = ImEx.ColorPickerButton("##diffuse"u8, "Change the diffuse value for this row."u8, row.Diffuse, out tmp.Diffuse, 'D');
+ var applied = ImEx.ColorPickerButton("##diffuse"u8, "Change the diffuse color for this row."u8, row.Diffuse, out tmp.Diffuse, 'D');
Im.Line.SameInner();
- applied |= ImEx.ColorPickerButton("##specular"u8, "Change the specular value for this row."u8, row.Specular, out tmp.Specular, 'S');
+ applied |= ImEx.ColorPickerButton("##specular"u8, "Change the specular color for this row."u8, row.Specular, out tmp.Specular, 'S');
Im.Line.SameInner();
- applied |= ImEx.ColorPickerButton("##emissive"u8, "Change the emissive value for this row."u8, row.Emissive, out tmp.Emissive, 'E');
+ applied |= ImEx.ColorPickerButton("##emissive"u8, "Change the emissive color for this row."u8, row.Emissive, out tmp.Emissive, 'E');
Im.Line.SameInner();
- Im.Item.SetNextWidth(GlossWidth * Im.Style.GlobalScale);
- applied |= AdvancedDyePopup.DragGloss(ref tmp.GlossStrength);
- Im.Tooltip.OnHover("Change the gloss strength for this row."u8);
+ ModeToggle(design, index, mode);
Im.Line.SameInner();
- Im.Item.SetNextWidth(SpecularStrengthWidth * Im.Style.GlobalScale);
- applied |= AdvancedDyePopup.DragSpecularStrength(ref tmp.SpecularStrength);
- Im.Tooltip.OnHover("Change the specular strength for this row."u8);
+ Im.Item.SetNextWidth(SliderWidth * Im.Style.GlobalScale);
+ var editAsRoughness = config.AlwaysEditAsRoughness ?? mode is ColorRow.Mode.Dawntrail;
+ applied |= (mode, editAsRoughness) switch
+ {
+ (ColorRow.Mode.Legacy, false) => AdvancedDyePopup.DragGloss(ref tmp.GlossStrength, true),
+ (ColorRow.Mode.Legacy, true) => AdvancedDyePopup.DragGlossAsRoughness(ref tmp.GlossStrength, true),
+ (ColorRow.Mode.Dawntrail, false) => AdvancedDyePopup.DragRoughnessAsGloss(ref tmp.Roughness, true),
+ (ColorRow.Mode.Dawntrail, true) => AdvancedDyePopup.DragRoughness(ref tmp.Roughness, true),
+ _ => throw new NotImplementedException(),
+ };
+ Im.Tooltip.OnHover(editAsRoughness
+ ? "Change the roughness for this row.\nControl and Right-Click to unset."u8
+ : "Change the gloss strength for this row.\nControl and Right-Click to unset."u8);
+ if (mode is ColorRow.Mode.Dawntrail)
+ {
+ Im.Line.SameInner();
+ Im.Item.SetNextWidth(SliderWidth * Im.Style.GlobalScale);
+ applied |= AdvancedDyePopup.DragMetalness(ref tmp.Metalness, true);
+ Im.Tooltip.OnHover("Change the metalness for this row.\nControl and Right-Click to unset."u8);
+ }
+ else
+ {
+ Im.Line.SameInner();
+ Im.Item.SetNextWidth(SliderWidth * Im.Style.GlobalScale);
+ applied |= AdvancedDyePopup.DragSpecularStrength(ref tmp.SpecularStrength, true);
+ Im.Tooltip.OnHover("Change the specular strength for this row.\nControl and Right-Click to unset."u8);
+ }
+
+ if (applied)
+ designManager.ChangeMaterialValue(design, index, tmp);
+ }
+
+ private void DrawRowExtra(Design design, MaterialValueIndex index, in ColorRow row, bool disabled, ColorRow.Mode mode, bool compact)
+ {
+ if (mode is not ColorRow.Mode.Dawntrail)
+ return;
+
+ var tmp = row;
+ using var _ = Im.Disabled(disabled);
+
+ if (!compact)
+ Im.Dummy(new Vector2(_buttonSize.X * 3 + _spacing * 2, _buttonSize.Y));
+
+ Im.Line.SameInner();
+ Im.Item.SetNextWidth(SheenSliderWidth * Im.Style.GlobalScale);
+ var applied = AdvancedDyePopup.DragSheen(ref tmp.Sheen, true);
+ Im.Tooltip.OnHover("Change the sheen strength for this row.\nControl and Right-Click to unset."u8);
+
+ Im.Line.SameInner();
+ Im.Item.SetNextWidth(SheenSliderWidth * Im.Style.GlobalScale);
+ applied |= AdvancedDyePopup.DragSheenTint(ref tmp.SheenTint, true);
+ Im.Tooltip.OnHover("Change the sheen tint for this row.\nControl and Right-Click to unset."u8);
+
+ Im.Line.SameInner();
+ Im.Item.SetNextWidth(SheenSliderWidth * Im.Style.GlobalScale);
+ applied |= AdvancedDyePopup.DragSheenRoughness(ref tmp.SheenAperture, true);
+ Im.Tooltip.OnHover("Change the sheen roughness for this row.\nControl and Right-Click to unset."u8);
+
if (applied)
designManager.ChangeMaterialValue(design, index, tmp);
}
diff --git a/Glamourer/Gui/Tabs/SettingsTab/SettingsTab.cs b/Glamourer/Gui/Tabs/SettingsTab/SettingsTab.cs
index de30188..249f3bc 100644
--- a/Glamourer/Gui/Tabs/SettingsTab/SettingsTab.cs
+++ b/Glamourer/Gui/Tabs/SettingsTab/SettingsTab.cs
@@ -235,6 +235,7 @@ public sealed class SettingsTab(
Checkbox("Smaller Equip Display"u8, "Use single-line display without icons and small dye buttons instead of double-line display."u8,
config.SmallEquip, v => config.SmallEquip = v);
DrawHeightUnitSettings();
+ DrawRoughnessSettings();
Checkbox("Show Application Checkboxes"u8,
"Show the application checkboxes in the Customization and Equipment panels of the design tab, instead of only showing them under Application Rules."u8,
!config.HideApplyCheckmarks, v => config.HideApplyCheckmarks = !v);
@@ -502,4 +503,34 @@ public sealed class SettingsTab(
LunaStyle.DrawAlignedHelpMarkerLabel("Character Height Display Type"u8,
"Select how to display the height of characters in real-world units, if at all."u8);
}
+
+ private void DrawRoughnessSettings()
+ {
+ Im.Item.SetNextWidthScaled(300);
+ using (var combo = Im.Combo.Begin("##alwaysEditAsRoughness"u8, ToRoughnessSettingString(config.AlwaysEditAsRoughness)))
+ {
+ if (combo)
+ foreach (var type in (IEnumerable)[null, true, false,])
+ {
+ if (Im.Selectable(ToRoughnessSettingString(type), config.AlwaysEditAsRoughness == type))
+ {
+ config.AlwaysEditAsRoughness = type;
+ config.Save();
+ }
+ }
+ }
+
+ LunaStyle.DrawAlignedHelpMarkerLabel("Gloss Strength and Roughness Display Type"u8,
+ "Select how to display and edit Gloss Strength and Roughness values.\nThe conversion formula used is an approximation and does not account for all the subtleties of legacy vs PBR shaders."u8);
+
+ return;
+
+ static ReadOnlySpan ToRoughnessSettingString(bool? alwaysEditAsRoughness)
+ => alwaysEditAsRoughness switch
+ {
+ null => "As-Is"u8,
+ true => "Always Roughness"u8,
+ false => "Always Gloss Strength"u8,
+ };
+ }
}
diff --git a/Glamourer/Interop/Material/MaterialManager.cs b/Glamourer/Interop/Material/MaterialManager.cs
index 3b99e2c..8cc7bdd 100644
--- a/Glamourer/Interop/Material/MaterialManager.cs
+++ b/Glamourer/Interop/Material/MaterialManager.cs
@@ -80,7 +80,7 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable
var idx = MaterialValueIndex.FromKey(values[i].Key);
var materialValue = values[i].Value;
ref var row = ref colorTable[idx.RowIndex];
- var newGame = new ColorRow(row);
+ var newGame = ColorRow.From(row, mode);
if (materialValue.EqualGame(newGame, drawData))
materialValue.Model.Apply(ref row, mode);
else
diff --git a/Glamourer/Interop/Material/MaterialValueManager.cs b/Glamourer/Interop/Material/MaterialValueManager.cs
index 01cb479..f490a60 100644
--- a/Glamourer/Interop/Material/MaterialValueManager.cs
+++ b/Glamourer/Interop/Material/MaterialValueManager.cs
@@ -11,7 +11,7 @@ using Penumbra.GameData.Structs;
namespace Glamourer.Interop.Material;
/// Values are not squared.
-public struct ColorRow(Vector3 diffuse, Vector3 specular, Vector3 emissive, float specularStrength, float glossStrength)
+public struct ColorRow(Vector3 diffuse, Vector3 specular, Vector3 emissive, float specularStrength, float glossStrength, float roughness, float metalness, float sheen, float sheenTint, float sheenAperture)
{
public enum Mode
{
@@ -19,26 +19,50 @@ public struct ColorRow(Vector3 diffuse, Vector3 specular, Vector3 emissive, floa
Dawntrail,
}
- public static readonly ColorRow Empty = new(Vector3.Zero, Vector3.Zero, Vector3.Zero, 1f, 1f);
+ public const float DefaultSpecularStrength = 1f;
+ public const float DefaultGlossStrength = 20f;
+ public const float DefaultRoughness = 0.5f;
+ public const float DefaultMetalness = 0f;
+ public const float DefaultSheen = 0.1f;
+ public const float DefaultSheenTint = 0.2f;
+ public const float DefaultSheenAperture = 5f;
+
+ public static readonly ColorRow Empty = new(Vector3.Zero, Vector3.Zero, Vector3.Zero, float.NaN, float.NaN, float.NaN, float.NaN, float.NaN,
+ float.NaN, float.NaN);
public Vector3 Diffuse = diffuse;
public Vector3 Specular = specular;
public Vector3 Emissive = emissive;
public float SpecularStrength = specularStrength;
public float GlossStrength = glossStrength;
+ public float Roughness = roughness;
+ public float Metalness = metalness;
+ public float Sheen = sheen;
+ public float SheenTint = sheenTint;
+ public float SheenAperture = sheenAperture;
- public ColorRow(in ColorTableRow row)
- : this(Root((Vector3)row.DiffuseColor), Root((Vector3)row.SpecularColor), Root((Vector3)row.EmissiveColor),
- (float)row.LegacySpecularStrength(),
- (float)row.LegacyGloss())
- { }
+ public static ColorRow From(in ColorTableRow row, Mode mode)
+ => mode switch
+ {
+ Mode.Legacy => new(Root((Vector3)row.DiffuseColor), Root((Vector3)row.SpecularColor), Root((Vector3)row.EmissiveColor),
+ (float)row.LegacySpecularStrength(), (float)row.LegacyGloss(), float.NaN, float.NaN, float.NaN, float.NaN, float.NaN),
+ Mode.Dawntrail => new(Root((Vector3)row.DiffuseColor), Root((Vector3)row.SpecularColor), Root((Vector3)row.EmissiveColor),
+ float.NaN, float.NaN, (float)row.DawntrailRoughness(), (float)row.DawntrailMetalness(), (float)row.DawntrailSheen(),
+ (float)row.DawntrailSheenTint(), (float)row.DawntrailSheenAperture()),
+ _ => throw new NotImplementedException(),
+ };
- public readonly bool NearEqual(in ColorRow rhs)
+ public readonly bool NearEqual(in ColorRow rhs, bool skipEmpty = false)
=> Diffuse.NearEqual(rhs.Diffuse)
&& Specular.NearEqual(rhs.Specular)
&& Emissive.NearEqual(rhs.Emissive)
- && SpecularStrength.NearEqual(rhs.SpecularStrength)
- && GlossStrength.NearEqual(rhs.GlossStrength);
+ && (float.IsNaN(SpecularStrength) ? skipEmpty || float.IsNaN(rhs.SpecularStrength) : SpecularStrength.NearEqual(rhs.SpecularStrength))
+ && (float.IsNaN(GlossStrength) ? skipEmpty || float.IsNaN(rhs.GlossStrength) : GlossStrength.NearEqual(rhs.GlossStrength))
+ && (float.IsNaN(Roughness) ? skipEmpty || float.IsNaN(rhs.Roughness) : Roughness.NearEqual(rhs.Roughness))
+ && (float.IsNaN(Metalness) ? skipEmpty || float.IsNaN(rhs.Metalness) : Metalness.NearEqual(rhs.Metalness))
+ && (float.IsNaN(Sheen) ? skipEmpty || float.IsNaN(rhs.Sheen) : Sheen.NearEqual(rhs.Sheen))
+ && (float.IsNaN(SheenAperture) ? skipEmpty || float.IsNaN(rhs.SheenAperture) : SheenAperture.NearEqual(rhs.SheenAperture))
+ && (float.IsNaN(SheenTint) ? skipEmpty || float.IsNaN(rhs.SheenTint) : SheenTint.NearEqual(rhs.SheenTint));
private static Vector3 Square(Vector3 value)
=> new(Square(value.X), Square(value.Y), Square(value.Z));
@@ -76,23 +100,85 @@ public struct ColorRow(Vector3 diffuse, Vector3 specular, Vector3 emissive, floa
ret = true;
}
- if (mode is Mode.Legacy)
+ switch (mode)
{
- if (!((float)row.LegacySpecularStrength()).NearEqual(SpecularStrength))
- {
- row.LegacySpecularStrengthWrite() = (Half)SpecularStrength;
- ret = true;
- }
+ case Mode.Legacy:
+ if (!float.IsNaN(SpecularStrength) && !((float)row.LegacySpecularStrength()).NearEqual(SpecularStrength))
+ {
+ row.LegacySpecularStrengthWrite() = (Half)SpecularStrength;
+ ret = true;
+ }
- if (!((float)row.LegacyGloss()).NearEqual(GlossStrength))
- {
- row.LegacyGlossWrite() = (Half)GlossStrength;
- ret = true;
- }
+ if (!float.IsNaN(GlossStrength) && !((float)row.LegacyGloss()).NearEqual(GlossStrength))
+ {
+ row.LegacyGlossWrite() = (Half)GlossStrength;
+ ret = true;
+ }
+
+ break;
+ case Mode.Dawntrail:
+ if (!float.IsNaN(Roughness) && !((float)row.DawntrailRoughness()).NearEqual(Roughness))
+ {
+ row.DawntrailRoughnessWrite() = (Half)Roughness;
+ ret = true;
+ }
+
+ if (!float.IsNaN(Metalness) && !((float)row.DawntrailMetalness()).NearEqual(Metalness))
+ {
+ row.DawntrailMetalnessWrite() = (Half)Metalness;
+ ret = true;
+ }
+
+ if (!float.IsNaN(Sheen) && !((float)row.DawntrailSheen()).NearEqual(Sheen))
+ {
+ row.DawntrailSheenWrite() = (Half)Sheen;
+ ret = true;
+ }
+
+ if (!float.IsNaN(SheenAperture) && !((float)row.DawntrailSheenAperture()).NearEqual(SheenAperture))
+ {
+ row.DawntrailSheenApertureWrite() = (Half)SheenAperture;
+ ret = true;
+ }
+
+ if (!float.IsNaN(SheenTint) && !((float)row.DawntrailSheenTint()).NearEqual(SheenTint))
+ {
+ row.DawntrailSheenTintWrite() = (Half)SheenTint;
+ ret = true;
+ }
+
+ break;
+ default: throw new NotImplementedException();
}
return ret;
}
+
+ public readonly ColorRow MergeOnto(ColorRow previous)
+ => new(Diffuse, Specular, Emissive, float.IsNaN(SpecularStrength) ? previous.SpecularStrength : SpecularStrength,
+ float.IsNaN(GlossStrength) ? previous.GlossStrength : GlossStrength, float.IsNaN(Roughness) ? previous.Roughness : Roughness,
+ float.IsNaN(Metalness) ? previous.Metalness : Metalness, float.IsNaN(Sheen) ? previous.Sheen : Sheen,
+ float.IsNaN(SheenTint) ? previous.SheenTint : SheenTint, float.IsNaN(SheenAperture) ? previous.SheenAperture : SheenAperture);
+
+ public readonly bool IsPartial(Mode mode)
+ => mode switch
+ {
+ Mode.Legacy => float.IsNaN(SpecularStrength) || float.IsNaN(GlossStrength),
+ Mode.Dawntrail => float.IsNaN(Roughness)
+ || float.IsNaN(Metalness)
+ || float.IsNaN(Sheen)
+ || float.IsNaN(SheenTint)
+ || float.IsNaN(SheenAperture),
+ _ => throw new NotImplementedException(),
+ };
+
+ public readonly Mode GuessMode()
+ => float.IsNaN(Roughness) && float.IsNaN(Metalness) && float.IsNaN(Sheen) && float.IsNaN(SheenTint) && float.IsNaN(SheenAperture)
+ ? Mode.Legacy
+ : Mode.Dawntrail;
+
+ public override readonly string ToString()
+ => $"[ColorRow Diffuse={Diffuse} Specular={Specular} Emissive={Emissive} SpecularStrength={SpecularStrength} GlossStrength={GlossStrength} Roughness={Roughness} Metalness={Metalness} Sheen={Sheen} SheenTint={SheenTint} SheenAperture={SheenAperture}]";
}
internal static class ColorTableRowExtensions
@@ -103,19 +189,50 @@ internal static class ColorTableRowExtensions
internal static Half LegacyGloss(this in ColorTableRow row)
=> row[3];
+ internal static Half DawntrailSheen(this in ColorTableRow row)
+ => row[12];
+
+ internal static Half DawntrailSheenTint(this in ColorTableRow row)
+ => row[13];
+
+ internal static Half DawntrailSheenAperture(this in ColorTableRow row)
+ => row[14];
+
+ internal static Half DawntrailRoughness(this in ColorTableRow row)
+ => row[16];
+
+ internal static Half DawntrailMetalness(this in ColorTableRow row)
+ => row[18];
+
internal static ref Half LegacySpecularStrengthWrite(this ref ColorTableRow row)
=> ref row[7];
internal static ref Half LegacyGlossWrite(this ref ColorTableRow row)
=> ref row[3];
+
+ internal static ref Half DawntrailSheenWrite(this ref ColorTableRow row)
+ => ref row[12];
+
+ internal static ref Half DawntrailSheenTintWrite(this ref ColorTableRow row)
+ => ref row[13];
+
+ internal static ref Half DawntrailSheenApertureWrite(this ref ColorTableRow row)
+ => ref row[14];
+
+ internal static ref Half DawntrailRoughnessWrite(this ref ColorTableRow row)
+ => ref row[16];
+
+ internal static ref Half DawntrailMetalnessWrite(this ref ColorTableRow row)
+ => ref row[18];
}
[JsonConverter(typeof(Converter))]
-public struct MaterialValueDesign(ColorRow value, bool enabled, bool revert)
+public struct MaterialValueDesign(ColorRow value, bool enabled, bool revert, ColorRow.Mode mode)
{
- public ColorRow Value = value;
- public bool Enabled = enabled;
- public bool Revert = revert;
+ public ColorRow Value = value;
+ public bool Enabled = enabled;
+ public bool Revert = revert;
+ public ColorRow.Mode Mode = mode;
public readonly bool Apply(ref MaterialValueState state)
{
@@ -145,6 +262,8 @@ public struct MaterialValueDesign(ColorRow value, bool enabled, bool revert)
writer.WriteStartObject();
writer.WritePropertyName("Revert");
writer.WriteValue(value.Revert);
+ writer.WritePropertyName("Mode");
+ writer.WriteValue(value.Mode.ToString());
writer.WritePropertyName("DiffuseR");
writer.WriteValue(value.Value.Diffuse.X);
writer.WritePropertyName("DiffuseG");
@@ -157,16 +276,54 @@ public struct MaterialValueDesign(ColorRow value, bool enabled, bool revert)
writer.WriteValue(value.Value.Specular.Y);
writer.WritePropertyName("SpecularB");
writer.WriteValue(value.Value.Specular.Z);
- writer.WritePropertyName("SpecularA");
- writer.WriteValue(value.Value.SpecularStrength);
+ if (!float.IsNaN(value.Value.SpecularStrength))
+ {
+ 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);
+ if (!float.IsNaN(value.Value.GlossStrength))
+ {
+ writer.WritePropertyName("Gloss");
+ writer.WriteValue(value.Value.GlossStrength);
+ }
+
+ if (!float.IsNaN(value.Value.Roughness))
+ {
+ writer.WritePropertyName("Roughness");
+ writer.WriteValue(value.Value.Roughness);
+ }
+
+ if (!float.IsNaN(value.Value.Metalness))
+ {
+ writer.WritePropertyName("Metalness");
+ writer.WriteValue(value.Value.Metalness);
+ }
+
+ if (!float.IsNaN(value.Value.Sheen))
+ {
+ writer.WritePropertyName("Sheen");
+ writer.WriteValue(value.Value.Sheen);
+ }
+
+ if (!float.IsNaN(value.Value.SheenTint))
+ {
+ writer.WritePropertyName("SheenTint");
+ writer.WriteValue(value.Value.SheenTint);
+ }
+
+ if (!float.IsNaN(value.Value.SheenAperture))
+ {
+ writer.WritePropertyName("SheenAperture");
+ writer.WriteValue(value.Value.SheenAperture);
+ }
+
writer.WritePropertyName("Enabled");
writer.WriteValue(value.Enabled);
writer.WriteEndObject();
@@ -177,19 +334,26 @@ public struct MaterialValueDesign(ColorRow value, bool enabled, bool revert)
JsonSerializer serializer)
{
var obj = JObject.Load(reader);
- Set(ref existingValue.Revert, obj["Revert"]?.Value());
- Set(ref existingValue.Value.Diffuse.X, obj["DiffuseR"]?.Value());
- Set(ref existingValue.Value.Diffuse.Y, obj["DiffuseG"]?.Value());
- Set(ref existingValue.Value.Diffuse.Z, obj["DiffuseB"]?.Value());
- Set(ref existingValue.Value.Specular.X, obj["SpecularR"]?.Value());
- Set(ref existingValue.Value.Specular.Y, obj["SpecularG"]?.Value());
- Set(ref existingValue.Value.Specular.Z, obj["SpecularB"]?.Value());
- Set(ref existingValue.Value.SpecularStrength, obj["SpecularA"]?.Value());
- Set(ref existingValue.Value.Emissive.X, obj["EmissiveR"]?.Value());
- Set(ref existingValue.Value.Emissive.Y, obj["EmissiveG"]?.Value());
- Set(ref existingValue.Value.Emissive.Z, obj["EmissiveB"]?.Value());
- Set(ref existingValue.Value.GlossStrength, obj["Gloss"]?.Value());
- existingValue.Enabled = obj["Enabled"]?.Value() ?? false;
+ if (obj["Mode"]?.Value() is { } mode)
+ Enum.TryParse(mode, true, out existingValue.Mode);
+ Set(ref existingValue.Revert, obj["Revert"]?.Value());
+ Set(ref existingValue.Value.Diffuse.X, obj["DiffuseR"]?.Value());
+ Set(ref existingValue.Value.Diffuse.Y, obj["DiffuseG"]?.Value());
+ Set(ref existingValue.Value.Diffuse.Z, obj["DiffuseB"]?.Value());
+ Set(ref existingValue.Value.Specular.X, obj["SpecularR"]?.Value());
+ Set(ref existingValue.Value.Specular.Y, obj["SpecularG"]?.Value());
+ Set(ref existingValue.Value.Specular.Z, obj["SpecularB"]?.Value());
+ Set(ref existingValue.Value.Emissive.X, obj["EmissiveR"]?.Value());
+ Set(ref existingValue.Value.Emissive.Y, obj["EmissiveG"]?.Value());
+ Set(ref existingValue.Value.Emissive.Z, obj["EmissiveB"]?.Value());
+ existingValue.Value.SpecularStrength = obj["SpecularA"]?.Value() ?? float.NaN;
+ existingValue.Value.GlossStrength = obj["Gloss"]?.Value() ?? float.NaN;
+ existingValue.Value.Roughness = obj["Roughness"]?.Value() ?? float.NaN;
+ existingValue.Value.Metalness = obj["Metalness"]?.Value() ?? float.NaN;
+ existingValue.Value.Sheen = obj["Sheen"]?.Value() ?? float.NaN;
+ existingValue.Value.SheenTint = obj["SheenTint"]?.Value() ?? float.NaN;
+ existingValue.Value.SheenAperture = obj["SheenAperture"]?.Value() ?? float.NaN;
+ existingValue.Enabled = obj["Enabled"]?.Value() ?? false;
return existingValue;
static void Set(ref T target, T? value)
@@ -222,10 +386,13 @@ public struct MaterialValueState(
&& DrawData.Weapon == rhsData.Weapon
&& DrawData.Variant == rhsData.Variant
&& DrawData.Stains == rhsData.Stains
- && Game.NearEqual(rhsRow);
+ && rhsRow.NearEqual(Game, true);
public readonly MaterialValueDesign Convert()
- => new(Model, true, false);
+ => new(Model, true, false, Model.GuessMode());
+
+ public readonly MaterialValueState MergeOnto(in ColorRow previous)
+ => new(Game, Model.MergeOnto(previous), DrawData, Source);
}
public readonly struct MaterialValueManager
diff --git a/Glamourer/State/InternalStateEditor.cs b/Glamourer/State/InternalStateEditor.cs
index eb3403d..1c41769 100644
--- a/Glamourer/State/InternalStateEditor.cs
+++ b/Glamourer/State/InternalStateEditor.cs
@@ -253,7 +253,7 @@ public class InternalStateEditor(
}
// Update if edited.
- state.Materials.UpdateValue(index, newValue, out _);
+ state.Materials.UpdateValue(index, newValue.MergeOnto(old.Model), out _);
return true;
}
diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs
index cbce7d5..ad77853 100644
--- a/Glamourer/State/StateEditor.cs
+++ b/Glamourer/State/StateEditor.cs
@@ -231,7 +231,7 @@ public class StateEditor(
Glamourer.Log.Verbose(
$"Set material value in state {state.Identifier.Incognito(null)} from {oldValue} to {newValue.Game}. [Affecting {actors.ToLazyString("nothing")}.]");
StateChanged.Invoke(StateChangeType.MaterialValue, settings.Source, state, actors,
- new MaterialTransaction(index, oldValue, newValue.Game));
+ new MaterialTransaction(index, oldValue, newValue.Game, null, null));
}
public void ResetMaterialValue(object data, MaterialValueIndex index, ApplySettings settings)
@@ -243,7 +243,8 @@ public class StateEditor(
var actors = Applier.ChangeMaterialValue(state, index, true);
Glamourer.Log.Verbose(
$"Reset material value in state {state.Identifier.Incognito(null)} to game value. [Affecting {actors.ToLazyString("nothing")}.]");
- StateChanged.Invoke(StateChangeType.MaterialValue, settings.Source, state, actors, new MaterialTransaction(index, null, null));
+ StateChanged.Invoke(StateChangeType.MaterialValue, settings.Source, state, actors,
+ new MaterialTransaction(index, null, null, null, null));
}
///