From ef26049c53d57cb5625f9dab476d5c61ae1c904b Mon Sep 17 00:00:00 2001 From: Adam Moy Date: Wed, 5 Feb 2025 11:25:09 -0600 Subject: [PATCH 1/9] Consider VertexElement's UsageIndex Allows VertexDeclarations to have multiple VertexElements of the same Type but different UsageIndex --- Penumbra/Import/Models/Export/MeshExporter.cs | 117 ++++++++++++------ .../Import/Models/Export/VertexFragment.cs | 109 ++++++++++++++++ 2 files changed, 187 insertions(+), 39 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 73160615..0dc8a9ac 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -82,13 +82,16 @@ public class MeshExporter if (skeleton != null) _boneIndexMap = BuildBoneIndexMap(skeleton.Value); + var usages = new Dictionary>(); - var usages = _mdl.VertexDeclarations[_meshIndex].VertexElements - .ToImmutableDictionary( - element => (MdlFile.VertexUsage)element.Usage, - element => (MdlFile.VertexType)element.Type - ); - + foreach (var element in _mdl.VertexDeclarations[_meshIndex].VertexElements) + { + if (!usages.ContainsKey((MdlFile.VertexUsage)element.Usage)) + { + usages.Add((MdlFile.VertexUsage)element.Usage, new Dictionary()); + } + usages[(MdlFile.VertexUsage)element.Usage][element.UsageIndex] = (MdlFile.VertexType)element.Type; + } _geometryType = GetGeometryType(usages); _materialType = GetMaterialType(usages); _skinningType = GetSkinningType(usages); @@ -104,14 +107,20 @@ public class MeshExporter /// Build a mapping between indices in this mesh's bone table (if any), and the glTF joint indices provided. private Dictionary? BuildBoneIndexMap(GltfSkeleton skeleton) { + Penumbra.Log.Debug("Building Bone Index Map"); // A BoneTableIndex of 255 means that this mesh is not skinned. if (XivMesh.BoneTableIndex == 255) + { + Penumbra.Log.Debug("BoneTableIndex was 255"); return null; + } var xivBoneTable = _mdl.BoneTables[XivMesh.BoneTableIndex]; var indexMap = new Dictionary(); // #TODO @ackwell maybe fix for V6 Models, I think this works fine. + Penumbra.Log.Debug($"Version is 5 {_mdl.Version == MdlFile.V5}"); + foreach (var (xivBoneIndex, tableIndex) in xivBoneTable.BoneIndex.Take((int)xivBoneTable.BoneCount).WithIndex()) { var boneName = _mdl.Bones[xivBoneIndex]; @@ -283,13 +292,20 @@ public class MeshExporter var vertices = new List(); - var attributes = new Dictionary(); + var attributes = new Dictionary>(); for (var vertexIndex = 0; vertexIndex < XivMesh.VertexCount; vertexIndex++) { attributes.Clear(); - foreach (var (usage, element) in sortedElements) - attributes[usage] = ReadVertexAttribute((MdlFile.VertexType)element.Type, streams[element.Stream]); + { + if (!attributes.TryGetValue(usage, out var value)) + { + value = new Dictionary(); + attributes[usage] = value; + } + + value[element.UsageIndex] = ReadVertexAttribute((MdlFile.VertexType)element.Type, streams[element.Stream]); + } var vertexGeometry = BuildVertexGeometry(attributes); var vertexMaterial = BuildVertexMaterial(attributes); @@ -320,7 +336,7 @@ public class MeshExporter } /// Get the vertex geometry type for this mesh's vertex usages. - private Type GetGeometryType(IReadOnlyDictionary usages) + private Type GetGeometryType(IReadOnlyDictionary> usages) { if (!usages.ContainsKey(MdlFile.VertexUsage.Position)) throw _notifier.Exception("Mesh does not contain position vertex elements."); @@ -335,28 +351,28 @@ public class MeshExporter } /// Build a geometry vertex from a vertex's attributes. - private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary attributes) + private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary> attributes) { if (_geometryType == typeof(VertexPosition)) return new VertexPosition( - ToVector3(attributes[MdlFile.VertexUsage.Position]) + ToVector3(attributes[MdlFile.VertexUsage.Position][0]) ); if (_geometryType == typeof(VertexPositionNormal)) return new VertexPositionNormal( - ToVector3(attributes[MdlFile.VertexUsage.Position]), - ToVector3(attributes[MdlFile.VertexUsage.Normal]) + ToVector3(attributes[MdlFile.VertexUsage.Position][0]), + ToVector3(attributes[MdlFile.VertexUsage.Normal][0]) ); if (_geometryType == typeof(VertexPositionNormalTangent)) { // (Bi)tangents are universally stored as ByteFloat4, which uses 0..1 to represent the full -1..1 range. // TODO: While this assumption is safe, it would be sensible to actually check. - var bitangent = ToVector4(attributes[MdlFile.VertexUsage.Tangent1]) * 2 - Vector4.One; + var bitangent = ToVector4(attributes[MdlFile.VertexUsage.Tangent1][0]) * 2 - Vector4.One; return new VertexPositionNormalTangent( - ToVector3(attributes[MdlFile.VertexUsage.Position]), - ToVector3(attributes[MdlFile.VertexUsage.Normal]), + ToVector3(attributes[MdlFile.VertexUsage.Position][0]), + ToVector3(attributes[MdlFile.VertexUsage.Normal][0]), bitangent ); } @@ -365,18 +381,23 @@ public class MeshExporter } /// Get the vertex material type for this mesh's vertex usages. - private Type GetMaterialType(IReadOnlyDictionary usages) + private Type GetMaterialType(IReadOnlyDictionary> usages) { var uvCount = 0; - if (usages.TryGetValue(MdlFile.VertexUsage.UV, out var type)) - uvCount = type switch + if (usages.TryGetValue(MdlFile.VertexUsage.UV, out var dict)) + { + foreach (var type in dict.Values) { - MdlFile.VertexType.Half2 => 1, - MdlFile.VertexType.Half4 => 2, - MdlFile.VertexType.Single2 => 1, - MdlFile.VertexType.Single4 => 2, - _ => throw _notifier.Exception($"Unexpected UV vertex type {type}."), - }; + uvCount += type switch + { + MdlFile.VertexType.Half2 => 1, + MdlFile.VertexType.Half4 => 2, + MdlFile.VertexType.Single2 => 1, + MdlFile.VertexType.Single4 => 2, + _ => throw _notifier.Exception($"Unexpected UV vertex type {type}."), + }; + } + } var materialUsages = ( uvCount, @@ -385,6 +406,8 @@ public class MeshExporter return materialUsages switch { + (3, true) => typeof(VertexTexture3ColorFfxiv), + (3, false) => typeof(VertexTexture3), (2, true) => typeof(VertexTexture2ColorFfxiv), (2, false) => typeof(VertexTexture2), (1, true) => typeof(VertexTexture1ColorFfxiv), @@ -397,28 +420,28 @@ public class MeshExporter } /// Build a material vertex from a vertex's attributes. - private IVertexMaterial BuildVertexMaterial(IReadOnlyDictionary attributes) + private IVertexMaterial BuildVertexMaterial(IReadOnlyDictionary> attributes) { if (_materialType == typeof(VertexEmpty)) return new VertexEmpty(); if (_materialType == typeof(VertexColorFfxiv)) - return new VertexColorFfxiv(ToVector4(attributes[MdlFile.VertexUsage.Color])); + return new VertexColorFfxiv(ToVector4(attributes[MdlFile.VertexUsage.Color][0])); if (_materialType == typeof(VertexTexture1)) - return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV])); + return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV][0])); if (_materialType == typeof(VertexTexture1ColorFfxiv)) return new VertexTexture1ColorFfxiv( - ToVector2(attributes[MdlFile.VertexUsage.UV]), - ToVector4(attributes[MdlFile.VertexUsage.Color]) + ToVector2(attributes[MdlFile.VertexUsage.UV][0]), + ToVector4(attributes[MdlFile.VertexUsage.Color][0]) ); // XIV packs two UVs into a single vec4 attribute. if (_materialType == typeof(VertexTexture2)) { - var uv = ToVector4(attributes[MdlFile.VertexUsage.UV]); + var uv = ToVector4(attributes[MdlFile.VertexUsage.UV][0]); return new VertexTexture2( new Vector2(uv.X, uv.Y), new Vector2(uv.Z, uv.W) @@ -427,11 +450,27 @@ public class MeshExporter if (_materialType == typeof(VertexTexture2ColorFfxiv)) { - var uv = ToVector4(attributes[MdlFile.VertexUsage.UV]); + var uv = ToVector4(attributes[MdlFile.VertexUsage.UV][0]); return new VertexTexture2ColorFfxiv( new Vector2(uv.X, uv.Y), new Vector2(uv.Z, uv.W), - ToVector4(attributes[MdlFile.VertexUsage.Color]) + ToVector4(attributes[MdlFile.VertexUsage.Color][0]) + ); + } + if (_materialType == typeof(VertexTexture3)) + { + throw _notifier.Exception("Unimplemented: Material Type is VertexTexture3"); + } + + if (_materialType == typeof(VertexTexture3ColorFfxiv)) + { + var uv0 = ToVector4(attributes[MdlFile.VertexUsage.UV][0]); + var uv1 = ToVector4(attributes[MdlFile.VertexUsage.UV][1]); + return new VertexTexture3ColorFfxiv( + new Vector2(uv0.X, uv0.Y), + new Vector2(uv0.Z, uv0.W), + new Vector2(uv1.X, uv1.Y), + ToVector4(attributes[MdlFile.VertexUsage.Color][0]) ); } @@ -439,11 +478,11 @@ public class MeshExporter } /// Get the vertex skinning type for this mesh's vertex usages. - private static Type GetSkinningType(IReadOnlyDictionary usages) + private static Type GetSkinningType(IReadOnlyDictionary> usages) { if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices)) { - if (usages[MdlFile.VertexUsage.BlendWeights] == MdlFile.VertexType.UShort4) + if (usages[MdlFile.VertexUsage.BlendWeights][0] == MdlFile.VertexType.UShort4) { return typeof(VertexJoints8); } @@ -457,7 +496,7 @@ public class MeshExporter } /// Build a skinning vertex from a vertex's attributes. - private IVertexSkinning BuildVertexSkinning(IReadOnlyDictionary attributes) + private IVertexSkinning BuildVertexSkinning(IReadOnlyDictionary> attributes) { if (_skinningType == typeof(VertexEmpty)) return new VertexEmpty(); @@ -467,8 +506,8 @@ public class MeshExporter if (_boneIndexMap == null) throw _notifier.Exception("Tried to build skinned vertex but no bone mappings are available."); - var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices]; - var weightsData = attributes[MdlFile.VertexUsage.BlendWeights]; + var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices][0]; + var weightsData = attributes[MdlFile.VertexUsage.BlendWeights][0]; var indices = ToByteArray(indiciesData); var weights = ToFloatArray(weightsData); diff --git a/Penumbra/Import/Models/Export/VertexFragment.cs b/Penumbra/Import/Models/Export/VertexFragment.cs index eff34d54..c9b97997 100644 --- a/Penumbra/Import/Models/Export/VertexFragment.cs +++ b/Penumbra/Import/Models/Export/VertexFragment.cs @@ -282,3 +282,112 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom throw new ArgumentOutOfRangeException(nameof(FfxivColor)); } } + +public struct VertexTexture3ColorFfxiv : 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("TEXCOORD_2", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("_FFXIV_COLOR", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + } + + public Vector2 TexCoord0; + public Vector2 TexCoord1; + public Vector2 TexCoord2; + public Vector4 FfxivColor; + + public int MaxColors + => 0; + + public int MaxTextCoords + => 3; + + private static readonly string[] CustomNames = ["_FFXIV_COLOR"]; + + public IEnumerable CustomAttributes + => CustomNames; + + public VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor) + { + TexCoord0 = texCoord0; + TexCoord1 = texCoord1; + TexCoord2 = texCoord2; + FfxivColor = ffxivColor; + } + + 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": + value = FfxivColor; + return true; + + default: + value = null; + return false; + } + } + + public void SetCustomAttribute(string attributeName, object value) + { + if (attributeName == "_FFXIV_COLOR" && value is Vector4 valueVector4) + FfxivColor = valueVector4; + } + + public Vector4 GetColor(int index) + => throw new ArgumentOutOfRangeException(nameof(index)); + + public void SetColor(int setIndex, Vector4 color) + { } + + public void Validate() + { + var components = new[] + { + FfxivColor.X, + FfxivColor.Y, + FfxivColor.Z, + FfxivColor.W, + }; + if (components.Any(component => component < 0 || component > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor)); + } +} From 2f0bf19d00f9047817fb8bbfba38e5471f14bb20 Mon Sep 17 00:00:00 2001 From: Adam Moy Date: Wed, 12 Feb 2025 09:48:22 -0600 Subject: [PATCH 2/9] Use First().Value --- Penumbra/Import/Models/Export/MeshExporter.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 0dc8a9ac..48f66177 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -355,24 +355,24 @@ public class MeshExporter { if (_geometryType == typeof(VertexPosition)) return new VertexPosition( - ToVector3(attributes[MdlFile.VertexUsage.Position][0]) + ToVector3(attributes[MdlFile.VertexUsage.Position].First().Value) ); if (_geometryType == typeof(VertexPositionNormal)) return new VertexPositionNormal( - ToVector3(attributes[MdlFile.VertexUsage.Position][0]), - ToVector3(attributes[MdlFile.VertexUsage.Normal][0]) + ToVector3(attributes[MdlFile.VertexUsage.Position].First().Value), + ToVector3(attributes[MdlFile.VertexUsage.Normal].First().Value) ); if (_geometryType == typeof(VertexPositionNormalTangent)) { // (Bi)tangents are universally stored as ByteFloat4, which uses 0..1 to represent the full -1..1 range. // TODO: While this assumption is safe, it would be sensible to actually check. - var bitangent = ToVector4(attributes[MdlFile.VertexUsage.Tangent1][0]) * 2 - Vector4.One; + var bitangent = ToVector4(attributes[MdlFile.VertexUsage.Tangent1].First().Value) * 2 - Vector4.One; return new VertexPositionNormalTangent( - ToVector3(attributes[MdlFile.VertexUsage.Position][0]), - ToVector3(attributes[MdlFile.VertexUsage.Normal][0]), + ToVector3(attributes[MdlFile.VertexUsage.Position].First().Value), + ToVector3(attributes[MdlFile.VertexUsage.Normal].First().Value), bitangent ); } @@ -426,22 +426,22 @@ public class MeshExporter return new VertexEmpty(); if (_materialType == typeof(VertexColorFfxiv)) - return new VertexColorFfxiv(ToVector4(attributes[MdlFile.VertexUsage.Color][0])); + return new VertexColorFfxiv(ToVector4(attributes[MdlFile.VertexUsage.Color].First().Value)); if (_materialType == typeof(VertexTexture1)) - return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV][0])); + return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV].First().Value)); if (_materialType == typeof(VertexTexture1ColorFfxiv)) return new VertexTexture1ColorFfxiv( - ToVector2(attributes[MdlFile.VertexUsage.UV][0]), - ToVector4(attributes[MdlFile.VertexUsage.Color][0]) + ToVector2(attributes[MdlFile.VertexUsage.UV].First().Value), + ToVector4(attributes[MdlFile.VertexUsage.Color].First().Value) ); // XIV packs two UVs into a single vec4 attribute. if (_materialType == typeof(VertexTexture2)) { - var uv = ToVector4(attributes[MdlFile.VertexUsage.UV][0]); + var uv = ToVector4(attributes[MdlFile.VertexUsage.UV].First().Value); return new VertexTexture2( new Vector2(uv.X, uv.Y), new Vector2(uv.Z, uv.W) @@ -450,11 +450,11 @@ public class MeshExporter if (_materialType == typeof(VertexTexture2ColorFfxiv)) { - var uv = ToVector4(attributes[MdlFile.VertexUsage.UV][0]); + var uv = ToVector4(attributes[MdlFile.VertexUsage.UV].First().Value); return new VertexTexture2ColorFfxiv( new Vector2(uv.X, uv.Y), new Vector2(uv.Z, uv.W), - ToVector4(attributes[MdlFile.VertexUsage.Color][0]) + ToVector4(attributes[MdlFile.VertexUsage.Color].First().Value) ); } if (_materialType == typeof(VertexTexture3)) @@ -470,7 +470,7 @@ public class MeshExporter new Vector2(uv0.X, uv0.Y), new Vector2(uv0.Z, uv0.W), new Vector2(uv1.X, uv1.Y), - ToVector4(attributes[MdlFile.VertexUsage.Color][0]) + ToVector4(attributes[MdlFile.VertexUsage.Color].First().Value) ); } @@ -482,7 +482,7 @@ public class MeshExporter { if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices)) { - if (usages[MdlFile.VertexUsage.BlendWeights][0] == MdlFile.VertexType.UShort4) + if (usages[MdlFile.VertexUsage.BlendWeights].First().Value == MdlFile.VertexType.UShort4) { return typeof(VertexJoints8); } @@ -506,8 +506,8 @@ public class MeshExporter if (_boneIndexMap == null) throw _notifier.Exception("Tried to build skinned vertex but no bone mappings are available."); - var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices][0]; - var weightsData = attributes[MdlFile.VertexUsage.BlendWeights][0]; + var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices].First().Value; + var weightsData = attributes[MdlFile.VertexUsage.BlendWeights].First().Value; var indices = ToByteArray(indiciesData); var weights = ToFloatArray(weightsData); From 579969a9e107e9a7b4b7adfef5f419cbed648899 Mon Sep 17 00:00:00 2001 From: Adam Moy Date: Wed, 12 Feb 2025 12:45:15 -0600 Subject: [PATCH 3/9] Using LINQ And also change types from using LINQ --- Penumbra/Import/Models/Export/MeshExporter.cs | 91 +++++++++---------- 1 file changed, 44 insertions(+), 47 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 48f66177..fb88dfc3 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -82,16 +82,16 @@ public class MeshExporter if (skeleton != null) _boneIndexMap = BuildBoneIndexMap(skeleton.Value); - var usages = new Dictionary>(); - foreach (var element in _mdl.VertexDeclarations[_meshIndex].VertexElements) - { - if (!usages.ContainsKey((MdlFile.VertexUsage)element.Usage)) - { - usages.Add((MdlFile.VertexUsage)element.Usage, new Dictionary()); - } - usages[(MdlFile.VertexUsage)element.Usage][element.UsageIndex] = (MdlFile.VertexType)element.Type; - } + var usages = _mdl.VertexDeclarations[_meshIndex].VertexElements + .GroupBy(ele => (MdlFile.VertexUsage)ele.Usage, ele => ele) + .ToImmutableDictionary( + g => g.Key, + g => g.OrderBy(ele => ele.UsageIndex) // OrderBy UsageIndex is probably unnecessary as they're probably already be in order + .Select(ele => (MdlFile.VertexType)ele.Type) + .ToList() + ); + _geometryType = GetGeometryType(usages); _materialType = GetMaterialType(usages); _skinningType = GetSkinningType(usages); @@ -287,25 +287,22 @@ public class MeshExporter var sortedElements = _mdl.VertexDeclarations[_meshIndex].VertexElements .OrderBy(element => element.Offset) - .Select(element => ((MdlFile.VertexUsage)element.Usage, element)) .ToList(); - var vertices = new List(); - var attributes = new Dictionary>(); + var attributes = new Dictionary>(); for (var vertexIndex = 0; vertexIndex < XivMesh.VertexCount; vertexIndex++) { attributes.Clear(); - foreach (var (usage, element) in sortedElements) - { - if (!attributes.TryGetValue(usage, out var value)) - { - value = new Dictionary(); - attributes[usage] = value; - } - - value[element.UsageIndex] = ReadVertexAttribute((MdlFile.VertexType)element.Type, streams[element.Stream]); - } + attributes = sortedElements + .GroupBy(element => element.Usage) + .ToDictionary( + x => (MdlFile.VertexUsage)x.Key, + x => x.OrderBy(ele => ele.UsageIndex) // Once again, OrderBy UsageIndex is probably unnecessary + .Select(ele => ReadVertexAttribute((MdlFile.VertexType)ele.Type, streams[ele.Stream])) + .ToList() + ); + var vertexGeometry = BuildVertexGeometry(attributes); var vertexMaterial = BuildVertexMaterial(attributes); @@ -336,7 +333,7 @@ public class MeshExporter } /// Get the vertex geometry type for this mesh's vertex usages. - private Type GetGeometryType(IReadOnlyDictionary> usages) + private Type GetGeometryType(IReadOnlyDictionary> usages) { if (!usages.ContainsKey(MdlFile.VertexUsage.Position)) throw _notifier.Exception("Mesh does not contain position vertex elements."); @@ -351,28 +348,28 @@ public class MeshExporter } /// Build a geometry vertex from a vertex's attributes. - private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary> attributes) + private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary> attributes) { if (_geometryType == typeof(VertexPosition)) return new VertexPosition( - ToVector3(attributes[MdlFile.VertexUsage.Position].First().Value) + ToVector3(attributes[MdlFile.VertexUsage.Position].First()) ); if (_geometryType == typeof(VertexPositionNormal)) return new VertexPositionNormal( - ToVector3(attributes[MdlFile.VertexUsage.Position].First().Value), - ToVector3(attributes[MdlFile.VertexUsage.Normal].First().Value) + ToVector3(attributes[MdlFile.VertexUsage.Position].First()), + ToVector3(attributes[MdlFile.VertexUsage.Normal].First()) ); if (_geometryType == typeof(VertexPositionNormalTangent)) { // (Bi)tangents are universally stored as ByteFloat4, which uses 0..1 to represent the full -1..1 range. // TODO: While this assumption is safe, it would be sensible to actually check. - var bitangent = ToVector4(attributes[MdlFile.VertexUsage.Tangent1].First().Value) * 2 - Vector4.One; + var bitangent = ToVector4(attributes[MdlFile.VertexUsage.Tangent1].First()) * 2 - Vector4.One; return new VertexPositionNormalTangent( - ToVector3(attributes[MdlFile.VertexUsage.Position].First().Value), - ToVector3(attributes[MdlFile.VertexUsage.Normal].First().Value), + ToVector3(attributes[MdlFile.VertexUsage.Position].First()), + ToVector3(attributes[MdlFile.VertexUsage.Normal].First()), bitangent ); } @@ -381,12 +378,12 @@ public class MeshExporter } /// Get the vertex material type for this mesh's vertex usages. - private Type GetMaterialType(IReadOnlyDictionary> usages) + private Type GetMaterialType(IReadOnlyDictionary> usages) { var uvCount = 0; - if (usages.TryGetValue(MdlFile.VertexUsage.UV, out var dict)) + if (usages.TryGetValue(MdlFile.VertexUsage.UV, out var list)) { - foreach (var type in dict.Values) + foreach (var type in list) { uvCount += type switch { @@ -420,28 +417,28 @@ public class MeshExporter } /// Build a material vertex from a vertex's attributes. - private IVertexMaterial BuildVertexMaterial(IReadOnlyDictionary> attributes) + private IVertexMaterial BuildVertexMaterial(IReadOnlyDictionary> attributes) { if (_materialType == typeof(VertexEmpty)) return new VertexEmpty(); if (_materialType == typeof(VertexColorFfxiv)) - return new VertexColorFfxiv(ToVector4(attributes[MdlFile.VertexUsage.Color].First().Value)); + return new VertexColorFfxiv(ToVector4(attributes[MdlFile.VertexUsage.Color].First())); if (_materialType == typeof(VertexTexture1)) - return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV].First().Value)); + return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV].First())); if (_materialType == typeof(VertexTexture1ColorFfxiv)) return new VertexTexture1ColorFfxiv( - ToVector2(attributes[MdlFile.VertexUsage.UV].First().Value), - ToVector4(attributes[MdlFile.VertexUsage.Color].First().Value) + ToVector2(attributes[MdlFile.VertexUsage.UV].First()), + ToVector4(attributes[MdlFile.VertexUsage.Color].First()) ); // XIV packs two UVs into a single vec4 attribute. if (_materialType == typeof(VertexTexture2)) { - var uv = ToVector4(attributes[MdlFile.VertexUsage.UV].First().Value); + var uv = ToVector4(attributes[MdlFile.VertexUsage.UV].First()); return new VertexTexture2( new Vector2(uv.X, uv.Y), new Vector2(uv.Z, uv.W) @@ -450,11 +447,11 @@ public class MeshExporter if (_materialType == typeof(VertexTexture2ColorFfxiv)) { - var uv = ToVector4(attributes[MdlFile.VertexUsage.UV].First().Value); + var uv = ToVector4(attributes[MdlFile.VertexUsage.UV].First()); return new VertexTexture2ColorFfxiv( new Vector2(uv.X, uv.Y), new Vector2(uv.Z, uv.W), - ToVector4(attributes[MdlFile.VertexUsage.Color].First().Value) + ToVector4(attributes[MdlFile.VertexUsage.Color].First()) ); } if (_materialType == typeof(VertexTexture3)) @@ -470,7 +467,7 @@ public class MeshExporter new Vector2(uv0.X, uv0.Y), new Vector2(uv0.Z, uv0.W), new Vector2(uv1.X, uv1.Y), - ToVector4(attributes[MdlFile.VertexUsage.Color].First().Value) + ToVector4(attributes[MdlFile.VertexUsage.Color].First()) ); } @@ -478,11 +475,11 @@ public class MeshExporter } /// Get the vertex skinning type for this mesh's vertex usages. - private static Type GetSkinningType(IReadOnlyDictionary> usages) + private static Type GetSkinningType(IReadOnlyDictionary> usages) { if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices)) { - if (usages[MdlFile.VertexUsage.BlendWeights].First().Value == MdlFile.VertexType.UShort4) + if (usages[MdlFile.VertexUsage.BlendWeights].First() == MdlFile.VertexType.UShort4) { return typeof(VertexJoints8); } @@ -496,7 +493,7 @@ public class MeshExporter } /// Build a skinning vertex from a vertex's attributes. - private IVertexSkinning BuildVertexSkinning(IReadOnlyDictionary> attributes) + private IVertexSkinning BuildVertexSkinning(IReadOnlyDictionary> attributes) { if (_skinningType == typeof(VertexEmpty)) return new VertexEmpty(); @@ -506,8 +503,8 @@ public class MeshExporter if (_boneIndexMap == null) throw _notifier.Exception("Tried to build skinned vertex but no bone mappings are available."); - var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices].First().Value; - var weightsData = attributes[MdlFile.VertexUsage.BlendWeights].First().Value; + var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices].First(); + var weightsData = attributes[MdlFile.VertexUsage.BlendWeights].First(); var indices = ToByteArray(indiciesData); var weights = ToFloatArray(weightsData); From b76626ac8dbe9e4595be5e87a8f221415314ed18 Mon Sep 17 00:00:00 2001 From: Adam Moy Date: Wed, 12 Feb 2025 13:18:30 -0600 Subject: [PATCH 4/9] Added VertexTexture3 Not sure of accuracy but followed existing pattern --- Penumbra/Import/Models/Export/MeshExporter.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index fb88dfc3..a3cc2f04 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -456,7 +456,14 @@ public class MeshExporter } if (_materialType == typeof(VertexTexture3)) { - throw _notifier.Exception("Unimplemented: Material Type is VertexTexture3"); + // Not 100% sure about this + var uv0 = ToVector4(attributes[MdlFile.VertexUsage.UV][0]); + var uv1 = ToVector4(attributes[MdlFile.VertexUsage.UV][1]); + return new VertexTexture3( + new Vector2(uv0.X, uv0.Y), + new Vector2(uv0.Z, uv0.W), + new Vector2(uv1.X, uv1.Y) + ); } if (_materialType == typeof(VertexTexture3ColorFfxiv)) From 6d2b72e0798ac6cad82d54d9d0996ae666ff4c1e Mon Sep 17 00:00:00 2001 From: Adam Moy Date: Wed, 12 Feb 2025 13:30:06 -0600 Subject: [PATCH 5/9] Removed irrelevant comments --- Penumbra/Import/Models/Export/MeshExporter.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index a3cc2f04..6d65e152 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -107,19 +107,14 @@ public class MeshExporter /// Build a mapping between indices in this mesh's bone table (if any), and the glTF joint indices provided. private Dictionary? BuildBoneIndexMap(GltfSkeleton skeleton) { - Penumbra.Log.Debug("Building Bone Index Map"); // A BoneTableIndex of 255 means that this mesh is not skinned. if (XivMesh.BoneTableIndex == 255) - { - Penumbra.Log.Debug("BoneTableIndex was 255"); return null; - } var xivBoneTable = _mdl.BoneTables[XivMesh.BoneTableIndex]; var indexMap = new Dictionary(); // #TODO @ackwell maybe fix for V6 Models, I think this works fine. - Penumbra.Log.Debug($"Version is 5 {_mdl.Version == MdlFile.V5}"); foreach (var (xivBoneIndex, tableIndex) in xivBoneTable.BoneIndex.Take((int)xivBoneTable.BoneCount).WithIndex()) { From 31f23024a45b0663b87c97a1a8c0fe56b4b891b7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 20 Feb 2025 18:36:08 +0100 Subject: [PATCH 6/9] Notify and fail when a list of vertex usages has more than one entry where this is not expected. --- Penumbra/Import/Models/Export/MeshExporter.cs | 57 ++++++++++--------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 6d65e152..aa0811d7 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -7,7 +7,6 @@ using Penumbra.GameData.Files; using Penumbra.GameData.Files.ModelStructs; using SharpGLTF.Geometry; using SharpGLTF.Geometry.VertexTypes; -using SharpGLTF.IO; using SharpGLTF.Materials; using SharpGLTF.Scenes; @@ -347,24 +346,24 @@ public class MeshExporter { if (_geometryType == typeof(VertexPosition)) return new VertexPosition( - ToVector3(attributes[MdlFile.VertexUsage.Position].First()) + ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)) ); if (_geometryType == typeof(VertexPositionNormal)) return new VertexPositionNormal( - ToVector3(attributes[MdlFile.VertexUsage.Position].First()), - ToVector3(attributes[MdlFile.VertexUsage.Normal].First()) + ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)), + ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal)) ); if (_geometryType == typeof(VertexPositionNormalTangent)) { // (Bi)tangents are universally stored as ByteFloat4, which uses 0..1 to represent the full -1..1 range. // TODO: While this assumption is safe, it would be sensible to actually check. - var bitangent = ToVector4(attributes[MdlFile.VertexUsage.Tangent1].First()) * 2 - Vector4.One; + var bitangent = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Tangent1)) * 2 - Vector4.One; return new VertexPositionNormalTangent( - ToVector3(attributes[MdlFile.VertexUsage.Position].First()), - ToVector3(attributes[MdlFile.VertexUsage.Normal].First()), + ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)), + ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal)), bitangent ); } @@ -418,22 +417,22 @@ public class MeshExporter return new VertexEmpty(); if (_materialType == typeof(VertexColorFfxiv)) - return new VertexColorFfxiv(ToVector4(attributes[MdlFile.VertexUsage.Color].First())); + return new VertexColorFfxiv(ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))); if (_materialType == typeof(VertexTexture1)) - return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV].First())); + return new VertexTexture1(ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV))); if (_materialType == typeof(VertexTexture1ColorFfxiv)) return new VertexTexture1ColorFfxiv( - ToVector2(attributes[MdlFile.VertexUsage.UV].First()), - ToVector4(attributes[MdlFile.VertexUsage.Color].First()) + ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)), + ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)) ); // XIV packs two UVs into a single vec4 attribute. if (_materialType == typeof(VertexTexture2)) { - var uv = ToVector4(attributes[MdlFile.VertexUsage.UV].First()); + var uv = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)); return new VertexTexture2( new Vector2(uv.X, uv.Y), new Vector2(uv.Z, uv.W) @@ -442,11 +441,11 @@ public class MeshExporter if (_materialType == typeof(VertexTexture2ColorFfxiv)) { - var uv = ToVector4(attributes[MdlFile.VertexUsage.UV].First()); + var uv = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)); return new VertexTexture2ColorFfxiv( new Vector2(uv.X, uv.Y), new Vector2(uv.Z, uv.W), - ToVector4(attributes[MdlFile.VertexUsage.Color].First()) + ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)) ); } if (_materialType == typeof(VertexTexture3)) @@ -469,7 +468,7 @@ public class MeshExporter new Vector2(uv0.X, uv0.Y), new Vector2(uv0.Z, uv0.W), new Vector2(uv1.X, uv1.Y), - ToVector4(attributes[MdlFile.VertexUsage.Color].First()) + ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)) ); } @@ -477,18 +476,13 @@ public class MeshExporter } /// Get the vertex skinning type for this mesh's vertex usages. - private static Type GetSkinningType(IReadOnlyDictionary> usages) + private Type GetSkinningType(IReadOnlyDictionary> usages) { if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices)) { - if (usages[MdlFile.VertexUsage.BlendWeights].First() == MdlFile.VertexType.UShort4) - { - return typeof(VertexJoints8); - } - else - { - return typeof(VertexJoints4); - } + return GetFirstSafe(usages, MdlFile.VertexUsage.BlendWeights) == MdlFile.VertexType.UShort4 + ? typeof(VertexJoints8) + : typeof(VertexJoints4); } return typeof(VertexEmpty); @@ -505,8 +499,8 @@ public class MeshExporter if (_boneIndexMap == null) throw _notifier.Exception("Tried to build skinned vertex but no bone mappings are available."); - var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices].First(); - var weightsData = attributes[MdlFile.VertexUsage.BlendWeights].First(); + var indiciesData = GetFirstSafe(attributes, MdlFile.VertexUsage.BlendIndices); + var weightsData = GetFirstSafe(attributes, MdlFile.VertexUsage.BlendWeights); var indices = ToByteArray(indiciesData); var weights = ToFloatArray(weightsData); @@ -533,6 +527,17 @@ public class MeshExporter throw _notifier.Exception($"Unknown skinning type {_skinningType}"); } + /// Check that the list has length 1 for any case where this is expected and return the one entry. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private T GetFirstSafe(IReadOnlyDictionary> attributes, MdlFile.VertexUsage usage) + { + var list = attributes[usage]; + if (list.Count != 1) + throw _notifier.Exception($"Multiple usage indices encountered for {usage}."); + + return list[0]; + } + /// Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. private static Vector2 ToVector2(object data) => data switch From f8d0616acd835eefc394b7303b6e4ee55f1e923d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 20 Feb 2025 18:36:33 +0100 Subject: [PATCH 7/9] Notify when an unhandled UV count is reached. --- Penumbra/Import/Models/Export/MeshExporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index aa0811d7..32b9b323 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -406,7 +406,7 @@ public class MeshExporter (0, true) => typeof(VertexColorFfxiv), (0, false) => typeof(VertexEmpty), - _ => throw new Exception("Unreachable."), + _ => throw _notifier.Exception($"Unhandled UV count of {uvCount} encountered."), }; } From d40c59eee98bcd2f20ac0ce701e181bf6eb7bf70 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 20 Feb 2025 18:36:46 +0100 Subject: [PATCH 8/9] Slight cleanup. --- .../Import/Models/Export/VertexFragment.cs | 56 ++++++------------- 1 file changed, 16 insertions(+), 40 deletions(-) diff --git a/Penumbra/Import/Models/Export/VertexFragment.cs b/Penumbra/Import/Models/Export/VertexFragment.cs index c9b97997..56495f2f 100644 --- a/Penumbra/Import/Models/Export/VertexFragment.cs +++ b/Penumbra/Import/Models/Export/VertexFragment.cs @@ -1,4 +1,3 @@ -using System; using SharpGLTF.Geometry.VertexTypes; using SharpGLTF.Memory; using SharpGLTF.Schema2; @@ -11,7 +10,7 @@ Realistically, it will need to stick around until transforms/mutations are built and there's reason to overhaul the export pipeline. */ -public struct VertexColorFfxiv : IVertexCustom +public struct VertexColorFfxiv(Vector4 ffxivColor) : IVertexCustom { public IEnumerable> GetEncodingAttributes() { @@ -20,7 +19,7 @@ public struct VertexColorFfxiv : IVertexCustom new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); } - public Vector4 FfxivColor; + public Vector4 FfxivColor = ffxivColor; public int MaxColors => 0; @@ -33,9 +32,6 @@ public struct VertexColorFfxiv : IVertexCustom public IEnumerable CustomAttributes => CustomNames; - public VertexColorFfxiv(Vector4 ffxivColor) - => FfxivColor = ffxivColor; - public void Add(in VertexMaterialDelta delta) { } @@ -88,7 +84,7 @@ public struct VertexColorFfxiv : IVertexCustom } } -public struct VertexTexture1ColorFfxiv : IVertexCustom +public struct VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) : IVertexCustom { public IEnumerable> GetEncodingAttributes() { @@ -98,9 +94,9 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); } - public Vector2 TexCoord0; + public Vector2 TexCoord0 = texCoord0; - public Vector4 FfxivColor; + public Vector4 FfxivColor = ffxivColor; public int MaxColors => 0; @@ -113,12 +109,6 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom public IEnumerable CustomAttributes => CustomNames; - public VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) - { - TexCoord0 = texCoord0; - FfxivColor = ffxivColor; - } - public void Add(in VertexMaterialDelta delta) { TexCoord0 += delta.TexCoord0Delta; @@ -182,7 +172,7 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom } } -public struct VertexTexture2ColorFfxiv : IVertexCustom +public struct VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor) : IVertexCustom { public IEnumerable> GetEncodingAttributes() { @@ -194,9 +184,9 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); } - public Vector2 TexCoord0; - public Vector2 TexCoord1; - public Vector4 FfxivColor; + public Vector2 TexCoord0 = texCoord0; + public Vector2 TexCoord1 = texCoord1; + public Vector4 FfxivColor = ffxivColor; public int MaxColors => 0; @@ -209,13 +199,6 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom public IEnumerable CustomAttributes => CustomNames; - public VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor) - { - TexCoord0 = texCoord0; - TexCoord1 = texCoord1; - FfxivColor = ffxivColor; - } - public void Add(in VertexMaterialDelta delta) { TexCoord0 += delta.TexCoord0Delta; @@ -283,7 +266,8 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom } } -public struct VertexTexture3ColorFfxiv : IVertexCustom +public struct VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor) + : IVertexCustom { public IEnumerable> GetEncodingAttributes() { @@ -297,10 +281,10 @@ public struct VertexTexture3ColorFfxiv : IVertexCustom new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); } - public Vector2 TexCoord0; - public Vector2 TexCoord1; - public Vector2 TexCoord2; - public Vector4 FfxivColor; + public Vector2 TexCoord0 = texCoord0; + public Vector2 TexCoord1 = texCoord1; + public Vector2 TexCoord2 = texCoord2; + public Vector4 FfxivColor = ffxivColor; public int MaxColors => 0; @@ -313,14 +297,6 @@ public struct VertexTexture3ColorFfxiv : IVertexCustom public IEnumerable CustomAttributes => CustomNames; - public VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor) - { - TexCoord0 = texCoord0; - TexCoord1 = texCoord1; - TexCoord2 = texCoord2; - FfxivColor = ffxivColor; - } - public void Add(in VertexMaterialDelta delta) { TexCoord0 += delta.TexCoord0Delta; @@ -387,7 +363,7 @@ public struct VertexTexture3ColorFfxiv : IVertexCustom FfxivColor.Z, FfxivColor.W, }; - if (components.Any(component => component < 0 || component > 1)) + if (components.Any(component => component is < 0f or > 1f)) throw new ArgumentOutOfRangeException(nameof(FfxivColor)); } } From 1f172b463206bc41b5769f0d6148d551aefdda50 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 20 Feb 2025 18:37:15 +0100 Subject: [PATCH 9/9] Make default constructed models use V6 instead of V5. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index f6dff467..b4a0806e 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit f6dff467c7dad6b1213a7d7b65d40a56450f0672 +Subproject commit b4a0806e00be4ce8cf3103fd526e4a412b4770b7