diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs new file mode 100644 index 00000000..941fa1d5 --- /dev/null +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -0,0 +1,217 @@ +using Lumina.Data.Parsing; +using OtterGui; +using SharpGLTF.Schema2; + +namespace Penumbra.Import.Models.Import; + +public class SubMeshImporter +{ + public struct SubMesh + { + public MdlStructs.SubmeshStruct Struct; + + public MdlStructs.VertexDeclarationStruct VertexDeclaration; + + public ushort VertexCount; + public byte[] Strides; + public List[] Streams; + + public ushort[] Indices; + + public Dictionary> ShapeValues; + } + + public static SubMesh Import(Node node, IDictionary? nodeBoneMap) + { + var importer = new SubMeshImporter(node, nodeBoneMap); + return importer.Create(); + } + + private readonly MeshPrimitive _primitive; + private readonly IDictionary? _nodeBoneMap; + + private List? _attributes; + + private ushort _vertexCount = 0; + private byte[] _strides = [0, 0, 0]; + private readonly List[] _streams; + + private ushort[]? _indices; + + private readonly List? _morphNames; + private Dictionary>? _shapeValues; + + private SubMeshImporter(Node node, IDictionary? nodeBoneMap) + { + var mesh = node.Mesh; + + var primitiveCount = mesh.Primitives.Count; + if (primitiveCount != 1) + { + var name = node.Name ?? mesh.Name ?? "(no name)"; + throw new Exception($"Mesh \"{name}\" has {primitiveCount} primitives, expected 1."); + } + + _primitive = mesh.Primitives[0]; + _nodeBoneMap = nodeBoneMap; + + try + { + _morphNames = mesh.Extras.GetNode("targetNames").Deserialize>(); + } + catch + { + _morphNames = null; + } + + // All meshes may use up to 3 byte streams. + _streams = new List[3]; + for (var i = 0; i < 3; i++) + _streams[i] = new List(); + } + + private SubMesh Create() + { + // Build all the data we'll need. + BuildIndices(); + BuildAttributes(); + BuildVertices(); + + ArgumentNullException.ThrowIfNull(_indices); + ArgumentNullException.ThrowIfNull(_attributes); + ArgumentNullException.ThrowIfNull(_shapeValues); + + return new SubMesh() + { + Struct = new MdlStructs.SubmeshStruct() + { + IndexOffset = 0, + IndexCount = (uint)_indices.Length, + AttributeIndexMask = 0, + + // TODO: Flesh these out. Game doesn't seem to rely on them existing, though. + BoneStartIndex = 0, + BoneCount = 0, + }, + + VertexDeclaration = new MdlStructs.VertexDeclarationStruct() + { + VertexElements = _attributes.Select(attribute => attribute.Element).ToArray(), + }, + + VertexCount = _vertexCount, + Strides = _strides, + Streams = _streams, + + Indices = _indices, + + ShapeValues = _shapeValues, + }; + } + + private void BuildIndices() + { + _indices = _primitive.GetIndices().Select(idx => (ushort)idx).ToArray(); + } + + private void BuildAttributes() + { + var accessors = _primitive.VertexAccessors; + + var morphAccessors = Enumerable.Range(0, _primitive.MorphTargetsCount) + .Select(index => _primitive.GetMorphTargetAccessors(index)); + + // Try to build all the attributes the mesh might use. + // The order here is chosen to match a typical model's element order. + var rawAttributes = new[] { + VertexAttribute.Position(accessors, morphAccessors), + VertexAttribute.BlendWeight(accessors), + VertexAttribute.BlendIndex(accessors, _nodeBoneMap), + VertexAttribute.Normal(accessors, morphAccessors), + VertexAttribute.Tangent1(accessors, morphAccessors), + VertexAttribute.Color(accessors), + VertexAttribute.Uv(accessors), + }; + + var attributes = new List(); + var offsets = new byte[] { 0, 0, 0 }; + foreach (var attribute in rawAttributes) + { + if (attribute == null) continue; + attributes.Add(attribute.WithOffset(offsets[attribute.Stream])); + offsets[attribute.Stream] += attribute.Size; + } + + _attributes = attributes; + // After building the attributes, the resulting next offsets are our stream strides. + _strides = offsets; + } + + private void BuildVertices() + { + ArgumentNullException.ThrowIfNull(_attributes); + + // Lists of vertex indices that are effected by each morph target for this primitive. + var morphModifiedVertices = Enumerable.Range(0, _primitive.MorphTargetsCount) + .Select(_ => new List()) + .ToArray(); + + // We can safely assume that POSITION exists by this point - and if, by some bizarre chance, it doesn't, failing out is sane. + _vertexCount = (ushort)_primitive.VertexAccessors["POSITION"].Count; + + for (var vertexIndex = 0; vertexIndex < _vertexCount; vertexIndex++) + { + // Write out vertex data to streams for each attribute. + foreach (var attribute in _attributes) + _streams[attribute.Stream].AddRange(attribute.Build(vertexIndex)); + + // Record which morph targets have values for this vertex, if any. + var changedMorphs = morphModifiedVertices + .WithIndex() + .Where(pair => _attributes.Any(attribute => attribute.HasMorph(pair.Index, vertexIndex))) + .Select(pair => pair.Value); + foreach (var modifiedVertices in changedMorphs) + modifiedVertices.Add(vertexIndex); + } + + BuildShapeValues(morphModifiedVertices); + } + + private void BuildShapeValues(List[] morphModifiedVertices) + { + ArgumentNullException.ThrowIfNull(_indices); + ArgumentNullException.ThrowIfNull(_attributes); + + var morphShapeValues = new Dictionary>(); + + foreach (var (modifiedVertices, morphIndex) in morphModifiedVertices.WithIndex()) + { + // Each for a given mesh, each shape key contains a list of shape value mappings. + var shapeValues = new List(); + + foreach (var vertexIndex in modifiedVertices) + { + // Write out the morphed vertex to the vertex streams. + foreach (var attribute in _attributes) + _streams[attribute.Stream].AddRange(attribute.BuildMorph(morphIndex, vertexIndex)); + + // Find any indices that target this vertex index and create a mapping. + var targetingIndices = _indices.WithIndex() + .SelectWhere(pair => (pair.Value == vertexIndex, pair.Index)); + foreach (var targetingIndex in targetingIndices) + shapeValues.Add(new MdlStructs.ShapeValueStruct() + { + BaseIndicesIndex = (ushort)targetingIndex, + ReplacingVertexIndex = _vertexCount, + }); + + _vertexCount++; + } + + var name = _morphNames != null ? _morphNames[morphIndex] : $"unnamed_shape_{morphIndex}"; + morphShapeValues.Add(name, shapeValues); + } + + _shapeValues = morphShapeValues; + } +} diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index cae068db..6bb9971c 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -20,6 +20,8 @@ public class VertexAttribute /// Build a byte array containing this vertex attribute's data, as modified by the specified morph target, for the specified vertex index. public readonly BuildMorphFn BuildMorph; + public byte Stream => Element.Stream; + /// Size in bytes of a single vertex's attribute value. public byte Size => (MdlFile.VertexType)Element.Type switch { diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index b3a1d9a1..3153da78 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -462,22 +462,22 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable 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; + 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(vertStreams[i]); - indexCount += idxCount; + 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(idxs.Select(idx => (ushort)(idx + vertOff))); - submeshes.Add(submesh with + indices.AddRange(subMeshThingy.Indices.Select(idx => (ushort)(idx + vertOff))); + submeshes.Add(subMeshThingy.Struct with { - IndexOffset = submesh.IndexOffset + idxOff + 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 subMorphData) + foreach (var (key, shapeValues) in subMeshThingy.ShapeValues) { List valueList; if (!morphData.TryGetValue(key, out valueList)) @@ -601,201 +601,6 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable return (jointNames, usedJoints); } - private ( - MdlStructs.VertexDeclarationStruct, - byte[], - // MdlStructs.MeshStruct, - MdlStructs.SubmeshStruct, - ushort, - IEnumerable[], - uint, - IEnumerable, - IDictionary> - ) NodeMeshThing(Node node, IDictionary? nodeBoneMap) - { - // BoneTable (mesh.btidx = 255 means unskinned) - // vertexdecl - - var mesh = node.Mesh; - - // TODO: should probably say _what_ mesh - // TODO: would be cool to support >1 primitive (esp. given they're effectively what submeshes are modeled as), but blender doesn't really use them, so not going to prio that at all. - if (mesh.Primitives.Count != 1) - throw new Exception($"Mesh has {mesh.Primitives.Count} primitives, expected 1."); - var primitive = mesh.Primitives[0]; - - var accessors = primitive.VertexAccessors; - - // var foo = primitive.GetMorphTargetAccessors(0); - // var bar = foo["POSITION"]; - // var baz = bar.AsVector3Array(); - - var morphAccessors = Enumerable.Range(0, primitive.MorphTargetsCount) - // todo: map by name, probably? or do that later (probably later) - .Select(index => primitive.GetMorphTargetAccessors(index)); - - // TODO: name - var morphChangedVerts = Enumerable.Range(0, primitive.MorphTargetsCount) - .Select(_ => new List()) - .ToArray(); - - var rawAttributes = new[] { - VertexAttribute.Position(accessors, morphAccessors), - VertexAttribute.BlendWeight(accessors), - VertexAttribute.BlendIndex(accessors, nodeBoneMap), - VertexAttribute.Normal(accessors, morphAccessors), - VertexAttribute.Tangent1(accessors, morphAccessors), - VertexAttribute.Color(accessors), - VertexAttribute.Uv(accessors), - }; - - var attributes = new List(); - var offsets = new byte[] { 0, 0, 0 }; - foreach (var attribute in rawAttributes) - { - if (attribute == null) continue; - var element = attribute.Element; - attributes.Add(attribute.WithOffset(offsets[element.Stream])); - offsets[element.Stream] += attribute.Size; - } - 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(); - - // todo: this is a bit lmao but also... probably the most sane option? getting the count that is - var vertexCount = primitive.VertexAccessors["POSITION"].Count; - for (var vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) - { - foreach (var attribute in attributes) - { - streams[attribute.Element.Stream].AddRange(attribute.Build(vertexIndex)); - } - - // this is a meme but idk maybe it's the best approach? it's not like the attr array is ever long - foreach (var (list, morphIndex) in morphChangedVerts.WithIndex()) - { - var hasMorph = attributes.Aggregate(false, (cur, attr) => cur || attr.HasMorph(morphIndex, vertexIndex)); - // Penumbra.Log.Information($"eh? {vertexIndex} {morphIndex}: {hasMorph}"); - if (hasMorph) - { - list.Add(vertexIndex); - } - } - } - - // indices - // var indexCount = primitive.GetIndexAccessor().Count; - // var indices = primitive.GetIndices() - // .SelectMany(index => BitConverter.GetBytes((ushort)index)) - // .ToArray(); - var indices = primitive.GetIndices().Select(idx => (ushort)idx).ToArray(); - - // BLAH - // foreach (var (list, morphIndex) in morphChangedVerts.WithIndex()) - // { - // Penumbra.Log.Information($"morph {morphIndex}: {string.Join(",", list)}"); - // } - // TODO BUILD THE MORPH VERTS - // (source, target) - var morphmappingstuff = new List[morphChangedVerts.Length]; - foreach (var (list, morphIndex) in morphChangedVerts.WithIndex()) - { - var morphmaplist = morphmappingstuff[morphIndex] = new(); - foreach (var vertIdx in list) - { - foreach (var attribute in attributes) - { - streams[attribute.Element.Stream].AddRange(attribute.BuildMorph(morphIndex, vertIdx)); - } - - var fuck = indices.WithIndex() - .Where(pair => pair.Value == vertIdx) - .Select(pair => pair.Index); - - foreach (var something in fuck) - { - morphmaplist.Add(new MdlStructs.ShapeValueStruct() - { - BaseIndicesIndex = (ushort)something, - ReplacingVertexIndex = (ushort)vertexCount, - }); - } - vertexCount++; - } - } - - // TODO: HANDLE THIS BEING MISSING - probably warn or something, it's not the end of the world - var morphData = new Dictionary>(); - if (morphmappingstuff.Length > 0) - { - var morphnames = mesh.Extras.GetNode("targetNames").Deserialize>(); - morphData = morphmappingstuff - .Zip(morphnames) - .ToDictionary( - (pair) => pair.Second, - (pair) => pair.First - ); - } - - // one of these per mesh - var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() - { - VertexElements = attributes.Select(attribute => attribute.Element).ToArray(), - }; - - // 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 = (ushort)vertexCount, - // IndexCount = (uint)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 = 1, - // // 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), - // }; - - // submesh - // TODO: once we have multiple submeshes, the _first_ should probably set an index offset of 0, and then further ones delta from there - and then they can be blindly adjusted by the parent that's laying out the meshes. - var xivSubmesh = new MdlStructs.SubmeshStruct() - { - IndexOffset = 0, - IndexCount = (uint)indices.Length, - AttributeIndexMask = 0, - // TODO: not sure how i want to handle these ones - BoneStartIndex = 0, - BoneCount = 1, - }; - - // var vertexBuffer = streams[0].Concat(streams[1]).Concat(streams[2]); - - return ( - vertexDeclaration, - strides, - // xivMesh, - xivSubmesh, - (ushort)vertexCount, - streams, - (uint)indices.Length, - indices, - morphData - ); - } - public bool Equals(IAction? other) { if (other is not ImportGltfAction rhs)