From 8140d085575aab238d5d844561afd8926d413d2d Mon Sep 17 00:00:00 2001 From: Ridan Vandenbergh Date: Mon, 4 Aug 2025 20:19:19 +0200 Subject: [PATCH] Add vertex material types for usages of 2 colour attributes --- Penumbra/Import/Models/Export/MeshExporter.cs | 83 +++- .../Import/Models/Export/VertexFragment.cs | 450 ++++++++++++++++++ 2 files changed, 523 insertions(+), 10 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 2e41f65a..6ea2b284 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -390,23 +390,30 @@ public class MeshExporter } } + usages.TryGetValue(MdlFile.VertexUsage.Color, out var colours); + var nColors = colours?.Count ?? 0; + var materialUsages = ( uvCount, - usages.ContainsKey(MdlFile.VertexUsage.Color) + nColors ); return materialUsages switch { - (3, true) => typeof(VertexTexture3ColorFfxiv), - (3, false) => typeof(VertexTexture3), - (2, true) => typeof(VertexTexture2ColorFfxiv), - (2, false) => typeof(VertexTexture2), - (1, true) => typeof(VertexTexture1ColorFfxiv), - (1, false) => typeof(VertexTexture1), - (0, true) => typeof(VertexColorFfxiv), - (0, false) => typeof(VertexEmpty), + (3, 2) => typeof(VertexTexture3Color2Ffxiv), + (3, 1) => typeof(VertexTexture3ColorFfxiv), + (3, 0) => typeof(VertexTexture3), + (2, 2) => typeof(VertexTexture2Color2Ffxiv), + (2, 1) => typeof(VertexTexture2ColorFfxiv), + (2, 0) => typeof(VertexTexture2), + (1, 2) => typeof(VertexTexture1Color2Ffxiv), + (1, 1) => typeof(VertexTexture1ColorFfxiv), + (1, 0) => typeof(VertexTexture1), + (0, 2) => typeof(VertexColor2Ffxiv), + (0, 1) => typeof(VertexColorFfxiv), + (0, 0) => typeof(VertexEmpty), - _ => throw _notifier.Exception($"Unhandled UV count of {uvCount} encountered."), + _ => throw _notifier.Exception($"Unhandled UV/color count of {uvCount}/{nColors} encountered."), }; } @@ -419,6 +426,12 @@ public class MeshExporter if (_materialType == typeof(VertexColorFfxiv)) return new VertexColorFfxiv(ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))); + if (_materialType == typeof(VertexColor2Ffxiv)) + { + var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color); + return new VertexColor2Ffxiv(ToVector4(color0), ToVector4(color1)); + } + if (_materialType == typeof(VertexTexture1)) return new VertexTexture1(ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV))); @@ -428,6 +441,16 @@ public class MeshExporter ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)) ); + if (_materialType == typeof(VertexTexture1Color2Ffxiv)) + { + var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color); + return new VertexTexture1Color2Ffxiv( + ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)), + ToVector4(color0), + ToVector4(color1) + ); + } + // XIV packs two UVs into a single vec4 attribute. if (_materialType == typeof(VertexTexture2)) @@ -448,6 +471,20 @@ public class MeshExporter ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)) ); } + + if (_materialType == typeof(VertexTexture2Color2Ffxiv)) + { + var uv = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)); + var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color); + + return new VertexTexture2Color2Ffxiv( + new Vector2(uv.X, uv.Y), + new Vector2(uv.Z, uv.W), + ToVector4(color0), + ToVector4(color1) + ); + } + if (_materialType == typeof(VertexTexture3)) { // Not 100% sure about this @@ -472,6 +509,21 @@ public class MeshExporter ); } + if (_materialType == typeof(VertexTexture3Color2Ffxiv)) + { + var uv0 = ToVector4(attributes[MdlFile.VertexUsage.UV][0]); + var uv1 = ToVector4(attributes[MdlFile.VertexUsage.UV][1]); + var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color); + + return new VertexTexture3Color2Ffxiv( + new Vector2(uv0.X, uv0.Y), + new Vector2(uv0.Z, uv0.W), + new Vector2(uv1.X, uv1.Y), + ToVector4(color0), + ToVector4(color1) + ); + } + throw _notifier.Exception($"Unknown material type {_skinningType}"); } @@ -537,6 +589,17 @@ public class MeshExporter return list[0]; } + + /// Check that the list has length 2 for any case where this is expected and return both entries. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private (T First, T Second) GetBothSafe(IReadOnlyDictionary> attributes, MdlFile.VertexUsage usage) + { + var list = attributes[usage]; + if (list.Count != 2) + throw _notifier.Exception($"{list.Count} usage indices encountered for {usage}, but expected 2."); + + return (list[0], list[1]); + } /// Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. private static Vector2 ToVector2(object data) diff --git a/Penumbra/Import/Models/Export/VertexFragment.cs b/Penumbra/Import/Models/Export/VertexFragment.cs index 56495f2f..463c59fc 100644 --- a/Penumbra/Import/Models/Export/VertexFragment.cs +++ b/Penumbra/Import/Models/Export/VertexFragment.cs @@ -84,6 +84,103 @@ public struct VertexColorFfxiv(Vector4 ffxivColor) : IVertexCustom } } +public struct VertexColor2Ffxiv(Vector4 ffxivColor0, Vector4 ffxivColor1) : IVertexCustom +{ + public IEnumerable> GetEncodingAttributes() + { + yield return new KeyValuePair("_FFXIV_COLOR_0", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + yield return new KeyValuePair("_FFXIV_COLOR_1", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + } + + public Vector4 FfxivColor0 = ffxivColor0; + public Vector4 FfxivColor1 = ffxivColor1; + + public int MaxColors + => 0; + + public int MaxTextCoords + => 0; + + private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"]; + + public IEnumerable CustomAttributes + => CustomNames; + + public void Add(in VertexMaterialDelta delta) + { } + + public VertexMaterialDelta Subtract(IVertexMaterial baseValue) + => new(Vector4.Zero, Vector4.Zero, Vector2.Zero, Vector2.Zero); + + public Vector2 GetTexCoord(int index) + => throw new ArgumentOutOfRangeException(nameof(index)); + + public void SetTexCoord(int setIndex, Vector2 coord) + { } + + public bool TryGetCustomAttribute(string attributeName, out object? value) + { + switch (attributeName) + { + case "_FFXIV_COLOR_0": + value = FfxivColor0; + return true; + + case "_FFXIV_COLOR_1": + value = FfxivColor1; + return true; + + default: + value = null; + return false; + } + } + + public void SetCustomAttribute(string attributeName, object value) + { + switch (attributeName) + { + case "_FFXIV_COLOR_0" when value is Vector4 valueVector4: + FfxivColor0 = valueVector4; + break; + case "_FFXIV_COLOR_1" when value is Vector4 valueVector4: + FfxivColor1 = valueVector4; + break; + } + } + + public Vector4 GetColor(int index) + => throw new ArgumentOutOfRangeException(nameof(index)); + + public void SetColor(int setIndex, Vector4 color) + { } + + public void Validate() + { + var components = new[] + { + FfxivColor0.X, + FfxivColor0.Y, + FfxivColor0.Z, + FfxivColor0.W, + }; + if (components.Any(component => component is < 0 or > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor0)); + components = + [ + FfxivColor1.X, + FfxivColor1.Y, + FfxivColor1.Z, + FfxivColor1.W, + ]; + if (components.Any(component => component is < 0 or > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor1)); + } +} + + public struct VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) : IVertexCustom { public IEnumerable> GetEncodingAttributes() @@ -172,6 +269,118 @@ public struct VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) : } } +public struct VertexTexture1Color2Ffxiv(Vector2 texCoord0, Vector4 ffxivColor0, Vector4 ffxivColor1) : IVertexCustom +{ + public IEnumerable> GetEncodingAttributes() + { + yield return new KeyValuePair("TEXCOORD_0", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("_FFXIV_COLOR_0", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + yield return new KeyValuePair("_FFXIV_COLOR_1", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + } + + public Vector2 TexCoord0 = texCoord0; + + public Vector4 FfxivColor0 = ffxivColor0; + public Vector4 FfxivColor1 = ffxivColor1; + + public int MaxColors + => 0; + + public int MaxTextCoords + => 1; + + private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"]; + + public IEnumerable CustomAttributes + => CustomNames; + + public void Add(in VertexMaterialDelta delta) + { + TexCoord0 += delta.TexCoord0Delta; + } + + public VertexMaterialDelta Subtract(IVertexMaterial baseValue) + => new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), Vector2.Zero); + + public Vector2 GetTexCoord(int index) + => index switch + { + 0 => TexCoord0, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + + public void SetTexCoord(int setIndex, Vector2 coord) + { + if (setIndex == 0) + TexCoord0 = coord; + if (setIndex >= 1) + throw new ArgumentOutOfRangeException(nameof(setIndex)); + } + + public bool TryGetCustomAttribute(string attributeName, out object? value) + { + switch (attributeName) + { + case "_FFXIV_COLOR_0": + value = FfxivColor0; + return true; + + case "_FFXIV_COLOR_1": + value = FfxivColor1; + return true; + + default: + value = null; + return false; + } + } + + public void SetCustomAttribute(string attributeName, object value) + { + switch (attributeName) + { + case "_FFXIV_COLOR_0" when value is Vector4 valueVector4: + FfxivColor0 = valueVector4; + break; + case "_FFXIV_COLOR_1" when value is Vector4 valueVector4: + FfxivColor1 = valueVector4; + break; + } + } + + public Vector4 GetColor(int index) + => throw new ArgumentOutOfRangeException(nameof(index)); + + public void SetColor(int setIndex, Vector4 color) + { } + + public void Validate() + { + var components = new[] + { + FfxivColor0.X, + FfxivColor0.Y, + FfxivColor0.Z, + FfxivColor0.W, + }; + if (components.Any(component => component is < 0 or > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor0)); + components = + [ + FfxivColor1.X, + FfxivColor1.Y, + FfxivColor1.Z, + FfxivColor1.W, + ]; + if (components.Any(component => component is < 0 or > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor1)); + } +} + + public struct VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor) : IVertexCustom { public IEnumerable> GetEncodingAttributes() @@ -266,6 +475,124 @@ public struct VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vec } } +public struct VertexTexture2Color2Ffxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor0, Vector4 ffxivColor1) : IVertexCustom +{ + public IEnumerable> GetEncodingAttributes() + { + yield return new KeyValuePair("TEXCOORD_0", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("TEXCOORD_1", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("_FFXIV_COLOR_0", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + yield return new KeyValuePair("_FFXIV_COLOR_1", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + } + + public Vector2 TexCoord0 = texCoord0; + public Vector2 TexCoord1 = texCoord1; + public Vector4 FfxivColor0 = ffxivColor0; + public Vector4 FfxivColor1 = ffxivColor1; + + public int MaxColors + => 0; + + public int MaxTextCoords + => 2; + + private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"]; + + public IEnumerable CustomAttributes + => CustomNames; + + public void Add(in VertexMaterialDelta delta) + { + TexCoord0 += delta.TexCoord0Delta; + TexCoord1 += delta.TexCoord1Delta; + } + + public VertexMaterialDelta Subtract(IVertexMaterial baseValue) + => new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1)); + + public Vector2 GetTexCoord(int index) + => index switch + { + 0 => TexCoord0, + 1 => TexCoord1, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + + public void SetTexCoord(int setIndex, Vector2 coord) + { + if (setIndex == 0) + TexCoord0 = coord; + if (setIndex == 1) + TexCoord1 = coord; + if (setIndex >= 2) + throw new ArgumentOutOfRangeException(nameof(setIndex)); + } + + public bool TryGetCustomAttribute(string attributeName, out object? value) + { + switch (attributeName) + { + case "_FFXIV_COLOR_0": + value = FfxivColor0; + return true; + + case "_FFXIV_COLOR_1": + value = FfxivColor1; + return true; + + default: + value = null; + return false; + } + } + + public void SetCustomAttribute(string attributeName, object value) + { + switch (attributeName) + { + case "_FFXIV_COLOR_0" when value is Vector4 valueVector4: + FfxivColor0 = valueVector4; + break; + case "_FFXIV_COLOR_1" when value is Vector4 valueVector4: + FfxivColor1 = valueVector4; + break; + } + } + + public Vector4 GetColor(int index) + => throw new ArgumentOutOfRangeException(nameof(index)); + + public void SetColor(int setIndex, Vector4 color) + { } + + public void Validate() + { + var components = new[] + { + FfxivColor0.X, + FfxivColor0.Y, + FfxivColor0.Z, + FfxivColor0.W, + }; + if (components.Any(component => component is < 0 or > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor0)); + components = + [ + FfxivColor1.X, + FfxivColor1.Y, + FfxivColor1.Z, + FfxivColor1.W, + ]; + if (components.Any(component => component is < 0 or > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor1)); + } + +} + public struct VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor) : IVertexCustom { @@ -367,3 +694,126 @@ public struct VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vec throw new ArgumentOutOfRangeException(nameof(FfxivColor)); } } + +public struct VertexTexture3Color2Ffxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor0, Vector4 ffxivColor1) + : IVertexCustom +{ + public IEnumerable> GetEncodingAttributes() + { + yield return new KeyValuePair("TEXCOORD_0", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("TEXCOORD_1", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("_FFXIV_COLOR_0", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + yield return new KeyValuePair("_FFXIV_COLOR_1", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + } + + public Vector2 TexCoord0 = texCoord0; + public Vector2 TexCoord1 = texCoord1; + public Vector2 TexCoord2 = texCoord2; + public Vector4 FfxivColor0 = ffxivColor0; + public Vector4 FfxivColor1 = ffxivColor1; + + public int MaxColors + => 0; + + public int MaxTextCoords + => 3; + + private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"]; + + public IEnumerable CustomAttributes + => CustomNames; + + public void Add(in VertexMaterialDelta delta) + { + TexCoord0 += delta.TexCoord0Delta; + TexCoord1 += delta.TexCoord1Delta; + TexCoord2 += delta.TexCoord2Delta; + } + + public VertexMaterialDelta Subtract(IVertexMaterial baseValue) + => new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1)); + + public Vector2 GetTexCoord(int index) + => index switch + { + 0 => TexCoord0, + 1 => TexCoord1, + 2 => TexCoord2, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + + public void SetTexCoord(int setIndex, Vector2 coord) + { + if (setIndex == 0) + TexCoord0 = coord; + if (setIndex == 1) + TexCoord1 = coord; + if (setIndex == 2) + TexCoord2 = coord; + if (setIndex >= 3) + throw new ArgumentOutOfRangeException(nameof(setIndex)); + } + + public bool TryGetCustomAttribute(string attributeName, out object? value) + { + switch (attributeName) + { + case "_FFXIV_COLOR_0": + value = FfxivColor0; + return true; + + case "_FFXIV_COLOR_1": + value = FfxivColor1; + return true; + + default: + value = null; + return false; + } + } + + public void SetCustomAttribute(string attributeName, object value) + { + switch (attributeName) + { + case "_FFXIV_COLOR_0" when value is Vector4 valueVector4: + FfxivColor0 = valueVector4; + break; + case "_FFXIV_COLOR_1" when value is Vector4 valueVector4: + FfxivColor1 = valueVector4; + break; + } + } + + public Vector4 GetColor(int index) + => throw new ArgumentOutOfRangeException(nameof(index)); + + public void SetColor(int setIndex, Vector4 color) + { } + + public void Validate() + { + var components = new[] + { + FfxivColor0.X, + FfxivColor0.Y, + FfxivColor0.Z, + FfxivColor0.W, + }; + if (components.Any(component => component is < 0 or > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor0)); + components = + [ + FfxivColor1.X, + FfxivColor1.Y, + FfxivColor1.Z, + FfxivColor1.W, + ]; + if (components.Any(component => component is < 0 or > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor1)); + } +}