mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-13 12:14:17 +01:00
Import all primitives of a mesh
This commit is contained in:
parent
8487661bc8
commit
b2bd31a166
5 changed files with 341 additions and 218 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
using Lumina.Data.Parsing;
|
using Lumina.Data.Parsing;
|
||||||
|
using OtterGui;
|
||||||
using SharpGLTF.Schema2;
|
using SharpGLTF.Schema2;
|
||||||
|
|
||||||
namespace Penumbra.Import.Models.Import;
|
namespace Penumbra.Import.Models.Import;
|
||||||
|
|
@ -117,21 +118,19 @@ public class MeshImporter(IEnumerable<Node> nodes, IoNotifier notifier)
|
||||||
|
|
||||||
var subMeshName = node.Name ?? node.Mesh.Name;
|
var subMeshName = node.Name ?? node.Mesh.Name;
|
||||||
|
|
||||||
var nodeBoneMap = CreateNodeBoneMap(node);
|
var subNotifier = notifier.WithContext($"Sub-mesh {subMeshName}");
|
||||||
var subMesh = SubMeshImporter.Import(node, nodeBoneMap, notifier.WithContext($"Sub-mesh {subMeshName}"));
|
var nodeBoneMap = CreateNodeBoneMap(node, subNotifier);
|
||||||
|
var subMesh = SubMeshImporter.Import(node, nodeBoneMap, subNotifier);
|
||||||
|
|
||||||
// TODO: Record a warning if there's a mismatch between current and incoming, as we can't support multiple materials per mesh.
|
|
||||||
_material ??= subMesh.Material;
|
_material ??= subMesh.Material;
|
||||||
|
if (subMesh.Material != null && _material != subMesh.Material)
|
||||||
|
notifier.Warning($"Meshes may only reference one material. Sub-mesh {subMeshName} material \"{subMesh.Material}\" has been ignored.");
|
||||||
|
|
||||||
// Check that vertex declarations match - we need to combine the buffers, so a mismatch would take a whole load of resolution.
|
// Check that vertex declarations match - we need to combine the buffers, so a mismatch would take a whole load of resolution.
|
||||||
if (_vertexDeclaration == null)
|
if (_vertexDeclaration == null)
|
||||||
_vertexDeclaration = subMesh.VertexDeclaration;
|
_vertexDeclaration = subMesh.VertexDeclaration;
|
||||||
else if (VertexDeclarationMismatch(subMesh.VertexDeclaration, _vertexDeclaration.Value))
|
else
|
||||||
throw notifier.Exception(
|
Utility.EnsureVertexDeclarationMatch(_vertexDeclaration.Value, subMesh.VertexDeclaration, notifier);
|
||||||
$@"All sub-meshes of a mesh must have equivalent vertex declarations.
|
|
||||||
Current: {FormatVertexDeclaration(_vertexDeclaration.Value)}
|
|
||||||
Sub-mesh ""{subMeshName}"": {FormatVertexDeclaration(subMesh.VertexDeclaration)}"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Given that strides are derived from declarations, a lack of mismatch in declarations means the strides are fine.
|
// 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 sub mesh return structure and computing when needed.
|
// TODO: I mean, given that strides are derivable, might be worth dropping strides from the sub mesh return structure and computing when needed.
|
||||||
|
|
@ -173,27 +172,7 @@ public class MeshImporter(IEnumerable<Node> nodes, IoNotifier notifier)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FormatVertexDeclaration(MdlStructs.VertexDeclarationStruct vertexDeclaration)
|
private Dictionary<ushort, ushort>? CreateNodeBoneMap(Node node, IoNotifier notifier)
|
||||||
=> string.Join(", ", vertexDeclaration.VertexElements.Select(element => $"{element.Usage} ({element.Type}@{element.Stream}:{element.Offset})"));
|
|
||||||
|
|
||||||
private static 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.
|
// Unskinned assets can skip this all of this.
|
||||||
if (node.Skin == null)
|
if (node.Skin == null)
|
||||||
|
|
@ -205,32 +184,27 @@ public class MeshImporter(IEnumerable<Node> nodes, IoNotifier notifier)
|
||||||
.Select(index => node.Skin.GetJoint(index).Joint.Name ?? "unnamed_joint")
|
.Select(index => node.Skin.GetJoint(index).Joint.Name ?? "unnamed_joint")
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
// TODO: This is duplicated with the sub mesh 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 notifier.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")
|
|
||||||
?? throw notifier.Exception($"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>();
|
var usedJoints = new HashSet<ushort>();
|
||||||
foreach (var joints in jointsAccessor.AsVector4Array())
|
|
||||||
|
foreach (var (primitive, primitiveIndex) in node.Mesh.Primitives.WithIndex())
|
||||||
{
|
{
|
||||||
for (var index = 0; index < 4; index++)
|
// Per glTF specification, an asset with a skin MUST contain skinning attributes on its meshes.
|
||||||
usedJoints.Add((ushort)joints[index]);
|
var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0")
|
||||||
|
?? throw notifier.Exception($"Primitive {primitiveIndex} 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.
|
||||||
|
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.
|
// Only initialise the bones list if we're actually going to put something in it.
|
||||||
_bones ??= [];
|
_bones ??= [];
|
||||||
|
|
||||||
// Build a dictionary of node-specific joint indices mesh-wide bone indices.
|
// Build a dictionary of node-specific joint indices mapped to mesh-wide bone indices.
|
||||||
var nodeBoneMap = new Dictionary<ushort, ushort>();
|
var nodeBoneMap = new Dictionary<ushort, ushort>();
|
||||||
foreach (var usedJoint in usedJoints)
|
foreach (var usedJoint in usedJoints)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
210
Penumbra/Import/Models/Import/PrimitiveImporter.cs
Normal file
210
Penumbra/Import/Models/Import/PrimitiveImporter.cs
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
using Lumina.Data.Parsing;
|
||||||
|
using OtterGui;
|
||||||
|
using SharpGLTF.Schema2;
|
||||||
|
|
||||||
|
namespace Penumbra.Import.Models.Import;
|
||||||
|
|
||||||
|
public class PrimitiveImporter
|
||||||
|
{
|
||||||
|
public struct Primitive
|
||||||
|
{
|
||||||
|
public string? Material;
|
||||||
|
|
||||||
|
public MdlStructs.VertexDeclarationStruct VertexDeclaration;
|
||||||
|
|
||||||
|
public ushort VertexCount;
|
||||||
|
public byte[] Strides;
|
||||||
|
public List<byte>[] Streams;
|
||||||
|
|
||||||
|
public ushort[] Indices;
|
||||||
|
|
||||||
|
public BoundingBox BoundingBox;
|
||||||
|
|
||||||
|
public List<List<MdlStructs.ShapeValueStruct>> ShapeValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Primitive Import(MeshPrimitive primitive, IDictionary<ushort, ushort>? nodeBoneMap, IoNotifier notifier)
|
||||||
|
{
|
||||||
|
var importer = new PrimitiveImporter(primitive, nodeBoneMap, notifier);
|
||||||
|
return importer.Create();
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly IoNotifier _notifier;
|
||||||
|
|
||||||
|
private readonly MeshPrimitive _primitive;
|
||||||
|
private readonly IDictionary<ushort, ushort>? _nodeBoneMap;
|
||||||
|
|
||||||
|
private ushort[]? _indices;
|
||||||
|
|
||||||
|
private List<VertexAttribute>? _vertexAttributes;
|
||||||
|
|
||||||
|
private ushort _vertexCount;
|
||||||
|
private byte[] _strides = [0, 0, 0];
|
||||||
|
private readonly List<byte>[] _streams = [[], [], []];
|
||||||
|
|
||||||
|
private BoundingBox _boundingBox = new BoundingBox();
|
||||||
|
|
||||||
|
private List<List<MdlStructs.ShapeValueStruct>>? _shapeValues;
|
||||||
|
|
||||||
|
private PrimitiveImporter(MeshPrimitive primitive, IDictionary<ushort, ushort>? nodeBoneMap, IoNotifier notifier)
|
||||||
|
{
|
||||||
|
_notifier = notifier;
|
||||||
|
_primitive = primitive;
|
||||||
|
_nodeBoneMap = nodeBoneMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Primitive Create()
|
||||||
|
{
|
||||||
|
// TODO: This structure is verging on a little silly. Reconsider.
|
||||||
|
BuildIndices();
|
||||||
|
BuildVertexAttributes();
|
||||||
|
BuildVertices();
|
||||||
|
BuildBoundingBox();
|
||||||
|
|
||||||
|
ArgumentNullException.ThrowIfNull(_vertexAttributes);
|
||||||
|
ArgumentNullException.ThrowIfNull(_indices);
|
||||||
|
ArgumentNullException.ThrowIfNull(_shapeValues);
|
||||||
|
|
||||||
|
var material = _primitive.Material.Name;
|
||||||
|
if (material == "")
|
||||||
|
material = null;
|
||||||
|
|
||||||
|
return new Primitive
|
||||||
|
{
|
||||||
|
Material = material,
|
||||||
|
VertexDeclaration = new MdlStructs.VertexDeclarationStruct
|
||||||
|
{
|
||||||
|
VertexElements = _vertexAttributes.Select(attribute => attribute.Element).ToArray(),
|
||||||
|
},
|
||||||
|
VertexCount = _vertexCount,
|
||||||
|
Strides = _strides,
|
||||||
|
Streams = _streams,
|
||||||
|
Indices = _indices,
|
||||||
|
BoundingBox = _boundingBox,
|
||||||
|
ShapeValues = _shapeValues,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildIndices()
|
||||||
|
{
|
||||||
|
// TODO: glTF supports a bunch of primitive types, ref. Schema2.PrimitiveType. All this code is currently assuming that it's using plain triangles (4). It should probably be generalised to other formats - I _suspect_ we should be able to get away with evaluating the indices to triangles with GetTriangleIndices, but will need investigation.
|
||||||
|
_indices = _primitive.GetIndices().Select(idx => (ushort)idx).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildVertexAttributes()
|
||||||
|
{
|
||||||
|
// Tangent calculation requires indices if missing.
|
||||||
|
ArgumentNullException.ThrowIfNull(_indices);
|
||||||
|
|
||||||
|
var accessors = _primitive.VertexAccessors;
|
||||||
|
|
||||||
|
var morphAccessors = Enumerable.Range(0, _primitive.MorphTargetsCount)
|
||||||
|
.Select(index => _primitive.GetMorphTargetAccessors(index)).ToList();
|
||||||
|
|
||||||
|
// 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, _notifier),
|
||||||
|
VertexAttribute.BlendWeight(accessors, _notifier),
|
||||||
|
VertexAttribute.BlendIndex(accessors, _nodeBoneMap, _notifier),
|
||||||
|
VertexAttribute.Normal(accessors, morphAccessors),
|
||||||
|
VertexAttribute.Tangent1(accessors, morphAccessors, _indices, _notifier),
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
_vertexAttributes = attributes;
|
||||||
|
// After building the attributes, the resulting next offsets are our stream strides.
|
||||||
|
_strides = offsets;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildVertices()
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(_vertexAttributes);
|
||||||
|
|
||||||
|
// 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 _vertexAttributes)
|
||||||
|
_streams[attribute.Stream].AddRange(attribute.Build(vertexIndex));
|
||||||
|
|
||||||
|
// Record which morph targets have values for this vertex, if any.
|
||||||
|
var changedMorphs = morphModifiedVertices
|
||||||
|
.WithIndex()
|
||||||
|
.Where(pair => _vertexAttributes.Any(attribute => attribute.HasMorph(pair.Index, vertexIndex)))
|
||||||
|
.Select(pair => pair.Value);
|
||||||
|
foreach (var modifiedVertices in changedMorphs)
|
||||||
|
modifiedVertices.Add(vertexIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildShapeValues(morphModifiedVertices);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildShapeValues(IEnumerable<List<int>> morphModifiedVertices)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(_indices);
|
||||||
|
ArgumentNullException.ThrowIfNull(_vertexAttributes);
|
||||||
|
|
||||||
|
var morphShapeValues = new List<List<MdlStructs.ShapeValueStruct>>();
|
||||||
|
|
||||||
|
foreach (var (modifiedVertices, morphIndex) in morphModifiedVertices.WithIndex())
|
||||||
|
{
|
||||||
|
// 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 _vertexAttributes)
|
||||||
|
_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));
|
||||||
|
shapeValues.AddRange(targetingIndices.Select(targetingIndex => new MdlStructs.ShapeValueStruct
|
||||||
|
{
|
||||||
|
BaseIndicesIndex = (ushort)targetingIndex,
|
||||||
|
ReplacingVertexIndex = _vertexCount,
|
||||||
|
}));
|
||||||
|
|
||||||
|
_vertexCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
morphShapeValues.Add(shapeValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
_shapeValues = morphShapeValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildBoundingBox()
|
||||||
|
{
|
||||||
|
var positions = _primitive.VertexAccessors["POSITION"].AsVector3Array();
|
||||||
|
foreach (var position in positions)
|
||||||
|
_boundingBox.Merge(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,7 +19,7 @@ public class SubMeshImporter
|
||||||
public byte[] Strides;
|
public byte[] Strides;
|
||||||
public List<byte>[] Streams;
|
public List<byte>[] Streams;
|
||||||
|
|
||||||
public ushort[] Indices;
|
public List<ushort> Indices;
|
||||||
|
|
||||||
public BoundingBox BoundingBox;
|
public BoundingBox BoundingBox;
|
||||||
|
|
||||||
|
|
@ -36,85 +36,56 @@ public class SubMeshImporter
|
||||||
|
|
||||||
private readonly IoNotifier _notifier;
|
private readonly IoNotifier _notifier;
|
||||||
|
|
||||||
private readonly MeshPrimitive _primitive;
|
private readonly Node _node;
|
||||||
private readonly IDictionary<ushort, ushort>? _nodeBoneMap;
|
private readonly IDictionary<ushort, ushort>? _nodeBoneMap;
|
||||||
private readonly IDictionary<string, JsonElement>? _nodeExtras;
|
|
||||||
|
|
||||||
private List<VertexAttribute>? _vertexAttributes;
|
private string? _material;
|
||||||
|
|
||||||
private ushort _vertexCount;
|
private MdlStructs.VertexDeclarationStruct? _vertexDeclaration;
|
||||||
private byte[] _strides = [0, 0, 0];
|
private ushort _vertexCount;
|
||||||
private readonly List<byte>[] _streams;
|
private byte[]? _strides;
|
||||||
|
private readonly List<byte>[] _streams = [[], [], []];
|
||||||
|
|
||||||
private ushort[]? _indices;
|
private List<ushort> _indices = [];
|
||||||
|
|
||||||
private BoundingBox _boundingBox = new BoundingBox();
|
private BoundingBox _boundingBox = new BoundingBox();
|
||||||
|
|
||||||
private string[]? _metaAttributes;
|
private readonly List<string>? _morphNames;
|
||||||
|
private Dictionary<string, List<MdlStructs.ShapeValueStruct>> _shapeValues = [];
|
||||||
private readonly List<string>? _morphNames;
|
|
||||||
private Dictionary<string, List<MdlStructs.ShapeValueStruct>>? _shapeValues;
|
|
||||||
|
|
||||||
private SubMeshImporter(Node node, IDictionary<ushort, ushort>? nodeBoneMap, IoNotifier notifier)
|
private SubMeshImporter(Node node, IDictionary<ushort, ushort>? nodeBoneMap, IoNotifier notifier)
|
||||||
{
|
{
|
||||||
_notifier = notifier;
|
_notifier = notifier;
|
||||||
|
_node = node;
|
||||||
var mesh = node.Mesh;
|
|
||||||
|
|
||||||
var primitiveCount = mesh.Primitives.Count;
|
|
||||||
if (primitiveCount != 1)
|
|
||||||
throw _notifier.Exception($"Mesh has {primitiveCount} primitives, expected 1.");
|
|
||||||
|
|
||||||
_primitive = mesh.Primitives[0];
|
|
||||||
_nodeBoneMap = nodeBoneMap;
|
_nodeBoneMap = nodeBoneMap;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_nodeExtras = node.Extras.Deserialize<Dictionary<string, JsonElement>>();
|
_morphNames = node.Mesh.Extras.GetNode("targetNames").Deserialize<List<string>>();
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
_nodeExtras = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_morphNames = mesh.Extras.GetNode("targetNames").Deserialize<List<string>>();
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
_morphNames = null;
|
_morphNames = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// All meshes may use up to 3 byte streams.
|
|
||||||
_streams = new List<byte>[3];
|
|
||||||
for (var streamIndex = 0; streamIndex < 3; streamIndex++)
|
|
||||||
_streams[streamIndex] = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private SubMesh Create()
|
private SubMesh Create()
|
||||||
{
|
{
|
||||||
// Build all the data we'll need.
|
// Build all the data we'll need.
|
||||||
// TODO: This structure is verging on a little silly. Reconsider.
|
foreach (var (primitive, index) in _node.Mesh.Primitives.WithIndex())
|
||||||
BuildIndices();
|
BuildPrimitive(primitive, index);
|
||||||
BuildVertexAttributes();
|
|
||||||
BuildVertices();
|
|
||||||
BuildBoundingBox();
|
|
||||||
BuildMetaAttributes();
|
|
||||||
|
|
||||||
ArgumentNullException.ThrowIfNull(_indices);
|
ArgumentNullException.ThrowIfNull(_indices);
|
||||||
ArgumentNullException.ThrowIfNull(_vertexAttributes);
|
ArgumentNullException.ThrowIfNull(_vertexDeclaration);
|
||||||
|
ArgumentNullException.ThrowIfNull(_strides);
|
||||||
ArgumentNullException.ThrowIfNull(_shapeValues);
|
ArgumentNullException.ThrowIfNull(_shapeValues);
|
||||||
ArgumentNullException.ThrowIfNull(_metaAttributes);
|
|
||||||
|
|
||||||
var material = _primitive.Material.Name;
|
var metaAttributes = BuildMetaAttributes();
|
||||||
if (material == "")
|
|
||||||
material = null;
|
|
||||||
|
|
||||||
// At this level, we assume that attributes are wholly controlled by this sub-mesh.
|
// At this level, we assume that attributes are wholly controlled by this sub-mesh.
|
||||||
var attributeMask = _metaAttributes.Length switch
|
var attributeMask = metaAttributes.Length switch
|
||||||
{
|
{
|
||||||
< 32 => (1u << _metaAttributes.Length) - 1,
|
< 32 => (1u << metaAttributes.Length) - 1,
|
||||||
32 => uint.MaxValue,
|
32 => uint.MaxValue,
|
||||||
> 32 => throw _notifier.Exception("Models may utilise a maximum of 32 attributes."),
|
> 32 => throw _notifier.Exception("Models may utilise a maximum of 32 attributes."),
|
||||||
};
|
};
|
||||||
|
|
@ -124,156 +95,92 @@ public class SubMeshImporter
|
||||||
SubMeshStruct = new MdlStructs.SubmeshStruct()
|
SubMeshStruct = new MdlStructs.SubmeshStruct()
|
||||||
{
|
{
|
||||||
IndexOffset = 0,
|
IndexOffset = 0,
|
||||||
IndexCount = (uint)_indices.Length,
|
IndexCount = (uint)_indices.Count,
|
||||||
AttributeIndexMask = attributeMask,
|
AttributeIndexMask = attributeMask,
|
||||||
|
|
||||||
// TODO: Flesh these out. Game doesn't seem to rely on them existing, though.
|
// TODO: Flesh these out. Game doesn't seem to rely on them existing, though.
|
||||||
BoneStartIndex = 0,
|
BoneStartIndex = 0,
|
||||||
BoneCount = 0,
|
BoneCount = 0,
|
||||||
},
|
},
|
||||||
Material = material,
|
Material = _material,
|
||||||
VertexDeclaration = new MdlStructs.VertexDeclarationStruct()
|
VertexDeclaration = _vertexDeclaration.Value,
|
||||||
{
|
|
||||||
VertexElements = _vertexAttributes.Select(attribute => attribute.Element).ToArray(),
|
|
||||||
},
|
|
||||||
VertexCount = _vertexCount,
|
VertexCount = _vertexCount,
|
||||||
Strides = _strides,
|
Strides = _strides,
|
||||||
Streams = _streams,
|
Streams = _streams,
|
||||||
Indices = _indices,
|
Indices = _indices,
|
||||||
BoundingBox = _boundingBox,
|
BoundingBox = _boundingBox,
|
||||||
MetaAttributes = _metaAttributes,
|
MetaAttributes = metaAttributes,
|
||||||
ShapeValues = _shapeValues,
|
ShapeValues = _shapeValues,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BuildIndices()
|
private void BuildPrimitive(MeshPrimitive meshPrimitive, int index)
|
||||||
{
|
{
|
||||||
// TODO: glTF supports a bunch of primitive types, ref. Schema2.PrimitiveType. All this code is currently assuming that it's using plain triangles (4). It should probably be generalised to other formats - I _suspect_ we should be able to get away with evaluating the indices to triangles with GetTriangleIndices, but will need investigation.
|
var vertexOffset = _vertexCount;
|
||||||
_indices = _primitive.GetIndices().Select(idx => (ushort)idx).ToArray();
|
var indexOffset = _indices.Count;
|
||||||
}
|
|
||||||
|
|
||||||
private void BuildVertexAttributes()
|
var primitive = PrimitiveImporter.Import(meshPrimitive, _nodeBoneMap, _notifier.WithContext($"Primitive {index}"));
|
||||||
{
|
|
||||||
// Tangent calculation requires indices if missing.
|
|
||||||
ArgumentNullException.ThrowIfNull(_indices);
|
|
||||||
|
|
||||||
var accessors = _primitive.VertexAccessors;
|
// Material
|
||||||
|
_material ??= primitive.Material;
|
||||||
|
if (primitive.Material != null && _material != primitive.Material)
|
||||||
|
_notifier.Warning($"Meshes may only reference one material. Primitive {index} material \"{primitive.Material}\" has been ignored.");
|
||||||
|
|
||||||
var morphAccessors = Enumerable.Range(0, _primitive.MorphTargetsCount)
|
// Vertex metadata
|
||||||
.Select(index => _primitive.GetMorphTargetAccessors(index)).ToList();
|
if (_vertexDeclaration == null)
|
||||||
|
_vertexDeclaration = primitive.VertexDeclaration;
|
||||||
|
else
|
||||||
|
Utility.EnsureVertexDeclarationMatch(_vertexDeclaration.Value, primitive.VertexDeclaration, _notifier);
|
||||||
|
|
||||||
// Try to build all the attributes the mesh might use.
|
_strides ??= primitive.Strides;
|
||||||
// The order here is chosen to match a typical model's element order.
|
|
||||||
var rawAttributes = new[]
|
// Vertices
|
||||||
|
_vertexCount += primitive.VertexCount;
|
||||||
|
|
||||||
|
foreach (var (stream, primitiveStream) in _streams.Zip(primitive.Streams))
|
||||||
|
stream.AddRange(primitiveStream);
|
||||||
|
|
||||||
|
// Indices
|
||||||
|
_indices.AddRange(primitive.Indices.Select(index => (ushort)(index + vertexOffset)));
|
||||||
|
|
||||||
|
// Shape values
|
||||||
|
foreach (var (primitiveShapeValues, morphIndex) in primitive.ShapeValues.WithIndex())
|
||||||
{
|
{
|
||||||
VertexAttribute.Position(accessors, morphAccessors, _notifier),
|
// Per glTF spec, all primitives MUST have the same number of morph targets in the same order.
|
||||||
VertexAttribute.BlendWeight(accessors, _notifier),
|
// As such, this lookup should be safe - a failure here is a broken glTF file.
|
||||||
VertexAttribute.BlendIndex(accessors, _nodeBoneMap, _notifier),
|
var name = _morphNames != null ? _morphNames[morphIndex] : $"unnamed_shape_{morphIndex}";
|
||||||
VertexAttribute.Normal(accessors, morphAccessors),
|
|
||||||
VertexAttribute.Tangent1(accessors, morphAccessors, _indices, _notifier),
|
|
||||||
VertexAttribute.Color(accessors),
|
|
||||||
VertexAttribute.Uv(accessors),
|
|
||||||
};
|
|
||||||
|
|
||||||
var attributes = new List<VertexAttribute>();
|
if (!_shapeValues.TryGetValue(name, out var subMeshShapeValues))
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
_vertexAttributes = attributes;
|
|
||||||
// After building the attributes, the resulting next offsets are our stream strides.
|
|
||||||
_strides = offsets;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void BuildVertices()
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(_vertexAttributes);
|
|
||||||
|
|
||||||
// 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 _vertexAttributes)
|
|
||||||
_streams[attribute.Stream].AddRange(attribute.Build(vertexIndex));
|
|
||||||
|
|
||||||
// Record which morph targets have values for this vertex, if any.
|
|
||||||
var changedMorphs = morphModifiedVertices
|
|
||||||
.WithIndex()
|
|
||||||
.Where(pair => _vertexAttributes.Any(attribute => attribute.HasMorph(pair.Index, vertexIndex)))
|
|
||||||
.Select(pair => pair.Value);
|
|
||||||
foreach (var modifiedVertices in changedMorphs)
|
|
||||||
modifiedVertices.Add(vertexIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
BuildShapeValues(morphModifiedVertices);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void BuildShapeValues(IEnumerable<List<int>> morphModifiedVertices)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(_indices);
|
|
||||||
ArgumentNullException.ThrowIfNull(_vertexAttributes);
|
|
||||||
|
|
||||||
var morphShapeValues = new Dictionary<string, List<MdlStructs.ShapeValueStruct>>();
|
|
||||||
|
|
||||||
foreach (var (modifiedVertices, morphIndex) in morphModifiedVertices.WithIndex())
|
|
||||||
{
|
|
||||||
// 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.
|
subMeshShapeValues = [];
|
||||||
foreach (var attribute in _vertexAttributes)
|
_shapeValues.Add(name, subMeshShapeValues);
|
||||||
_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));
|
|
||||||
shapeValues.AddRange(targetingIndices.Select(targetingIndex => new MdlStructs.ShapeValueStruct
|
|
||||||
{
|
|
||||||
BaseIndicesIndex = (ushort)targetingIndex,
|
|
||||||
ReplacingVertexIndex = _vertexCount,
|
|
||||||
}));
|
|
||||||
|
|
||||||
_vertexCount++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var name = _morphNames != null ? _morphNames[morphIndex] : $"unnamed_shape_{morphIndex}";
|
subMeshShapeValues.AddRange(primitiveShapeValues.Select(value => value with
|
||||||
morphShapeValues.Add(name, shapeValues);
|
{
|
||||||
|
BaseIndicesIndex = (ushort)(value.BaseIndicesIndex + indexOffset),
|
||||||
|
ReplacingVertexIndex = (ushort)(value.ReplacingVertexIndex + vertexOffset),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
_shapeValues = morphShapeValues;
|
// Bounds
|
||||||
|
_boundingBox.Merge(primitive.BoundingBox);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BuildBoundingBox()
|
private string[] BuildMetaAttributes()
|
||||||
{
|
{
|
||||||
var positions = _primitive.VertexAccessors["POSITION"].AsVector3Array();
|
Dictionary<string, JsonElement>? nodeExtras;
|
||||||
foreach (var position in positions)
|
try
|
||||||
_boundingBox.Merge(position);
|
{
|
||||||
}
|
nodeExtras = _node.Extras.Deserialize<Dictionary<string, JsonElement>>();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
nodeExtras = null;
|
||||||
|
}
|
||||||
|
|
||||||
private void BuildMetaAttributes()
|
|
||||||
{
|
|
||||||
// We consider any "extras" key with a boolean value set to `true` to be an attribute.
|
// We consider any "extras" key with a boolean value set to `true` to be an attribute.
|
||||||
_metaAttributes = _nodeExtras?
|
return nodeExtras?
|
||||||
.Where(pair => pair.Value.ValueKind == JsonValueKind.True)
|
.Where(pair => pair.Value.ValueKind == JsonValueKind.True)
|
||||||
.Select(pair => pair.Key)
|
.Select(pair => pair.Key)
|
||||||
.ToArray() ?? [];
|
.ToArray() ?? [];
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
using Lumina.Data.Parsing;
|
||||||
|
|
||||||
namespace Penumbra.Import.Models.Import;
|
namespace Penumbra.Import.Models.Import;
|
||||||
|
|
||||||
public static class Utility
|
public static class Utility
|
||||||
|
|
@ -31,4 +33,35 @@ public static class Utility
|
||||||
|
|
||||||
return newMask;
|
return newMask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary> Ensures that the two vertex declarations provided are equal, throwing if not. </summary>
|
||||||
|
public static void EnsureVertexDeclarationMatch(MdlStructs.VertexDeclarationStruct current, MdlStructs.VertexDeclarationStruct @new, IoNotifier notifier)
|
||||||
|
{
|
||||||
|
if (VertexDeclarationMismatch(current, @new))
|
||||||
|
throw notifier.Exception(
|
||||||
|
$@"All sub-meshes of a mesh must have equivalent vertex declarations.
|
||||||
|
Current: {FormatVertexDeclaration(current)}
|
||||||
|
New: {FormatVertexDeclaration(@new)}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatVertexDeclaration(MdlStructs.VertexDeclarationStruct vertexDeclaration)
|
||||||
|
=> string.Join(", ", vertexDeclaration.VertexElements.Select(element => $"{element.Usage} ({element.Type}@{element.Stream}:{element.Offset})"));
|
||||||
|
|
||||||
|
private static 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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ using Lumina.Data.Parsing;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using Penumbra.GameData;
|
using Penumbra.GameData;
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
using Penumbra.Import.Models;
|
|
||||||
using Penumbra.Import.Models.Export;
|
using Penumbra.Import.Models.Export;
|
||||||
using Penumbra.Meta.Manipulations;
|
using Penumbra.Meta.Manipulations;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue