mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Clean up submeshes
This commit is contained in:
parent
b5b3e1b1f2
commit
6de3077afa
3 changed files with 229 additions and 205 deletions
217
Penumbra/Import/Models/Import/SubMeshImporter.cs
Normal file
217
Penumbra/Import/Models/Import/SubMeshImporter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue