Clean up meshes

This commit is contained in:
ackwell 2024-01-06 20:40:30 +11:00
parent 6de3077afa
commit 1a1c662364
3 changed files with 260 additions and 239 deletions

View file

@ -0,0 +1,240 @@
using Lumina.Data.Parsing;
using SharpGLTF.Schema2;
namespace Penumbra.Import.Models.Import;
public class MeshImporter
{
public struct Mesh
{
public MdlStructs.MeshStruct MeshStruct;
public List<MdlStructs.SubmeshStruct> SubMeshStructs;
public MdlStructs.VertexDeclarationStruct VertexDeclaration;
public IEnumerable<byte> VertexBuffer;
public List<ushort> Indicies;
public List<string>? Bones;
public List<MeshShapeKey> ShapeKeys;
}
public struct MeshShapeKey
{
public string Name;
public MdlStructs.ShapeMeshStruct ShapeMesh;
public List<MdlStructs.ShapeValueStruct> ShapeValues;
}
public static Mesh Import(IEnumerable<Node> nodes)
{
var importer = new MeshImporter(nodes);
return importer.Create();
}
private IEnumerable<Node> _nodes;
private List<MdlStructs.SubmeshStruct> _subMeshes = new();
private MdlStructs.VertexDeclarationStruct? _vertexDeclaration;
private byte[]? _strides;
private ushort _vertexCount = 0;
private List<byte>[] _streams;
private List<ushort> _indices = new();
private List<string>? _bones;
private readonly Dictionary<string, List<MdlStructs.ShapeValueStruct>> _shapeValues = new();
private MeshImporter(IEnumerable<Node> nodes)
{
_nodes = nodes;
// All meshes may use up to 3 byte streams.
_streams = new List<byte>[3];
for (var streamIndex = 0; streamIndex < 3; streamIndex++)
_streams[streamIndex] = new List<byte>();
}
private Mesh Create()
{
foreach (var node in _nodes)
BuildSubMeshForNode(node);
ArgumentNullException.ThrowIfNull(_strides);
ArgumentNullException.ThrowIfNull(_vertexDeclaration);
return new Mesh()
{
MeshStruct = new MdlStructs.MeshStruct()
{
VertexBufferOffset = [0, (uint)_streams[0].Count, (uint)(_streams[0].Count + _streams[1].Count)],
VertexBufferStride = _strides,
VertexCount = _vertexCount,
VertexStreamCount = (byte)_vertexDeclaration.Value.VertexElements
.Select(element => element.Stream + 1)
.Max(),
StartIndex = 0,
IndexCount = (uint)_indices.Count,
// TODO: import material names
MaterialIndex = 0,
SubMeshIndex = 0,
SubMeshCount = (ushort)_subMeshes.Count,
BoneTableIndex = 0,
},
SubMeshStructs = _subMeshes,
VertexDeclaration = _vertexDeclaration.Value,
VertexBuffer = _streams[0].Concat(_streams[1]).Concat(_streams[2]),
Indicies = _indices,
Bones = _bones,
ShapeKeys = _shapeValues
.Select(pair => new MeshShapeKey()
{
Name = pair.Key,
ShapeMesh = new MdlStructs.ShapeMeshStruct()
{
MeshIndexOffset = 0,
ShapeValueOffset = 0,
ShapeValueCount = (uint)pair.Value.Count,
},
ShapeValues = pair.Value,
})
.ToList(),
};
}
private void BuildSubMeshForNode(Node node)
{
// Record some offsets we'll be using later, before they get mutated with sub-mesh values.
var vertexOffset = _vertexCount;
var indexOffset = _indices.Count;
var nodeBoneMap = CreateNodeBoneMap(node);
var subMesh = SubMeshImporter.Import(node, nodeBoneMap);
var subMeshName = node.Name ?? node.Mesh.Name;
// Check that vertex declarations match - we need to combine the buffers, so a mismatch would take a whole load of resolution.
if (_vertexDeclaration == null)
_vertexDeclaration = subMesh.VertexDeclaration;
else if (VertexDeclarationMismatch(subMesh.VertexDeclaration, _vertexDeclaration.Value))
throw new Exception($"Sub-mesh \"{subMeshName}\" vertex declaration mismatch. All sub-meshes of a mesh must have equivalent vertex declarations.");
// Given that strides are derived from declarations, a lack of mismatch in declarations means the strides are fine.
// TODO: I mean, given that strides are derivable, might be worth dropping strides from the submesh return structure and computing when needed.
if (_strides == null)
_strides = subMesh.Strides;
// Merge the sub-mesh streams into the main mesh stream bodies.
_vertexCount += subMesh.VertexCount;
for (var streamIndex = 0; streamIndex < 3; streamIndex++)
_streams[streamIndex].AddRange(subMesh.Streams[streamIndex]);
// As we're appending vertex data to the buffers, we need to update indices to point into that later block.
_indices.AddRange(subMesh.Indices.Select(index => (ushort)(index + vertexOffset)));
// Merge the sub-mesh's shape values into the mesh's.
foreach (var (name, subMeshShapeValues) in subMesh.ShapeValues)
{
if (!_shapeValues.TryGetValue(name, out var meshShapeValues))
{
meshShapeValues = new();
_shapeValues.Add(name, meshShapeValues);
}
meshShapeValues.AddRange(subMeshShapeValues.Select(value => value with
{
BaseIndicesIndex = (ushort)(value.BaseIndicesIndex + indexOffset),
ReplacingVertexIndex = (ushort)(value.ReplacingVertexIndex + vertexOffset),
}));
}
// And finally, merge in the sub-mesh struct itself.
_subMeshes.Add(subMesh.SubMeshStruct with
{
IndexOffset = (ushort)(subMesh.SubMeshStruct.IndexOffset + indexOffset),
});
}
private bool VertexDeclarationMismatch(MdlStructs.VertexDeclarationStruct a, MdlStructs.VertexDeclarationStruct b)
{
var elA = a.VertexElements;
var elB = b.VertexElements;
if (elA.Length != elB.Length) return true;
// NOTE: This assumes that elements will always be in the same order. Under the current implementation, that's guaranteed.
return elA.Zip(elB).Any(pair =>
pair.First.Usage != pair.Second.Usage
|| pair.First.Type != pair.Second.Type
|| pair.First.Offset != pair.Second.Offset
|| pair.First.Stream != pair.Second.Stream
);
}
private Dictionary<ushort, ushort>? CreateNodeBoneMap(Node node)
{
// Unskinned assets can skip this all of this.
if (node.Skin == null)
return null;
// Build an array of joint names, preserving the joint index from the skin.
// Any unnamed joints we'll be coalescing on a fallback bone name - though this is realistically unlikely to occur.
var jointNames = Enumerable.Range(0, node.Skin.JointsCount)
.Select(index => node.Skin.GetJoint(index).Joint.Name ?? "unnamed_joint")
.ToArray();
// TODO: This is duplicated with the submesh importer - would be good to avoid (not that it's a huge issue).
var mesh = node.Mesh;
var meshName = node.Name ?? mesh.Name ?? "(no name)";
var primitiveCount = mesh.Primitives.Count;
if (primitiveCount != 1)
{
throw new Exception($"Mesh \"{meshName}\" has {primitiveCount} primitives, expected 1.");
}
var primitive = mesh.Primitives[0];
// Per glTF specification, an asset with a skin MUST contain skinning attributes on its mesh.
var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0");
if (jointsAccessor == null)
throw new Exception($"Skinned mesh \"{meshName}\" is skinned but does not contain skinning vertex attributes.");
// Build a set of joints that are referenced by this mesh.
// TODO: Would be neat to omit 0-weighted joints here, but doing so will require some further work on bone mapping behavior to ensure the unweighted joints can still be resolved to valid bone indices during vertex data construction.
var usedJoints = new HashSet<ushort>();
foreach (var joints in jointsAccessor.AsVector4Array())
for (var index = 0; index < 4; index++)
usedJoints.Add((ushort)joints[index]);
// Only initialise the bones list if we're actually going to put something in it.
if (_bones == null)
_bones = new();
// Build a dictionary of node-specific joint indices mesh-wide bone indices.
var nodeBoneMap = new Dictionary<ushort, ushort>();
foreach (var usedJoint in usedJoints)
{
var jointName = jointNames[usedJoint];
var boneIndex = _bones.IndexOf(jointName);
if (boneIndex == -1)
{
boneIndex = _bones.Count;
_bones.Add(jointName);
}
nodeBoneMap.Add(usedJoint, (ushort)boneIndex);
}
return nodeBoneMap;
}
}

View file

@ -8,7 +8,7 @@ public class SubMeshImporter
{
public struct SubMesh
{
public MdlStructs.SubmeshStruct Struct;
public MdlStructs.SubmeshStruct SubMeshStruct;
public MdlStructs.VertexDeclarationStruct VertexDeclaration;
@ -66,8 +66,8 @@ public class SubMeshImporter
// All meshes may use up to 3 byte streams.
_streams = new List<byte>[3];
for (var i = 0; i < 3; i++)
_streams[i] = new List<byte>();
for (var streamIndex = 0; streamIndex < 3; streamIndex++)
_streams[streamIndex] = new List<byte>();
}
private SubMesh Create()
@ -83,7 +83,7 @@ public class SubMeshImporter
return new SubMesh()
{
Struct = new MdlStructs.SubmeshStruct()
SubMeshStruct = new MdlStructs.SubmeshStruct()
{
IndexOffset = 0,
IndexCount = (uint)_indices.Length,

View file

@ -221,23 +221,13 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
var idxOffset = indices.Count;
var shapeValueOffset = shapeValues.Count;
var (
vertexDeclaration,
// boneTable,
xivMesh,
xivSubmeshes,
meshVertexBuffer,
meshIndices,
meshShapeData,
meshBoneList
) = MeshThing(submeshnodes);
var meshthing = MeshImporter.Import(submeshnodes);
var boneTableIndex = 255;
// TODO: a better check than this would be real good
if (meshBoneList.Count() > 0)
if (meshthing.Bones != null)
{
var boneIndices = new List<ushort>();
foreach (var mb in meshBoneList)
foreach (var mb in meshthing.Bones)
{
var boneIndex = bones.IndexOf(mb);
if (boneIndex == -1)
@ -262,43 +252,43 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
});
}
vertexDeclarations.Add(vertexDeclaration);
var meshStartIndex = (uint)(xivMesh.StartIndex + idxOffset / sizeof(ushort));
meshes.Add(xivMesh with
vertexDeclarations.Add(meshthing.VertexDeclaration);
var meshStartIndex = (uint)(meshthing.MeshStruct.StartIndex + idxOffset / sizeof(ushort));
meshes.Add(meshthing.MeshStruct with
{
SubMeshIndex = (ushort)(xivMesh.SubMeshIndex + subOffset),
SubMeshIndex = (ushort)(meshthing.MeshStruct.SubMeshIndex + subOffset),
// TODO: should probably define a type for index type hey.
BoneTableIndex = (ushort)boneTableIndex,
StartIndex = meshStartIndex,
VertexBufferOffset = xivMesh.VertexBufferOffset
VertexBufferOffset = meshthing.MeshStruct.VertexBufferOffset
.Select(offset => (uint)(offset + vertOffset))
.ToArray(),
});
// TODO: could probably do this with linq cleaner
foreach (var xivSubmesh in xivSubmeshes)
foreach (var xivSubmesh in meshthing.SubMeshStructs)
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))
});
vertexBuffer.AddRange(meshVertexBuffer);
indices.AddRange(meshIndices.SelectMany(index => BitConverter.GetBytes((ushort)index)));
foreach (var (key, (shapeMesh, meshShapeValues)) in meshShapeData)
vertexBuffer.AddRange(meshthing.VertexBuffer);
indices.AddRange(meshthing.Indicies.SelectMany(index => BitConverter.GetBytes((ushort)index)));
foreach (var shapeKey in meshthing.ShapeKeys)
{
List<MdlStructs.ShapeMeshStruct> keyshapedata;
if (!shapeData.TryGetValue(key, out keyshapedata))
if (!shapeData.TryGetValue(shapeKey.Name, out keyshapedata))
{
keyshapedata = new();
shapeData.Add(key, keyshapedata);
shapeData.Add(shapeKey.Name, keyshapedata);
}
keyshapedata.Add(shapeMesh with
keyshapedata.Add(shapeKey.ShapeMesh with
{
MeshIndexOffset = meshStartIndex,
ShapeValueOffset = (uint)shapeValueOffset,
});
shapeValues.AddRange(meshShapeValues);
shapeValues.AddRange(shapeKey.ShapeValues);
}
}
@ -392,215 +382,6 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
Out = mdl;
}
// this return type is an absolute meme, class that shit up.
private (
MdlStructs.VertexDeclarationStruct,
// MdlStructs.BoneTableStruct,
MdlStructs.MeshStruct,
IEnumerable<MdlStructs.SubmeshStruct>,
IEnumerable<byte>,
IEnumerable<ushort>,
IDictionary<string, (MdlStructs.ShapeMeshStruct, List<MdlStructs.ShapeValueStruct>)>,
IEnumerable<string>
) MeshThing(IEnumerable<Node> nodes)
{
var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() { VertexElements = Array.Empty<MdlStructs.VertexElement>() };
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<byte>[3];
for (var i = 0; i < 3; i++)
streams[i] = new List<byte>();
var indexCount = (uint)0;
var indices = new List<ushort>();
var strides = new byte[] { 0, 0, 0 };
var submeshes = new List<MdlStructs.SubmeshStruct>();
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.
foreach (var node in nodes)
{
var vertOff = vertexCount;
var idxOff = indexCount;
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 subMeshThingy = SubMeshImporter.Import(node, nodeBoneMap);
vertexDeclaration = subMeshThingy.VertexDeclaration; // TODO: CHECK EQUAL AFTER FIRST
strides = subMeshThingy.Strides; // ALSO CHECK EQUAL
vertexCount += subMeshThingy.VertexCount;
for (var i = 0; i < 3; i++)
streams[i].AddRange(subMeshThingy.Streams[i]);
indexCount += (uint)subMeshThingy.Indices.Length;
// we need to offset the indexes to point into the new stuff
indices.AddRange(subMeshThingy.Indices.Select(idx => (ushort)(idx + vertOff)));
submeshes.Add(subMeshThingy.Struct with
{
IndexOffset = subMeshThingy.Struct.IndexOffset + idxOff
// TODO: bone stuff probably
});
// TODO: HANDLE MORPHS, NEED TO ADJUST EVERY VALUE'S INDEX OFFSETS
foreach (var (key, shapeValues) in subMeshThingy.ShapeValues)
{
List<MdlStructs.ShapeValueStruct> valueList;
if (!morphData.TryGetValue(key, out valueList))
{
valueList = new();
morphData.Add(key, valueList);
}
valueList.AddRange(
shapeValues
.Select(value => value with
{
// but this is actually an index index
BaseIndicesIndex = (ushort)(value.BaseIndicesIndex + idxOff),
// this is a vert idx
ReplacingVertexIndex = (ushort)(value.ReplacingVertexIndex + vertOff),
})
);
}
}
// 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],
// };
// mesh
var xivMesh = new MdlStructs.MeshStruct()
{
// TODO: sum across submeshes.
// TODO: would be cool to share verts on submesh boundaries but that's way out of scope for now.
VertexCount = vertexCount,
IndexCount = indexCount,
// TODO: will have to think about how to represent this - materials can be named, so maybe adjust in parent?
MaterialIndex = 0,
// TODO: this will need adjusting by parent
SubMeshIndex = 0,
SubMeshCount = (ushort)submeshes.Count,
// TODO: update in parent
BoneTableIndex = 0,
// TODO: this is relative to the lod's index buffer, and is an index, not byte offset
StartIndex = 0,
// TODO: these are relative to the lod vertex buffer. these values are accurate for a 0 offset, but lod will need to adjust
VertexBufferOffset = [0, (uint)streams[0].Count, (uint)(streams[0].Count + streams[1].Count)],
VertexBufferStride = strides,
// VertexStreamCount = /* 2 */ (byte)(attributes.Select(attribute => attribute.Element.Stream).Max() + 1),
VertexStreamCount = (byte)(vertexDeclaration.VertexElements.Select(element => element.Stream).Max() + 1)
};
// TODO: can probably get away with flattening the values and blindly setting offsets in parent - mesh matters above, but the values are already Dealt With at this point
var shapeData = morphData.ToDictionary(
(pair) => pair.Key,
pair => (
new MdlStructs.ShapeMeshStruct()
{
// TODO: this needs to be adjusted by the parent
MeshIndexOffset = 0,
ShapeValueCount = (uint)pair.Value.Count,
// TODO: Also update by parent
ShapeValueOffset = 0,
},
pair.Value
)
);
return (
vertexDeclaration,
// boneTable,
xivMesh,
submeshes,
streams[0].Concat(streams[1]).Concat(streams[2]),
indices,
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);
}
public bool Equals(IAction? other)
{
if (other is not ImportGltfAction rhs)