Clean up submeshes

This commit is contained in:
ackwell 2024-01-06 16:37:41 +11:00
parent b5b3e1b1f2
commit 6de3077afa
3 changed files with 229 additions and 205 deletions

View file

@ -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<byte>[] Streams;
public ushort[] Indices;
public Dictionary<string, List<MdlStructs.ShapeValueStruct>> ShapeValues;
}
public static SubMesh Import(Node node, IDictionary<ushort, ushort>? nodeBoneMap)
{
var importer = new SubMeshImporter(node, nodeBoneMap);
return importer.Create();
}
private readonly MeshPrimitive _primitive;
private readonly IDictionary<ushort, ushort>? _nodeBoneMap;
private List<VertexAttribute>? _attributes;
private ushort _vertexCount = 0;
private byte[] _strides = [0, 0, 0];
private readonly List<byte>[] _streams;
private ushort[]? _indices;
private readonly List<string>? _morphNames;
private Dictionary<string, List<MdlStructs.ShapeValueStruct>>? _shapeValues;
private SubMeshImporter(Node node, IDictionary<ushort, ushort>? 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<List<string>>();
}
catch
{
_morphNames = null;
}
// 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>();
}
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<VertexAttribute>();
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<int>())
.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<int>[] morphModifiedVertices)
{
ArgumentNullException.ThrowIfNull(_indices);
ArgumentNullException.ThrowIfNull(_attributes);
var morphShapeValues = new Dictionary<string, List<MdlStructs.ShapeValueStruct>>();
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<MdlStructs.ShapeValueStruct>();
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;
}
}

View file

@ -20,6 +20,8 @@ public class VertexAttribute
/// <summary> Build a byte array containing this vertex attribute's data, as modified by the specified morph target, for the specified vertex index. </summary>
public readonly BuildMorphFn BuildMorph;
public byte Stream => Element.Stream;
/// <summary> Size in bytes of a single vertex's attribute value. </summary>
public byte Size => (MdlFile.VertexType)Element.Type switch
{

View file

@ -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<MdlStructs.ShapeValueStruct> 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<byte>[],
uint,
IEnumerable<ushort>,
IDictionary<string, List<MdlStructs.ShapeValueStruct>>
) NodeMeshThing(Node node, IDictionary<ushort, ushort>? 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<int>())
.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<VertexAttribute>();
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<byte>[3];
for (var i = 0; i < 3; i++)
streams[i] = new List<byte>();
// 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<MdlStructs.ShapeValueStruct>[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<string, List<MdlStructs.ShapeValueStruct>>();
if (morphmappingstuff.Length > 0)
{
var morphnames = mesh.Extras.GetNode("targetNames").Deserialize<List<string>>();
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)