From a059942bb2c4d4ae6efbbef19a92b36a512695a8 Mon Sep 17 00:00:00 2001 From: ackwell Date: Mon, 1 Jan 2024 12:57:56 +1100 Subject: [PATCH] Clean up + docs --- Penumbra/Import/Models/Export/MeshExporter.cs | 52 ++++++++++++++----- .../Import/Models/Export/ModelExporter.cs | 3 ++ Penumbra/Import/Models/Export/Skeleton.cs | 7 +++ Penumbra/Import/Models/ModelManager.cs | 1 + 4 files changed, 49 insertions(+), 14 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index e835fe62..cf7cc975 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using Lumina.Data.Parsing; using Lumina.Extensions; +using OtterGui; using Penumbra.GameData.Files; using SharpGLTF.Geometry; using SharpGLTF.Geometry.VertexTypes; @@ -25,7 +26,6 @@ public class MeshExporter public void AddToScene(SceneBuilder scene) { - // TODO: throw if mesh has skinned vertices but no joints are available? foreach (var mesh in _meshes) if (_joints == null) scene.AddRigidMesh(mesh, Matrix4x4.Identity); @@ -73,8 +73,11 @@ public class MeshExporter // If there's skinning usages but no bone mapping, there's probably something wrong with the data. if (_skinningType != typeof(VertexEmpty) && _boneIndexMap == null) Penumbra.Log.Warning($"Mesh {meshIndex} has skinned vertex usages but no bone information was provided."); + + Penumbra.Log.Debug($"Mesh {meshIndex} using vertex types geometry: {_geometryType.Name}, material: {_materialType.Name}, skinning: {_skinningType.Name}"); } + /// Build a mapping between indices in this mesh's bone table (if any), and the glTF joint indices provdied. private Dictionary? BuildBoneIndexMap(Dictionary boneNameMap) { // A BoneTableIndex of 255 means that this mesh is not skinned. @@ -97,6 +100,7 @@ public class MeshExporter return indexMap; } + /// Build glTF meshes for this XIV mesh. private IMeshBuilder[] BuildMeshes() { var indices = BuildIndices(); @@ -105,16 +109,19 @@ public class MeshExporter // NOTE: Index indices are specified relative to the LOD's 0, but we're reading chunks for each mesh, so we're specifying the index base relative to the mesh's base. if (XivMesh.SubMeshCount == 0) - return [BuildMesh(indices, vertices, 0, (int)XivMesh.IndexCount)]; + return [BuildMesh($"mesh {_meshIndex}", indices, vertices, 0, (int)XivMesh.IndexCount)]; return _mdl.SubMeshes .Skip(XivMesh.SubMeshIndex) .Take(XivMesh.SubMeshCount) - .Select(submesh => BuildMesh(indices, vertices, (int)(submesh.IndexOffset - XivMesh.StartIndex), (int)submesh.IndexCount)) + .WithIndex() + .Select(submesh => BuildMesh($"mesh {_meshIndex}.{submesh.Index}", indices, vertices, (int)(submesh.Value.IndexOffset - XivMesh.StartIndex), (int)submesh.Value.IndexCount)) .ToArray(); } + /// Build a mesh from the provided indices and vertices. A subset of the full indices may be built by providing an index base and count. private IMeshBuilder BuildMesh( + string name, IReadOnlyList indices, IReadOnlyList vertices, int indexBase, @@ -127,7 +134,7 @@ public class MeshExporter _materialType, _skinningType ); - var meshBuilder = (IMeshBuilder)Activator.CreateInstance(meshBuilderType, $"mesh{_meshIndex}")!; + var meshBuilder = (IMeshBuilder)Activator.CreateInstance(meshBuilderType, name)!; // TODO: share materials &c var materialBuilder = new MaterialBuilder() @@ -191,6 +198,7 @@ public class MeshExporter return meshBuilder; } + /// Read in the indices for this mesh. private IReadOnlyList BuildIndices() { var reader = new BinaryReader(new MemoryStream(_mdl.RemainingData)); @@ -198,6 +206,7 @@ public class MeshExporter return reader.ReadStructuresAsArray((int)XivMesh.IndexCount); } + /// Build glTF-compatible vertex data for all vertices in this mesh. private IReadOnlyList BuildVertices() { var vertexBuilderType = typeof(VertexBuilder<,,>) @@ -224,7 +233,7 @@ public class MeshExporter attributes.Clear(); foreach (var (usage, element) in sortedElements) - attributes[usage] = ReadVertexAttribute(streams[element.Stream], element); + attributes[usage] = ReadVertexAttribute((MdlFile.VertexType)element.Type, streams[element.Stream]); var vertexGeometry = BuildVertexGeometry(attributes); var vertexMaterial = BuildVertexMaterial(attributes); @@ -237,9 +246,10 @@ public class MeshExporter return vertices; } - private object ReadVertexAttribute(BinaryReader reader, MdlStructs.VertexElement element) + /// Read a vertex attribute of the specified type from a vertex buffer stream. + private object ReadVertexAttribute(MdlFile.VertexType type, BinaryReader reader) { - return (MdlFile.VertexType)element.Type switch + return type switch { MdlFile.VertexType.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), @@ -252,6 +262,7 @@ public class MeshExporter }; } + /// Get the vertex geometry type for this mesh's vertex usages. private Type GetGeometryType(IReadOnlySet usages) { if (!usages.Contains(MdlFile.VertexUsage.Position)) @@ -266,6 +277,7 @@ public class MeshExporter return typeof(VertexPositionNormalTangent); } + /// Build a geometry vertex from a vertex's attributes. private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary attributes) { if (_geometryType == typeof(VertexPosition)) @@ -289,6 +301,7 @@ public class MeshExporter throw new Exception($"Unknown geometry type {_geometryType}."); } + /// Get the vertex material type for this mesh's vertex usages. private Type GetMaterialType(IReadOnlySet usages) { // TODO: IIUC, xiv's uv2 is usually represented as the second two components of a vec4 uv attribute - add support. @@ -306,6 +319,7 @@ public class MeshExporter }; } + /// Build a material vertex from a vertex's attributes. private IVertexMaterial BuildVertexMaterial(IReadOnlyDictionary attributes) { if (_materialType == typeof(VertexEmpty)) @@ -326,15 +340,16 @@ public class MeshExporter throw new Exception($"Unknown material type {_skinningType}"); } + /// Get the vertex skinning type for this mesh's vertex usages. private Type GetSkinningType(IReadOnlySet usages) { - // TODO: possibly need to check only index - weight might be missing? if (usages.Contains(MdlFile.VertexUsage.BlendWeights) && usages.Contains(MdlFile.VertexUsage.BlendIndices)) return typeof(VertexJoints4); return typeof(VertexEmpty); } + /// Build a skinning vertex from a vertex's attributes. private IVertexSkinning BuildVertexSkinning(IReadOnlyDictionary attributes) { if (_skinningType == typeof(VertexEmpty)) @@ -342,17 +357,21 @@ public class MeshExporter if (_skinningType == typeof(VertexJoints4)) { - // todo: this shouldn't happen... right? better approach? if (_boneIndexMap == null) - throw new Exception("cannot build skinned vertex without index mapping"); + throw new Exception("Tried to build skinned vertex but no bone mappings are available."); var indices = ToByteArray(attributes[MdlFile.VertexUsage.BlendIndices]); var weights = ToVector4(attributes[MdlFile.VertexUsage.BlendWeights]); - // todo: if this throws on the bone index map, the mod is broken, as it contains weights for bones that do not exist. - // i've not seen any of these that even tt can understand var bindings = Enumerable.Range(0, 4) - .Select(index => (_boneIndexMap[indices[index]], weights[index])) + .Select(bindingIndex => { + // NOTE: I've not seen any files that throw this error that aren't completely broken. + var xivBoneIndex = indices[bindingIndex]; + if (!_boneIndexMap.TryGetValue(xivBoneIndex, out var jointIndex)) + throw new Exception($"Vertex contains weight for unknown bone index {xivBoneIndex}."); + + return (jointIndex, weights[bindingIndex]); + }) .ToArray(); return new VertexJoints4(bindings); } @@ -360,10 +379,12 @@ public class MeshExporter throw new Exception($"Unknown skinning type {_skinningType}"); } - // Some tangent W values that should be -1 are stored as 0. + /// Clamps any tangent W value other than 1 to -1. + /// Some XIV models seemingly store -1 as 0, this patches over that. private Vector4 FixTangentVector(Vector4 tangent) => tangent with { W = tangent.W == 1 ? 1 : -1 }; + /// Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. private Vector2 ToVector2(object data) => data switch { @@ -373,6 +394,7 @@ public class MeshExporter _ => throw new ArgumentOutOfRangeException($"Invalid Vector2 input {data}") }; + /// Convert a vertex attribute value to a Vector3. Supported inputs are Vector2, Vector3, and Vector4. private Vector3 ToVector3(object data) => data switch { @@ -382,6 +404,7 @@ public class MeshExporter _ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}") }; + /// Convert a vertex attribute value to a Vector4. Supported inputs are Vector2, Vector3, and Vector4. private Vector4 ToVector4(object data) => data switch { @@ -391,6 +414,7 @@ public class MeshExporter _ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}") }; + /// Convert a vertex attribute value to a byte array. private byte[] ToByteArray(object data) => data switch { diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs index c8716cf3..35819e7a 100644 --- a/Penumbra/Import/Models/Export/ModelExporter.cs +++ b/Penumbra/Import/Models/Export/ModelExporter.cs @@ -30,6 +30,7 @@ public class ModelExporter } } + /// Export a model in preparation for usage in a glTF file. If provided, skeleton will be used to skin the resulting meshes where appropriate. public static Model Export(MdlFile mdl, XivSkeleton? xivSkeleton) { var gltfSkeleton = xivSkeleton != null ? ConvertSkeleton(xivSkeleton) : null; @@ -37,6 +38,7 @@ public class ModelExporter return new Model(meshes, gltfSkeleton); } + /// Convert a .mdl to a mesh (group) per LoD. private static List ConvertMeshes(MdlFile mdl, GltfSkeleton? skeleton) { var meshes = new List(); @@ -56,6 +58,7 @@ public class ModelExporter return meshes; } + /// Convert XIV skeleton data into a glTF-compatible node tree, with mappings. private static GltfSkeleton? ConvertSkeleton(XivSkeleton skeleton) { NodeBuilder? root = null; diff --git a/Penumbra/Import/Models/Export/Skeleton.cs b/Penumbra/Import/Models/Export/Skeleton.cs index 13379dc4..09cdcc32 100644 --- a/Penumbra/Import/Models/Export/Skeleton.cs +++ b/Penumbra/Import/Models/Export/Skeleton.cs @@ -2,6 +2,7 @@ using SharpGLTF.Scenes; namespace Penumbra.Import.Models.Export; +/// Representation of a skeleton within XIV. public class XivSkeleton { public Bone[] Bones; @@ -25,9 +26,15 @@ public class XivSkeleton } } +/// Representation of a glTF-compatible skeleton. public struct GltfSkeleton { + /// Root node of the skeleton. public NodeBuilder Root; + + /// Flattened list of skeleton nodes. public NodeBuilder[] Joints; + + /// Mapping of bone names to their index within the joints array. public Dictionary Names; } diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 4f761549..e71b8baf 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -88,6 +88,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable gltfModel.SaveGLTF(_outputPath); } + /// Attempt to read out the pertinent information from a .sklb. private XivSkeleton? BuildSkeleton(CancellationToken cancel) { if (_sklb == null)