diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 37ccb79d..f2ec6f1a 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -120,13 +120,12 @@ public class VertexAttribute return new VertexAttribute( element, - // TODO: TEMP TESTING PINNED TO BONE 0 UNTIL I SET UP BONE MAPPINGS - index => BuildByteFloat4(Vector4.UnitX) + index => BuildByteFloat4(values[index]) ); } // TODO: this will need to take in a skeleton mapping of some kind so i can persist the bones used and wire up the joints correctly. hopefully by the "write vertex buffer" stage of building, we already know something about the skeleton. - public static VertexAttribute? BlendIndex(Accessors accessors) + public static VertexAttribute? BlendIndex(Accessors accessors, IDictionary? boneMap) { if (!accessors.TryGetValue("JOINTS_0", out var accessor)) return null; @@ -134,6 +133,9 @@ public class VertexAttribute if (!accessors.ContainsKey("WEIGHTS_0")) throw new Exception("Mesh contained JOINTS_0 attribute but no corresponding WEIGHTS_0 attribute."); + if (boneMap == null) + throw new Exception("Mesh contained JOINTS_0 attribute but no bone mapping was created."); + var element = new MdlStructs.VertexElement() { Stream = 0, @@ -145,8 +147,15 @@ public class VertexAttribute return new VertexAttribute( element, - // TODO: TEMP TESTING PINNED TO BONE 0 UNTIL I SET UP BONE MAPPINGS - index => BuildUInt(Vector4.Zero) + index => { + var foo = values[index]; + return BuildUInt(new Vector4( + boneMap[(ushort)foo.X], + boneMap[(ushort)foo.Y], + boneMap[(ushort)foo.Z], + boneMap[(ushort)foo.W] + )); + } ); } diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index d849f3eb..875c6071 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -189,7 +189,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var nodes = model.LogicalNodes .Where(node => node.Mesh != null) .Take(6) // this model has all 3 lods in it - the first 6 are the real lod0 - .SelectWhere(node => { + .SelectWhere(node => + { var name = node.Name ?? node.Mesh.Name; var match = MeshNameGroupingRegex().Match(name); return match.Success @@ -201,6 +202,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable // this is a representation of a single LoD var vertexDeclarations = new List(); + var bones = new List(); var boneTables = new List(); var meshes = new List(); var submeshes = new List(); @@ -209,7 +211,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var shapeData = new Dictionary>(); var shapeValues = new List(); - + foreach (var submeshnodes in nodes) { var boneTableOffset = boneTables.Count; @@ -221,21 +223,52 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var ( vertexDeclaration, - boneTable, + // boneTable, xivMesh, xivSubmeshes, meshVertexBuffer, meshIndices, - meshShapeData // fasdfasd + meshShapeData, + meshBoneList ) = MeshThing(submeshnodes); + var boneTableIndex = 255; + // TODO: a better check than this would be real good + if (meshBoneList.Count() > 0) + { + var boneIndices = new List(); + foreach (var mb in meshBoneList) + { + var boneIndex = bones.IndexOf(mb); + if (boneIndex == -1) + { + boneIndex = bones.Count; + bones.Add(mb); + } + boneIndices.Add((ushort)boneIndex); + } + + if (boneIndices.Count > 64) + throw new Exception("One mesh cannot be weighted to more than 64 bones."); + + var boneIndicesArray = new ushort[64]; + Array.Copy(boneIndices.ToArray(), boneIndicesArray, boneIndices.Count); + + boneTableIndex = boneTableOffset; + boneTables.Add(new MdlStructs.BoneTableStruct() + { + BoneCount = (byte)boneIndices.Count, + BoneIndex = boneIndicesArray, + }); + } + vertexDeclarations.Add(vertexDeclaration); - boneTables.Add(boneTable); var meshStartIndex = (uint)(xivMesh.StartIndex + idxOffset / sizeof(ushort)); - meshes.Add(xivMesh with { + meshes.Add(xivMesh with + { SubMeshIndex = (ushort)(xivMesh.SubMeshIndex + subOffset), // TODO: should probably define a type for index type hey. - BoneTableIndex = (ushort)(xivMesh.BoneTableIndex + boneTableOffset), + BoneTableIndex = (ushort)boneTableIndex, StartIndex = meshStartIndex, VertexBufferOffset = xivMesh.VertexBufferOffset .Select(offset => (uint)(offset + vertOffset)) @@ -243,7 +276,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable }); // TODO: could probably do this with linq cleaner foreach (var xivSubmesh in xivSubmeshes) - submeshes.Add(xivSubmesh with { + submeshes.Add(xivSubmesh with + { // TODO: this will need to keep ticking up for each submesh in the same mesh IndexOffset = (uint)(xivSubmesh.IndexOffset + idxOffset / sizeof(ushort)) }); @@ -258,7 +292,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable shapeData.Add(key, keyshapedata); } - keyshapedata.Add(shapeMesh with { + keyshapedata.Add(shapeMesh with + { MeshIndexOffset = meshStartIndex, ShapeValueOffset = (uint)shapeValueOffset, }); @@ -347,9 +382,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable IndexDataOffset = (uint)vertexBuffer.Count, }, ], - Bones = [ - "j_kosi", - ], + Bones = bones.ToArray(), Materials = [ "/mt_c0201e6180_top_a.mtrl", ], @@ -362,15 +395,16 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable // this return type is an absolute meme, class that shit up. private ( MdlStructs.VertexDeclarationStruct, - MdlStructs.BoneTableStruct, + // MdlStructs.BoneTableStruct, MdlStructs.MeshStruct, IEnumerable, IEnumerable, IEnumerable, - IDictionary)> + IDictionary)>, + IEnumerable ) MeshThing(IEnumerable nodes) { - var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() { VertexElements = Array.Empty()}; + var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() { VertexElements = Array.Empty() }; var vertexCount = (ushort)0; // there's gotta be a better way to do this with streams or enumerables or something, surely var streams = new List[3]; @@ -378,17 +412,57 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable streams[i] = new List(); var indexCount = (uint)0; var indices = new List(); - var strides = new byte[] {0, 0, 0}; + var strides = new byte[] { 0, 0, 0 }; var submeshes = new List(); var morphData = new Dictionary>(); + /* + THOUGHTS + per submesh node, before calling down to build the mesh, build a bone mapping of joint index -> bone name (not node index) - the joint indexes are what will be used in the vertices. + per submesh node, eagerly collect all blend indexes (joints) used before building anything - just as a set or something + the above means i can create a limited set and a mapping, i.e. if skeleton contains {0->a 1->b 2->c}, and mesh contains 0, 2, then i can output [a, c] + {0->0, 2->1} + (throw if >64 entries in that name array) + + then for the second prim, + again get the joint-name mapping, and again get the joint set + then can extend the values. using the samme example, if skeleton2 contains {0->c 1->d, 2->e} and mesh contains [0,2] again, then bone array can be extended to [a, c, e] and the mesh-specific mapping would be {0->1, 2->2} + + repeat, etc + */ + + var usedBones = new List(); + // TODO: check that attrs/elems/strides match - we should be generating per-mesh stuff for sanity's sake, but we need to make sure they match if there's >1 node mesh in a mesh. foreach (var node in nodes) { var vertOff = vertexCount; var idxOff = indexCount; - var (vertDecl, newStrides, submesh, vertCount, vertStreams, idxCount, idxs, subMorphData) = NodeMeshThing(node); + Dictionary? nodeBoneMap = null; + var bonething = WalkBoneThing(node); + if (bonething.HasValue) + { + var (boneNames, usedJoints) = bonething.Value; + nodeBoneMap = new(); + + // todo: probably linq this shit + foreach (var usedJoint in usedJoints) + { + // this is the 0,2 + var boneName = boneNames[usedJoint]; + var boneIdx = usedBones.IndexOf(boneName); + if (boneIdx == -1) + { + boneIdx = usedBones.Count; + usedBones.Add(boneName); + } + nodeBoneMap.Add(usedJoint, (ushort)boneIdx); + } + + Penumbra.Log.Information($"nbm {string.Join(",", nodeBoneMap.Select(kv => $"{kv.Key}:{kv.Value}"))}"); + } + + var (vertDecl, newStrides, submesh, vertCount, vertStreams, idxCount, idxs, subMorphData) = NodeMeshThing(node, nodeBoneMap); vertexDeclaration = vertDecl; // TODO: CHECK EQUAL AFTER FIRST strides = newStrides; // ALSO CHECK EQUAL vertexCount += vertCount; @@ -397,7 +471,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable indexCount += idxCount; // we need to offset the indexes to point into the new stuff indices.AddRange(idxs.Select(idx => (ushort)(idx + vertOff))); - submeshes.Add(submesh with { + submeshes.Add(submesh with + { IndexOffset = submesh.IndexOffset + idxOff // TODO: bone stuff probably }); @@ -412,7 +487,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable } valueList.AddRange( shapeValues - .Select(value => value with { + .Select(value => value with + { // but this is actually an index index BaseIndicesIndex = (ushort)(value.BaseIndicesIndex + idxOff), // this is a vert idx @@ -424,12 +500,12 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable // one of these per skinned mesh. // TODO: check if mesh has skinning at all. (err if mixed?) - var boneTable = new MdlStructs.BoneTableStruct() - { - BoneCount = 1, - // this needs to be the full 64. this should be fine _here_ with 0s because i only have one bone, but will need to be fully populated properly. in real files. - BoneIndex = new ushort[64], - }; + // var boneTable = new MdlStructs.BoneTableStruct() + // { + // BoneCount = 1, + // // this needs to be the full 64. this should be fine _here_ with 0s because i only have one bone, but will need to be fully populated properly. in real files. + // BoneIndex = new ushort[64], + // }; // mesh var xivMesh = new MdlStructs.MeshStruct() @@ -472,15 +548,59 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable return ( vertexDeclaration, - boneTable, + // boneTable, xivMesh, submeshes, streams[0].Concat(streams[1]).Concat(streams[2]), indices, - shapeData + shapeData, + usedBones ); } + private (string[], ISet)? WalkBoneThing(Node node) + { + // + if (node.Skin == null) + return null; + + var jointNames = Enumerable.Range(0, node.Skin.JointsCount) + .Select(index => node.Skin.GetJoint(index).Joint.Name ?? $"UNNAMED") + .ToArray(); + + // it might make sense to do this in the submesh handling - i do need to maintain the mesh-wide bone list, but that can be passed in/out, perhaps? + var mesh = node.Mesh; + if (mesh.Primitives.Count != 1) + throw new Exception($"Mesh has {mesh.Primitives.Count} primitives, expected 1."); + var primitive = mesh.Primitives[0]; + + var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0"); + if (jointsAccessor == null) + throw new Exception($"Skinned meshes must contain a JOINTS_0 attribute."); + + // var weightsAccssor = primitive.GetVertexAccessor("WEIGHTS_0"); + // if (weightsAccssor == null) + // throw new Exception($"Skinned meshes must contain a WEIGHTS_0 attribute."); + + var usedJoints = new HashSet(); + + // TODO: would be neat to omit any joints that are only used in 0-weight positions, but doing so would require being a _little_ smarter in vertex attrs on how to fall back when mappings aren't found - or otherwise try to ensure that mappings for unused stuff always exists + // foreach (var (joints, weights) in jointsAccessor.AsVector4Array().Zip(weightsAccssor.AsVector4Array())) + // { + // for (var index = 0; index < 4; index++) + // if (weights[index] > 0) + // usedJoints.Add((ushort)joints[index]); + // } + + foreach (var joints in jointsAccessor.AsVector4Array()) + { + for (var index = 0; index < 4; index++) + usedJoints.Add((ushort)joints[index]); + } + + return (jointNames, usedJoints); + } + private ( MdlStructs.VertexDeclarationStruct, byte[], @@ -491,7 +611,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable uint, IEnumerable, IDictionary> - ) NodeMeshThing(Node node) + ) NodeMeshThing(Node node, IDictionary? nodeBoneMap) { // BoneTable (mesh.btidx = 255 means unskinned) // vertexdecl @@ -505,7 +625,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var primitive = mesh.Primitives[0]; var accessors = primitive.VertexAccessors; - + // var foo = primitive.GetMorphTargetAccessors(0); // var bar = foo["POSITION"]; // var baz = bar.AsVector3Array(); @@ -522,7 +642,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var rawAttributes = new[] { VertexAttribute.Position(accessors, morphAccessors), VertexAttribute.BlendWeight(accessors), - VertexAttribute.BlendIndex(accessors), + VertexAttribute.BlendIndex(accessors, nodeBoneMap), VertexAttribute.Normal(accessors, morphAccessors), VertexAttribute.Tangent1(accessors, morphAccessors), VertexAttribute.Color(accessors), @@ -530,14 +650,14 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable }; var attributes = new List(); - var offsets = new byte[] {0, 0, 0}; + var offsets = new byte[] { 0, 0, 0 }; foreach (var attribute in rawAttributes) { if (attribute == null) continue; var element = attribute.Element; // recreating this here really sucks - add a "withstream" or something. attributes.Add(new VertexAttribute( - element with {Offset = offsets[element.Stream]}, + element with { Offset = offsets[element.Stream] }, attribute.Build, attribute.HasMorph, attribute.BuildMorph @@ -547,7 +667,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var strides = offsets; // TODO: when merging submeshes, i'll need to check that vert els are the same for all of them, as xiv only stores verts at the mesh level and shares them. - + var streams = new List[3]; for (var i = 0; i < 3; i++) streams[i] = new List(); @@ -604,7 +724,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable foreach (var something in fuck) { - morphmaplist.Add(new MdlStructs.ShapeValueStruct(){ + morphmaplist.Add(new MdlStructs.ShapeValueStruct() + { BaseIndicesIndex = (ushort)something, ReplacingVertexIndex = (ushort)vertexCount, }); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 5d9abda6..e030c8c0 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -81,7 +81,7 @@ public partial class ModEditWindow public void Import() { // TODO: this needs to be fleshed out a bunch. - _edit._models.ImportGltf().ContinueWith(v => Initialize(v.Result)); + _edit._models.ImportGltf().ContinueWith(v => Initialize(v.Result ?? Mdl)); } /// Export model to an interchange format.