mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2025-12-15 05:04:16 +01:00
433 lines
15 KiB
C#
433 lines
15 KiB
C#
global using StateMaterialManager = Glamourer.Interop.Material.MaterialValueManager<Glamourer.Interop.Material.MaterialValueState>;
|
|
global using DesignMaterialManager = Glamourer.Interop.Material.MaterialValueManager<Glamourer.Interop.Material.MaterialValueDesign>;
|
|
using Glamourer.GameData;
|
|
using Glamourer.State;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using Penumbra.GameData.Files.MaterialStructs;
|
|
using Penumbra.GameData.Structs;
|
|
|
|
|
|
namespace Glamourer.Interop.Material;
|
|
|
|
/// <summary> Values are not squared. </summary>
|
|
public struct ColorRow(Vector3 diffuse, Vector3 specular, Vector3 emissive, float specularStrength, float glossStrength)
|
|
{
|
|
public enum Mode
|
|
{
|
|
Legacy,
|
|
Dawntrail,
|
|
}
|
|
|
|
public static readonly ColorRow Empty = new(Vector3.Zero, Vector3.Zero, Vector3.Zero, 1f, 1f);
|
|
|
|
public Vector3 Diffuse = diffuse;
|
|
public Vector3 Specular = specular;
|
|
public Vector3 Emissive = emissive;
|
|
public float SpecularStrength = specularStrength;
|
|
public float GlossStrength = glossStrength;
|
|
|
|
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 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);
|
|
|
|
private static Vector3 Square(Vector3 value)
|
|
=> new(Square(value.X), Square(value.Y), Square(value.Z));
|
|
|
|
private static float Square(float value)
|
|
=> value < 0 ? -value * value : value * value;
|
|
|
|
private static Vector3 Root(Vector3 value)
|
|
=> new(Root(value.X), Root(value.Y), Root(value.Z));
|
|
|
|
private static float Root(float value)
|
|
=> value < 0 ? MathF.Sqrt(-value) : MathF.Sqrt(value);
|
|
|
|
public readonly bool Apply(ref ColorTableRow row, Mode mode)
|
|
{
|
|
var ret = false;
|
|
var d = Square(Diffuse);
|
|
if (!((Vector3)row.DiffuseColor).NearEqual(d))
|
|
{
|
|
row.DiffuseColor = (HalfColor)d;
|
|
ret = true;
|
|
}
|
|
|
|
var s = Square(Specular);
|
|
if (!((Vector3)row.SpecularColor).NearEqual(s))
|
|
{
|
|
row.SpecularColor = (HalfColor)s;
|
|
ret = true;
|
|
}
|
|
|
|
var e = Square(Emissive);
|
|
if (!((Vector3)row.EmissiveColor).NearEqual(e))
|
|
{
|
|
row.EmissiveColor = (HalfColor)e;
|
|
ret = true;
|
|
}
|
|
|
|
if (mode is Mode.Legacy)
|
|
{
|
|
if (!((float)row.LegacySpecularStrength()).NearEqual(SpecularStrength))
|
|
{
|
|
row.LegacySpecularStrengthWrite() = (Half)SpecularStrength;
|
|
ret = true;
|
|
}
|
|
|
|
if (!((float)row.LegacyGloss()).NearEqual(GlossStrength))
|
|
{
|
|
row.LegacyGlossWrite() = (Half)GlossStrength;
|
|
ret = true;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
internal static class ColorTableRowExtensions
|
|
{
|
|
internal static Half LegacySpecularStrength(this in ColorTableRow row)
|
|
=> row[7];
|
|
|
|
internal static Half LegacyGloss(this in ColorTableRow row)
|
|
=> row[3];
|
|
|
|
internal static ref Half LegacySpecularStrengthWrite(this ref ColorTableRow row)
|
|
=> ref row[7];
|
|
|
|
internal static ref Half LegacyGlossWrite(this ref ColorTableRow row)
|
|
=> ref row[3];
|
|
}
|
|
|
|
[JsonConverter(typeof(Converter))]
|
|
public struct MaterialValueDesign(ColorRow value, bool enabled, bool revert)
|
|
{
|
|
public ColorRow Value = value;
|
|
public bool Enabled = enabled;
|
|
public bool Revert = revert;
|
|
|
|
public readonly bool Apply(ref MaterialValueState state)
|
|
{
|
|
if (!Enabled)
|
|
return false;
|
|
|
|
if (Revert)
|
|
{
|
|
if (state.Model.NearEqual(state.Game))
|
|
return false;
|
|
|
|
state.Model = state.Game;
|
|
return true;
|
|
}
|
|
|
|
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("Revert");
|
|
writer.WriteValue(value.Revert);
|
|
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.Revert, obj["Revert"]?.Value<bool>());
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
{ }
|
|
|
|
public ColorRow Game = game;
|
|
public ColorRow Model = model;
|
|
public readonly CharacterWeapon DrawData = drawData;
|
|
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.Stains == rhsData.Stains
|
|
&& Game.NearEqual(rhsRow);
|
|
|
|
public readonly MaterialValueDesign Convert()
|
|
=> new(Model, true, false);
|
|
}
|
|
|
|
public readonly struct MaterialValueManager<T>
|
|
{
|
|
private readonly List<(uint Key, T Value)> _values = [];
|
|
|
|
public MaterialValueManager()
|
|
{ }
|
|
|
|
public void Clear()
|
|
=> _values.Clear();
|
|
|
|
public MaterialValueManager<T> Clone()
|
|
{
|
|
var ret = new MaterialValueManager<T>();
|
|
ret._values.AddRange(_values);
|
|
return ret;
|
|
}
|
|
|
|
public bool TryGetValue(MaterialValueIndex index, out T value)
|
|
=> TryGetValue(index.Key, out value);
|
|
|
|
public bool TryGetValue(uint key, out T value)
|
|
{
|
|
if (_values.Count == 0)
|
|
{
|
|
value = default!;
|
|
return false;
|
|
}
|
|
|
|
var idx = Search(key);
|
|
if (idx >= 0)
|
|
{
|
|
value = _values[idx].Value;
|
|
return true;
|
|
}
|
|
|
|
value = default!;
|
|
return false;
|
|
}
|
|
|
|
public bool TryAddValue(MaterialValueIndex index, in T value)
|
|
=> TryAddValue(index.Key, value);
|
|
|
|
public bool TryAddValue(uint key, in T value)
|
|
{
|
|
var idx = Search(key);
|
|
if (idx >= 0)
|
|
return false;
|
|
|
|
_values.Insert(~idx, (key, value));
|
|
return true;
|
|
}
|
|
|
|
public bool RemoveValue(MaterialValueIndex index)
|
|
=> RemoveValue(index.Key);
|
|
|
|
public bool RemoveValue(uint key)
|
|
{
|
|
if (_values.Count == 0)
|
|
return false;
|
|
|
|
var idx = Search(key);
|
|
if (idx < 0)
|
|
return false;
|
|
|
|
_values.RemoveAt(idx);
|
|
return true;
|
|
}
|
|
|
|
public void AddOrUpdateValue(MaterialValueIndex index, in T value)
|
|
=> AddOrUpdateValue(index.Key, value);
|
|
|
|
public void AddOrUpdateValue(uint key, in T value)
|
|
{
|
|
var idx = Search(key);
|
|
if (idx < 0)
|
|
_values.Insert(~idx, (key, value));
|
|
else
|
|
_values[idx] = (key, value);
|
|
}
|
|
|
|
public bool UpdateValue(MaterialValueIndex index, in T value, out T oldValue)
|
|
=> UpdateValue(index.Key, value, out oldValue);
|
|
|
|
public bool UpdateValue(uint key, in T value, out T oldValue)
|
|
{
|
|
if (_values.Count == 0)
|
|
{
|
|
oldValue = default!;
|
|
return false;
|
|
}
|
|
|
|
var idx = Search(key);
|
|
if (idx < 0)
|
|
{
|
|
oldValue = default!;
|
|
return false;
|
|
}
|
|
|
|
oldValue = _values[idx].Value;
|
|
_values[idx] = (key, value);
|
|
return true;
|
|
}
|
|
|
|
public IReadOnlyList<(uint Key, T Value)> Values
|
|
=> _values;
|
|
|
|
public int RemoveValues(MaterialValueIndex min, MaterialValueIndex max)
|
|
{
|
|
var (minIdx, maxIdx) = MaterialValueManager.GetMinMax<T>(CollectionsMarshal.AsSpan(_values), min.Key, max.Key);
|
|
if (minIdx < 0)
|
|
return 0;
|
|
|
|
var count = maxIdx - minIdx;
|
|
_values.RemoveRange(minIdx, count);
|
|
return count;
|
|
}
|
|
|
|
public ReadOnlySpan<(uint Key, T Value)> GetValues(MaterialValueIndex min, MaterialValueIndex max)
|
|
=> MaterialValueManager.Filter<T>(CollectionsMarshal.AsSpan(_values), min, max);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
|
private int Search(uint key)
|
|
=> _values.BinarySearch((key, default!), MaterialValueManager.Comparer<T>.Instance);
|
|
}
|
|
|
|
public static class MaterialValueManager
|
|
{
|
|
internal class Comparer<T> : IComparer<(uint Key, T Value)>
|
|
{
|
|
public static readonly Comparer<T> Instance = new();
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
|
int IComparer<(uint Key, T Value)>.Compare((uint Key, T Value) x, (uint Key, T Value) y)
|
|
=> x.Key.CompareTo(y.Key);
|
|
}
|
|
|
|
public static bool GetSpecific<T>(ReadOnlySpan<(uint Key, T Value)> values, MaterialValueIndex index, out T ret)
|
|
{
|
|
var idx = values.BinarySearch((index.Key, default!), Comparer<T>.Instance);
|
|
if (idx < 0)
|
|
{
|
|
ret = default!;
|
|
return false;
|
|
}
|
|
|
|
ret = values[idx].Value;
|
|
return true;
|
|
}
|
|
|
|
public static ReadOnlySpan<(uint Key, T Value)> Filter<T>(ReadOnlySpan<(uint Key, T Value)> values, MaterialValueIndex min,
|
|
MaterialValueIndex max)
|
|
{
|
|
var (minIdx, maxIdx) = GetMinMax(values, min.Key, max.Key);
|
|
return minIdx < 0 ? [] : values[minIdx..(maxIdx + 1)];
|
|
}
|
|
|
|
/// <summary> Obtain the minimum index and maximum index for a minimum and maximum key. </summary>
|
|
internal static (int MinIdx, int MaxIdx) GetMinMax<T>(ReadOnlySpan<(uint Key, T Value)> values, uint minKey, uint maxKey)
|
|
{
|
|
// Find the minimum index by binary search.
|
|
var idx = values.BinarySearch((minKey, default!), Comparer<T>.Instance);
|
|
var minIdx = idx;
|
|
|
|
// If the key does not exist, check if it is an invalid range or set it correctly.
|
|
if (minIdx < 0)
|
|
{
|
|
minIdx = ~minIdx;
|
|
if (minIdx == values.Length || values[minIdx].Key > maxKey)
|
|
return (-1, -1);
|
|
|
|
idx = minIdx;
|
|
}
|
|
else
|
|
{
|
|
// If it does exist, go upwards until the first key is reached that is actually smaller.
|
|
while (minIdx > 0 && values[minIdx - 1].Key >= minKey)
|
|
--minIdx;
|
|
}
|
|
|
|
// Check if the range can be valid.
|
|
if (values[minIdx].Key < minKey || values[minIdx].Key > maxKey)
|
|
return (-1, -1);
|
|
|
|
|
|
// Do pretty much the same but in the other direction with the maximum key.
|
|
var maxIdx = values[idx..].BinarySearch((maxKey, default!), Comparer<T>.Instance);
|
|
if (maxIdx < 0)
|
|
{
|
|
maxIdx = ~maxIdx + idx;
|
|
return maxIdx > minIdx ? (minIdx, maxIdx - 1) : (-1, -1);
|
|
}
|
|
|
|
maxIdx += idx;
|
|
|
|
while (maxIdx < values.Length - 1 && values[maxIdx + 1].Key <= maxKey)
|
|
++maxIdx;
|
|
|
|
if (values[maxIdx].Key < minKey || values[maxIdx].Key > maxKey)
|
|
return (-1, -1);
|
|
|
|
return (minIdx, maxIdx);
|
|
}
|
|
}
|