Bone table imports

This commit is contained in:
ackwell 2024-01-05 22:35:36 +11:00
parent 6641f5425b
commit 70a09264a8
3 changed files with 170 additions and 40 deletions

View file

@ -120,13 +120,12 @@ public class VertexAttribute
return new VertexAttribute( return new VertexAttribute(
element, element,
// TODO: TEMP TESTING PINNED TO BONE 0 UNTIL I SET UP BONE MAPPINGS index => BuildByteFloat4(values[index])
index => BuildByteFloat4(Vector4.UnitX)
); );
} }
// 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. // 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<ushort, ushort>? boneMap)
{ {
if (!accessors.TryGetValue("JOINTS_0", out var accessor)) if (!accessors.TryGetValue("JOINTS_0", out var accessor))
return null; return null;
@ -134,6 +133,9 @@ public class VertexAttribute
if (!accessors.ContainsKey("WEIGHTS_0")) if (!accessors.ContainsKey("WEIGHTS_0"))
throw new Exception("Mesh contained JOINTS_0 attribute but no corresponding WEIGHTS_0 attribute."); 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() var element = new MdlStructs.VertexElement()
{ {
Stream = 0, Stream = 0,
@ -145,8 +147,15 @@ public class VertexAttribute
return new VertexAttribute( return new VertexAttribute(
element, element,
// TODO: TEMP TESTING PINNED TO BONE 0 UNTIL I SET UP BONE MAPPINGS index => {
index => BuildUInt(Vector4.Zero) var foo = values[index];
return BuildUInt(new Vector4(
boneMap[(ushort)foo.X],
boneMap[(ushort)foo.Y],
boneMap[(ushort)foo.Z],
boneMap[(ushort)foo.W]
));
}
); );
} }

View file

@ -189,7 +189,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
var nodes = model.LogicalNodes var nodes = model.LogicalNodes
.Where(node => node.Mesh != null) .Where(node => node.Mesh != null)
.Take(6) // this model has all 3 lods in it - the first 6 are the real lod0 .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 name = node.Name ?? node.Mesh.Name;
var match = MeshNameGroupingRegex().Match(name); var match = MeshNameGroupingRegex().Match(name);
return match.Success return match.Success
@ -201,6 +202,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
// this is a representation of a single LoD // this is a representation of a single LoD
var vertexDeclarations = new List<MdlStructs.VertexDeclarationStruct>(); var vertexDeclarations = new List<MdlStructs.VertexDeclarationStruct>();
var bones = new List<string>();
var boneTables = new List<MdlStructs.BoneTableStruct>(); var boneTables = new List<MdlStructs.BoneTableStruct>();
var meshes = new List<MdlStructs.MeshStruct>(); var meshes = new List<MdlStructs.MeshStruct>();
var submeshes = new List<MdlStructs.SubmeshStruct>(); var submeshes = new List<MdlStructs.SubmeshStruct>();
@ -221,21 +223,52 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
var ( var (
vertexDeclaration, vertexDeclaration,
boneTable, // boneTable,
xivMesh, xivMesh,
xivSubmeshes, xivSubmeshes,
meshVertexBuffer, meshVertexBuffer,
meshIndices, meshIndices,
meshShapeData // fasdfasd meshShapeData,
meshBoneList
) = MeshThing(submeshnodes); ) = MeshThing(submeshnodes);
var boneTableIndex = 255;
// TODO: a better check than this would be real good
if (meshBoneList.Count() > 0)
{
var boneIndices = new List<ushort>();
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); vertexDeclarations.Add(vertexDeclaration);
boneTables.Add(boneTable);
var meshStartIndex = (uint)(xivMesh.StartIndex + idxOffset / sizeof(ushort)); var meshStartIndex = (uint)(xivMesh.StartIndex + idxOffset / sizeof(ushort));
meshes.Add(xivMesh with { meshes.Add(xivMesh with
{
SubMeshIndex = (ushort)(xivMesh.SubMeshIndex + subOffset), SubMeshIndex = (ushort)(xivMesh.SubMeshIndex + subOffset),
// TODO: should probably define a type for index type hey. // TODO: should probably define a type for index type hey.
BoneTableIndex = (ushort)(xivMesh.BoneTableIndex + boneTableOffset), BoneTableIndex = (ushort)boneTableIndex,
StartIndex = meshStartIndex, StartIndex = meshStartIndex,
VertexBufferOffset = xivMesh.VertexBufferOffset VertexBufferOffset = xivMesh.VertexBufferOffset
.Select(offset => (uint)(offset + vertOffset)) .Select(offset => (uint)(offset + vertOffset))
@ -243,7 +276,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
}); });
// TODO: could probably do this with linq cleaner // TODO: could probably do this with linq cleaner
foreach (var xivSubmesh in xivSubmeshes) 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 // TODO: this will need to keep ticking up for each submesh in the same mesh
IndexOffset = (uint)(xivSubmesh.IndexOffset + idxOffset / sizeof(ushort)) IndexOffset = (uint)(xivSubmesh.IndexOffset + idxOffset / sizeof(ushort))
}); });
@ -258,7 +292,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
shapeData.Add(key, keyshapedata); shapeData.Add(key, keyshapedata);
} }
keyshapedata.Add(shapeMesh with { keyshapedata.Add(shapeMesh with
{
MeshIndexOffset = meshStartIndex, MeshIndexOffset = meshStartIndex,
ShapeValueOffset = (uint)shapeValueOffset, ShapeValueOffset = (uint)shapeValueOffset,
}); });
@ -347,9 +382,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
IndexDataOffset = (uint)vertexBuffer.Count, IndexDataOffset = (uint)vertexBuffer.Count,
}, },
], ],
Bones = [ Bones = bones.ToArray(),
"j_kosi",
],
Materials = [ Materials = [
"/mt_c0201e6180_top_a.mtrl", "/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. // this return type is an absolute meme, class that shit up.
private ( private (
MdlStructs.VertexDeclarationStruct, MdlStructs.VertexDeclarationStruct,
MdlStructs.BoneTableStruct, // MdlStructs.BoneTableStruct,
MdlStructs.MeshStruct, MdlStructs.MeshStruct,
IEnumerable<MdlStructs.SubmeshStruct>, IEnumerable<MdlStructs.SubmeshStruct>,
IEnumerable<byte>, IEnumerable<byte>,
IEnumerable<ushort>, IEnumerable<ushort>,
IDictionary<string, (MdlStructs.ShapeMeshStruct, List<MdlStructs.ShapeValueStruct>)> IDictionary<string, (MdlStructs.ShapeMeshStruct, List<MdlStructs.ShapeValueStruct>)>,
IEnumerable<string>
) MeshThing(IEnumerable<Node> nodes) ) MeshThing(IEnumerable<Node> nodes)
{ {
var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() { VertexElements = Array.Empty<MdlStructs.VertexElement>()}; var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() { VertexElements = Array.Empty<MdlStructs.VertexElement>() };
var vertexCount = (ushort)0; var vertexCount = (ushort)0;
// there's gotta be a better way to do this with streams or enumerables or something, surely // there's gotta be a better way to do this with streams or enumerables or something, surely
var streams = new List<byte>[3]; var streams = new List<byte>[3];
@ -378,17 +412,57 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
streams[i] = new List<byte>(); streams[i] = new List<byte>();
var indexCount = (uint)0; var indexCount = (uint)0;
var indices = new List<ushort>(); var indices = new List<ushort>();
var strides = new byte[] {0, 0, 0}; var strides = new byte[] { 0, 0, 0 };
var submeshes = new List<MdlStructs.SubmeshStruct>(); var submeshes = new List<MdlStructs.SubmeshStruct>();
var morphData = new Dictionary<string, List<MdlStructs.ShapeValueStruct>>(); var morphData = new Dictionary<string, List<MdlStructs.ShapeValueStruct>>();
/*
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<string>();
// 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. // 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) foreach (var node in nodes)
{ {
var vertOff = vertexCount; var vertOff = vertexCount;
var idxOff = indexCount; var idxOff = indexCount;
var (vertDecl, newStrides, submesh, vertCount, vertStreams, idxCount, idxs, subMorphData) = NodeMeshThing(node); Dictionary<ushort, ushort>? 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 vertexDeclaration = vertDecl; // TODO: CHECK EQUAL AFTER FIRST
strides = newStrides; // ALSO CHECK EQUAL strides = newStrides; // ALSO CHECK EQUAL
vertexCount += vertCount; vertexCount += vertCount;
@ -397,7 +471,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
indexCount += idxCount; indexCount += idxCount;
// we need to offset the indexes to point into the new stuff // we need to offset the indexes to point into the new stuff
indices.AddRange(idxs.Select(idx => (ushort)(idx + vertOff))); indices.AddRange(idxs.Select(idx => (ushort)(idx + vertOff)));
submeshes.Add(submesh with { submeshes.Add(submesh with
{
IndexOffset = submesh.IndexOffset + idxOff IndexOffset = submesh.IndexOffset + idxOff
// TODO: bone stuff probably // TODO: bone stuff probably
}); });
@ -412,7 +487,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
} }
valueList.AddRange( valueList.AddRange(
shapeValues shapeValues
.Select(value => value with { .Select(value => value with
{
// but this is actually an index index // but this is actually an index index
BaseIndicesIndex = (ushort)(value.BaseIndicesIndex + idxOff), BaseIndicesIndex = (ushort)(value.BaseIndicesIndex + idxOff),
// this is a vert idx // this is a vert idx
@ -424,12 +500,12 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
// one of these per skinned mesh. // one of these per skinned mesh.
// TODO: check if mesh has skinning at all. (err if mixed?) // TODO: check if mesh has skinning at all. (err if mixed?)
var boneTable = new MdlStructs.BoneTableStruct() // var boneTable = new MdlStructs.BoneTableStruct()
{ // {
BoneCount = 1, // 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. // // 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], // BoneIndex = new ushort[64],
}; // };
// mesh // mesh
var xivMesh = new MdlStructs.MeshStruct() var xivMesh = new MdlStructs.MeshStruct()
@ -472,15 +548,59 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
return ( return (
vertexDeclaration, vertexDeclaration,
boneTable, // boneTable,
xivMesh, xivMesh,
submeshes, submeshes,
streams[0].Concat(streams[1]).Concat(streams[2]), streams[0].Concat(streams[1]).Concat(streams[2]),
indices, indices,
shapeData shapeData,
usedBones
); );
} }
private (string[], ISet<ushort>)? 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<ushort>();
// 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 ( private (
MdlStructs.VertexDeclarationStruct, MdlStructs.VertexDeclarationStruct,
byte[], byte[],
@ -491,7 +611,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
uint, uint,
IEnumerable<ushort>, IEnumerable<ushort>,
IDictionary<string, List<MdlStructs.ShapeValueStruct>> IDictionary<string, List<MdlStructs.ShapeValueStruct>>
) NodeMeshThing(Node node) ) NodeMeshThing(Node node, IDictionary<ushort, ushort>? nodeBoneMap)
{ {
// BoneTable (mesh.btidx = 255 means unskinned) // BoneTable (mesh.btidx = 255 means unskinned)
// vertexdecl // vertexdecl
@ -522,7 +642,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
var rawAttributes = new[] { var rawAttributes = new[] {
VertexAttribute.Position(accessors, morphAccessors), VertexAttribute.Position(accessors, morphAccessors),
VertexAttribute.BlendWeight(accessors), VertexAttribute.BlendWeight(accessors),
VertexAttribute.BlendIndex(accessors), VertexAttribute.BlendIndex(accessors, nodeBoneMap),
VertexAttribute.Normal(accessors, morphAccessors), VertexAttribute.Normal(accessors, morphAccessors),
VertexAttribute.Tangent1(accessors, morphAccessors), VertexAttribute.Tangent1(accessors, morphAccessors),
VertexAttribute.Color(accessors), VertexAttribute.Color(accessors),
@ -530,14 +650,14 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
}; };
var attributes = new List<VertexAttribute>(); var attributes = new List<VertexAttribute>();
var offsets = new byte[] {0, 0, 0}; var offsets = new byte[] { 0, 0, 0 };
foreach (var attribute in rawAttributes) foreach (var attribute in rawAttributes)
{ {
if (attribute == null) continue; if (attribute == null) continue;
var element = attribute.Element; var element = attribute.Element;
// recreating this here really sucks - add a "withstream" or something. // recreating this here really sucks - add a "withstream" or something.
attributes.Add(new VertexAttribute( attributes.Add(new VertexAttribute(
element with {Offset = offsets[element.Stream]}, element with { Offset = offsets[element.Stream] },
attribute.Build, attribute.Build,
attribute.HasMorph, attribute.HasMorph,
attribute.BuildMorph attribute.BuildMorph
@ -604,7 +724,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
foreach (var something in fuck) foreach (var something in fuck)
{ {
morphmaplist.Add(new MdlStructs.ShapeValueStruct(){ morphmaplist.Add(new MdlStructs.ShapeValueStruct()
{
BaseIndicesIndex = (ushort)something, BaseIndicesIndex = (ushort)something,
ReplacingVertexIndex = (ushort)vertexCount, ReplacingVertexIndex = (ushort)vertexCount,
}); });

View file

@ -81,7 +81,7 @@ public partial class ModEditWindow
public void Import() public void Import()
{ {
// TODO: this needs to be fleshed out a bunch. // 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));
} }
/// <summary> Export model to an interchange format. </summary> /// <summary> Export model to an interchange format. </summary>