diff --git a/Penumbra/Import/Models/MeshConverter.cs b/Penumbra/Import/Models/MeshConverter.cs index 2fcd2816..30d24c17 100644 --- a/Penumbra/Import/Models/MeshConverter.cs +++ b/Penumbra/Import/Models/MeshConverter.cs @@ -10,9 +10,9 @@ namespace Penumbra.Import.Modules; public sealed class MeshConverter { - public static IMeshBuilder ToGltf(MdlFile mdl, byte lod, ushort meshIndex) + public static IMeshBuilder ToGltf(MdlFile mdl, byte lod, ushort meshIndex, Dictionary? boneNameMap) { - var self = new MeshConverter(mdl, lod, meshIndex); + var self = new MeshConverter(mdl, lod, meshIndex, boneNameMap); return self.BuildMesh(); } @@ -23,21 +23,49 @@ public sealed class MeshConverter private readonly ushort _meshIndex; private MdlStructs.MeshStruct Mesh => _mdl.Meshes[_meshIndex]; - private readonly Type _geometryType; + private readonly Dictionary? _boneIndexMap; - private MeshConverter(MdlFile mdl, byte lod, ushort meshIndex) + private readonly Type _geometryType; + private readonly Type _skinningType; + + private MeshConverter(MdlFile mdl, byte lod, ushort meshIndex, Dictionary? boneNameMap) { _mdl = mdl; _lod = lod; _meshIndex = meshIndex; + if (boneNameMap != null) + _boneIndexMap = BuildBoneIndexMap(boneNameMap); + var usages = _mdl.VertexDeclarations[_meshIndex].VertexElements .Select(element => (MdlFile.VertexUsage)element.Usage) .ToImmutableHashSet(); _geometryType = GetGeometryType(usages); + _skinningType = GetSkinningType(usages); } + private Dictionary BuildBoneIndexMap(Dictionary boneNameMap) + { + // todo: BoneTableIndex of 255 means null? if so, it should probably feed into the attributes we assign... + var xivBoneTable = _mdl.BoneTables[Mesh.BoneTableIndex]; + + var indexMap = new Dictionary(); + + foreach (var xivBoneIndex in xivBoneTable.BoneIndex.Take(xivBoneTable.BoneCount)) + { + var boneName = _mdl.Bones[xivBoneIndex]; + if (!boneNameMap.TryGetValue(boneName, out var gltfBoneIndex)) + // TODO: handle - i think this is a hard failure, it means that a bone name in the model doesn't exist in the armature. + throw new Exception($"looking for {boneName} in {string.Join(", ", boneNameMap.Keys)}"); + + indexMap.Add(xivBoneIndex, gltfBoneIndex); + } + + return indexMap; + } + + // TODO: consider a struct return type private IMeshBuilder BuildMesh() { var indices = BuildIndices(); @@ -47,7 +75,7 @@ public sealed class MeshConverter typeof(MaterialBuilder), _geometryType, typeof(VertexEmpty), - typeof(VertexEmpty) + _skinningType ); var meshBuilder = (IMeshBuilder)Activator.CreateInstance(meshBuilderType, $"mesh{_meshIndex}")!; @@ -81,7 +109,7 @@ public sealed class MeshConverter private IReadOnlyList BuildVertices() { var vertexBuilderType = typeof(VertexBuilder<,,>) - .MakeGenericType(_geometryType, typeof(VertexEmpty), typeof(VertexEmpty)); + .MakeGenericType(_geometryType, typeof(VertexEmpty), _skinningType); // NOTE: This assumes that buffer streams are tightly packed, which has proven safe across tested files. If this assumption is broken, seeks will need to be moved into the vertex element loop. var streams = new BinaryReader[MaximumMeshBufferStreams]; @@ -107,8 +135,9 @@ public sealed class MeshConverter attributes[usage] = ReadVertexAttribute(streams[element.Stream], element); var vertexGeometry = BuildVertexGeometry(attributes); + var vertexSkinning = BuildVertexSkinning(attributes); - var vertexBuilder = (IVertexBuilder)Activator.CreateInstance(vertexBuilderType, vertexGeometry, new VertexEmpty(), new VertexEmpty())!; + var vertexBuilder = (IVertexBuilder)Activator.CreateInstance(vertexBuilderType, vertexGeometry, new VertexEmpty(), vertexSkinning)!; vertices.Add(vertexBuilder); } @@ -167,6 +196,40 @@ public sealed class MeshConverter throw new Exception($"Unknown geometry type {_geometryType}."); } + 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); + } + + private IVertexSkinning BuildVertexSkinning(IReadOnlyDictionary attributes) + { + if (_skinningType == typeof(VertexEmpty)) + return new VertexEmpty(); + + 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"); + + 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])) + .ToArray(); + return new VertexJoints4(bindings); + } + + throw new Exception($"Unknown skinning type {_skinningType}"); + } + // Some tangent W values that should be -1 are stored as 0. private Vector4 FixTangentVector(Vector4 tangent) => tangent with { W = tangent.W == 1 ? 1 : -1 }; @@ -188,4 +251,11 @@ public sealed class MeshConverter Vector4 v4 => v4, _ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}") }; + + private byte[] ToByteArray(object data) + => data switch + { + byte[] value => value, + _ => throw new ArgumentOutOfRangeException($"Invalid byte[] input {data}") + }; } diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 9f56588a..027ac841 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -75,20 +75,24 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable { var scene = new SceneBuilder(); - var skeletonRoot = BuildSkeleton(cancel); - if (skeletonRoot != null) - scene.AddNode(skeletonRoot); + var skeleton = BuildSkeleton(cancel); + if (skeleton != null) + scene.AddNode(skeleton.Value.Root); // TODO: group by LoD in output tree for (byte lodIndex = 0; lodIndex < _mdl.LodCount; lodIndex++) { var lod = _mdl.Lods[lodIndex]; - // TODO: consider other types? + // TODO: consider other types of mesh? for (ushort meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++) { - var meshBuilder = MeshConverter.ToGltf(_mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset)); - scene.AddRigidMesh(meshBuilder, Matrix4x4.Identity); + var meshBuilder = MeshConverter.ToGltf(_mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset), skeleton?.Names); + // TODO: use a value from the mesh converter for this check, rather than assuming that it has joints + if (skeleton == null) + scene.AddRigidMesh(meshBuilder, Matrix4x4.Identity); + else + scene.AddSkinnedMesh(meshBuilder, Matrix4x4.Identity, skeleton?.Joints); } } @@ -97,7 +101,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable } // TODO: this should be moved to a seperate model converter or something - private NodeBuilder? BuildSkeleton(CancellationToken cancel) + private (NodeBuilder Root, NodeBuilder[] Joints, Dictionary Names)? BuildSkeleton(CancellationToken cancel) { if (_sklb == null) return null; @@ -114,15 +118,17 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable // this is (less) atrocious NodeBuilder? root = null; - var boneMap = new Dictionary(); + var names = new Dictionary(); + var joints = new List(); for (var boneIndex = 0; boneIndex < skeleton.Bones.Length; boneIndex++) { var bone = skeleton.Bones[boneIndex]; - if (boneMap.ContainsKey(bone.Name)) continue; + if (names.ContainsKey(bone.Name)) continue; var node = new NodeBuilder(bone.Name); - boneMap[bone.Name] = node; + names[bone.Name] = joints.Count; + joints.Add(node); node.SetLocalTransform(new AffineTransform( bone.Transform.Scale, @@ -136,11 +142,14 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable continue; } - var parent = boneMap[skeleton.Bones[bone.ParentIndex].Name]; + var parent = joints[names[skeleton.Bones[bone.ParentIndex].Name]]; parent.AddNode(node); } - return root; + if (root == null) + return null; + + return (root, joints.ToArray(), names); } public bool Equals(IAction? other)