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));
+ }
+}