Merge branch 'mdl-io-triage-3'

This commit is contained in:
Ottermandias 2024-01-23 23:23:17 +01:00
commit b543d9fc1d
9 changed files with 424 additions and 289 deletions

@ -1 +1 @@
Subproject commit ab09e21fa46be83f82c400dfd2fe05a281b6f28d Subproject commit 63f4de7305616b6cb8921513e5d83baa8913353f

View file

@ -297,8 +297,8 @@ public class MeshExporter
{ {
MdlFile.VertexType.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()),
MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()),
MdlFile.VertexType.UInt => reader.ReadBytes(4), MdlFile.VertexType.UByte4 => reader.ReadBytes(4),
MdlFile.VertexType.ByteFloat4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, MdlFile.VertexType.NByte4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f,
reader.ReadByte() / 255f), reader.ReadByte() / 255f),
MdlFile.VertexType.Half2 => new Vector2((float)reader.ReadHalf(), (float)reader.ReadHalf()), MdlFile.VertexType.Half2 => new Vector2((float)reader.ReadHalf(), (float)reader.ReadHalf()),
MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(),

View file

@ -2,11 +2,11 @@ using Lumina.Data.Parsing;
namespace Penumbra.Import.Models.Import; namespace Penumbra.Import.Models.Import;
/// <summary> Mutable representation of the bounding box surrouding a collection of vertices. </summary> /// <summary> Mutable representation of the bounding box surrounding a collection of vertices. </summary>
public class BoundingBox public class BoundingBox
{ {
private Vector3 _minimum = Vector3.Zero; private Vector3 _minimum = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
private Vector3 _maximum = Vector3.Zero; private Vector3 _maximum = new(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity);
/// <summary> Use the specified position to update this bounding box, expanding it if necessary. </summary> /// <summary> Use the specified position to update this bounding box, expanding it if necessary. </summary>
public void Merge(Vector3 position) public void Merge(Vector3 position)
@ -25,7 +25,7 @@ public class BoundingBox
/// <summary> Convert this bounding box to the struct format used in .mdl data structures. </summary> /// <summary> Convert this bounding box to the struct format used in .mdl data structures. </summary>
public MdlStructs.BoundingBoxStruct ToStruct() public MdlStructs.BoundingBoxStruct ToStruct()
=> new MdlStructs.BoundingBoxStruct => new()
{ {
Min = [_minimum.X, _minimum.Y, _minimum.Z, 1], Min = [_minimum.X, _minimum.Y, _minimum.Z, 1],
Max = [_maximum.X, _maximum.Y, _maximum.Z, 1], Max = [_maximum.X, _maximum.Y, _maximum.Z, 1],

View file

@ -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;
@ -52,7 +53,7 @@ public class MeshImporter(IEnumerable<Node> nodes, IoNotifier notifier)
private List<string>? _bones; private List<string>? _bones;
private readonly BoundingBox _boundingBox = new BoundingBox(); private readonly BoundingBox _boundingBox = new();
private readonly List<string> _metaAttributes = []; private readonly List<string> _metaAttributes = [];
@ -117,21 +118,20 @@ 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 +173,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 +185,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 usedJoints = new HashSet<ushort>();
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]; foreach (var (primitive, primitiveIndex) in node.Mesh.Primitives.WithIndex())
{
// Per glTF specification, an asset with a skin MUST contain skinning attributes on its mesh. // Per glTF specification, an asset with a skin MUST contain skinning attributes on its meshes.
var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0") var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0")
?? throw notifier.Exception($"Mesh \"{meshName}\" is skinned but does not contain skinning vertex attributes."); ?? 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. // 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. // 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()) foreach (var joints in jointsAccessor.AsVector4Array())
{ {
for (var index = 0; index < 4; index++) for (var index = 0; index < 4; index++)
usedJoints.Add((ushort)joints[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)
{ {

View file

@ -0,0 +1,211 @@
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 readonly BoundingBox _boundingBox = new();
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 index = vertexIndex;
var changedMorphs = morphModifiedVertices
.WithIndex()
.Where(pair => _vertexAttributes.Any(attribute => attribute.HasMorph(pair.Index, index)))
.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);
}
}

View file

@ -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 MdlStructs.VertexDeclarationStruct? _vertexDeclaration;
private ushort _vertexCount; private ushort _vertexCount;
private byte[] _strides = [0, 0, 0]; private byte[]? _strides;
private readonly List<byte>[] _streams; private readonly List<byte>[] _streams = [[], [], []];
private ushort[]? _indices; private readonly List<ushort> _indices = [];
private BoundingBox _boundingBox = new BoundingBox(); private readonly BoundingBox _boundingBox = new();
private string[]? _metaAttributes;
private readonly List<string>? _morphNames; private readonly List<string>? _morphNames;
private Dictionary<string, List<MdlStructs.ShapeValueStruct>>? _shapeValues; private readonly 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,158 +95,95 @@ 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}"));
// 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.");
// Vertex metadata
if (_vertexDeclaration == null)
_vertexDeclaration = primitive.VertexDeclaration;
else
Utility.EnsureVertexDeclarationMatch(_vertexDeclaration.Value, primitive.VertexDeclaration, _notifier);
_strides ??= primitive.Strides;
// Vertices
_vertexCount += primitive.VertexCount;
foreach (var (stream, primitiveStream) in _streams.Zip(primitive.Streams))
stream.AddRange(primitiveStream);
// Indices
_indices.AddRange(primitive.Indices.Select(i => (ushort)(i + vertexOffset)));
// Shape values
foreach (var (primitiveShapeValues, morphIndex) in primitive.ShapeValues.WithIndex())
{ {
// Tangent calculation requires indices if missing. // Per glTF spec, all primitives MUST have the same number of morph targets in the same order.
ArgumentNullException.ThrowIfNull(_indices); // As such, this lookup should be safe - a failure here is a broken glTF file.
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 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.
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++;
}
var name = _morphNames != null ? _morphNames[morphIndex] : $"unnamed_shape_{morphIndex}"; var name = _morphNames != null ? _morphNames[morphIndex] : $"unnamed_shape_{morphIndex}";
morphShapeValues.Add(name, shapeValues);
}
_shapeValues = morphShapeValues; if (!_shapeValues.TryGetValue(name, out var subMeshShapeValues))
}
private void BuildBoundingBox()
{ {
var positions = _primitive.VertexAccessors["POSITION"].AsVector3Array(); subMeshShapeValues = [];
foreach (var position in positions) _shapeValues.Add(name, subMeshShapeValues);
_boundingBox.Merge(position);
} }
private void BuildMetaAttributes() subMeshShapeValues.AddRange(primitiveShapeValues.Select(value => value with
{ {
BaseIndicesIndex = (ushort)(value.BaseIndicesIndex + indexOffset),
ReplacingVertexIndex = (ushort)(value.ReplacingVertexIndex + vertexOffset),
}));
}
// Bounds
_boundingBox.Merge(primitive.BoundingBox);
}
private string[] BuildMetaAttributes()
{
Dictionary<string, JsonElement>? nodeExtras;
try
{
nodeExtras = _node.Extras.Deserialize<Dictionary<string, JsonElement>>();
}
catch
{
nodeExtras = null;
}
// 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()
?? [];
} }
} }

View file

@ -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
@ -26,9 +28,45 @@ public static class Utility
newAttributes.Add(metaAttribute); newAttributes.Add(metaAttribute);
attributeIndex = newAttributes.Count - 1; attributeIndex = newAttributes.Count - 1;
} }
newMask |= 1u << attributeIndex; newMask |= 1u << attributeIndex;
} }
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
);
}
} }

View file

@ -30,10 +30,14 @@ public class VertexAttribute
public byte Size public byte Size
=> (MdlFile.VertexType)Element.Type switch => (MdlFile.VertexType)Element.Type switch
{ {
MdlFile.VertexType.Single1 => 4,
MdlFile.VertexType.Single2 => 8,
MdlFile.VertexType.Single3 => 12, MdlFile.VertexType.Single3 => 12,
MdlFile.VertexType.Single4 => 16, MdlFile.VertexType.Single4 => 16,
MdlFile.VertexType.UInt => 4, MdlFile.VertexType.UByte4 => 4,
MdlFile.VertexType.ByteFloat4 => 4, MdlFile.VertexType.Short2 => 4,
MdlFile.VertexType.Short4 => 8,
MdlFile.VertexType.NByte4 => 4,
MdlFile.VertexType.Half2 => 4, MdlFile.VertexType.Half2 => 4,
MdlFile.VertexType.Half4 => 8, MdlFile.VertexType.Half4 => 8,
@ -126,7 +130,7 @@ public class VertexAttribute
var element = new MdlStructs.VertexElement() var element = new MdlStructs.VertexElement()
{ {
Stream = 0, Stream = 0,
Type = (byte)MdlFile.VertexType.ByteFloat4, Type = (byte)MdlFile.VertexType.NByte4,
Usage = (byte)MdlFile.VertexUsage.BlendWeights, Usage = (byte)MdlFile.VertexUsage.BlendWeights,
}; };
@ -134,7 +138,7 @@ public class VertexAttribute
return new VertexAttribute( return new VertexAttribute(
element, element,
index => BuildByteFloat4(values[index]) index => BuildNByte4(values[index])
); );
} }
@ -152,7 +156,7 @@ public class VertexAttribute
var element = new MdlStructs.VertexElement() var element = new MdlStructs.VertexElement()
{ {
Stream = 0, Stream = 0,
Type = (byte)MdlFile.VertexType.UInt, Type = (byte)MdlFile.VertexType.UByte4,
Usage = (byte)MdlFile.VertexUsage.BlendIndices, Usage = (byte)MdlFile.VertexUsage.BlendIndices,
}; };
@ -163,7 +167,7 @@ public class VertexAttribute
index => index =>
{ {
var gltfIndices = values[index]; var gltfIndices = values[index];
return BuildUInt(new Vector4( return BuildUByte4(new Vector4(
boneMap[(ushort)gltfIndices.X], boneMap[(ushort)gltfIndices.X],
boneMap[(ushort)gltfIndices.Y], boneMap[(ushort)gltfIndices.Y],
boneMap[(ushort)gltfIndices.Z], boneMap[(ushort)gltfIndices.Z],
@ -181,7 +185,7 @@ public class VertexAttribute
var element = new MdlStructs.VertexElement() var element = new MdlStructs.VertexElement()
{ {
Stream = 1, Stream = 1,
Type = (byte)MdlFile.VertexType.Half4, Type = (byte)MdlFile.VertexType.Single3,
Usage = (byte)MdlFile.VertexUsage.Normal, Usage = (byte)MdlFile.VertexUsage.Normal,
}; };
@ -193,7 +197,7 @@ public class VertexAttribute
return new VertexAttribute( return new VertexAttribute(
element, element,
index => BuildHalf4(new Vector4(values[index], 0)), index => BuildSingle3(values[index]),
buildMorph: (morphIndex, vertexIndex) => buildMorph: (morphIndex, vertexIndex) =>
{ {
var value = values[vertexIndex]; var value = values[vertexIndex];
@ -202,7 +206,7 @@ public class VertexAttribute
if (delta != null) if (delta != null)
value += delta.Value; value += delta.Value;
return BuildHalf4(new Vector4(value, 0)); return BuildSingle3(value);
} }
); );
} }
@ -224,20 +228,20 @@ public class VertexAttribute
// There's only one TEXCOORD, output UV coordinates as vec2s. // There's only one TEXCOORD, output UV coordinates as vec2s.
if (!accessors.TryGetValue("TEXCOORD_1", out var accessor2)) if (!accessors.TryGetValue("TEXCOORD_1", out var accessor2))
return new VertexAttribute( return new VertexAttribute(
element with { Type = (byte)MdlFile.VertexType.Half2 }, element with { Type = (byte)MdlFile.VertexType.Single2 },
index => BuildHalf2(values1[index]) index => BuildSingle2(values1[index])
); );
var values2 = accessor2.AsVector2Array(); var values2 = accessor2.AsVector2Array();
// Two TEXCOORDs are available, repack them into xiv's vec4 [0X, 0Y, 1X, 1Y] format. // Two TEXCOORDs are available, repack them into xiv's vec4 [0X, 0Y, 1X, 1Y] format.
return new VertexAttribute( return new VertexAttribute(
element with { Type = (byte)MdlFile.VertexType.Half4 }, element with { Type = (byte)MdlFile.VertexType.Single4 },
index => index =>
{ {
var value1 = values1[index]; var value1 = values1[index];
var value2 = values2[index]; var value2 = values2[index];
return BuildHalf4(new Vector4(value1.X, value1.Y, value2.X, value2.Y)); return BuildSingle4(new Vector4(value1.X, value1.Y, value2.X, value2.Y));
} }
); );
} }
@ -264,7 +268,7 @@ public class VertexAttribute
var element = new MdlStructs.VertexElement var element = new MdlStructs.VertexElement
{ {
Stream = 1, Stream = 1,
Type = (byte)MdlFile.VertexType.ByteFloat4, Type = (byte)MdlFile.VertexType.NByte4,
Usage = (byte)MdlFile.VertexUsage.Tangent1, Usage = (byte)MdlFile.VertexUsage.Tangent1,
}; };
@ -305,7 +309,7 @@ public class VertexAttribute
// Byte floats encode 0..1, and bitangents are stored as -1..1. Convert. // Byte floats encode 0..1, and bitangents are stored as -1..1. Convert.
bitangent = (bitangent + Vector3.One) / 2; bitangent = (bitangent + Vector3.One) / 2;
return BuildByteFloat4(new Vector4(bitangent, handedness)); return BuildNByte4(new Vector4(bitangent, handedness));
} }
/// <summary> Attempt to calculate tangent values based on other pre-existing data. </summary> /// <summary> Attempt to calculate tangent values based on other pre-existing data. </summary>
@ -402,7 +406,7 @@ public class VertexAttribute
var element = new MdlStructs.VertexElement() var element = new MdlStructs.VertexElement()
{ {
Stream = 1, Stream = 1,
Type = (byte)MdlFile.VertexType.ByteFloat4, Type = (byte)MdlFile.VertexType.NByte4,
Usage = (byte)MdlFile.VertexUsage.Color, Usage = (byte)MdlFile.VertexUsage.Color,
}; };
@ -411,10 +415,17 @@ public class VertexAttribute
return new VertexAttribute( return new VertexAttribute(
element, element,
index => BuildByteFloat4(values?[index] ?? Vector4.One) index => BuildNByte4(values?[index] ?? Vector4.One)
); );
} }
private static byte[] BuildSingle2(Vector2 input)
=>
[
..BitConverter.GetBytes(input.X),
..BitConverter.GetBytes(input.Y),
];
private static byte[] BuildSingle3(Vector3 input) private static byte[] BuildSingle3(Vector3 input)
=> =>
[ [
@ -423,7 +434,16 @@ public class VertexAttribute
..BitConverter.GetBytes(input.Z), ..BitConverter.GetBytes(input.Z),
]; ];
private static byte[] BuildUInt(Vector4 input) private static byte[] BuildSingle4(Vector4 input)
=>
[
..BitConverter.GetBytes(input.X),
..BitConverter.GetBytes(input.Y),
..BitConverter.GetBytes(input.Z),
..BitConverter.GetBytes(input.W),
];
private static byte[] BuildUByte4(Vector4 input)
=> =>
[ [
(byte)input.X, (byte)input.X,
@ -432,7 +452,7 @@ public class VertexAttribute
(byte)input.W, (byte)input.W,
]; ];
private static byte[] BuildByteFloat4(Vector4 input) private static byte[] BuildNByte4(Vector4 input)
=> =>
[ [
(byte)Math.Round(input.X * 255f), (byte)Math.Round(input.X * 255f),
@ -440,20 +460,4 @@ public class VertexAttribute
(byte)Math.Round(input.Z * 255f), (byte)Math.Round(input.Z * 255f),
(byte)Math.Round(input.W * 255f), (byte)Math.Round(input.W * 255f),
]; ];
private static byte[] BuildHalf2(Vector2 input)
=>
[
..BitConverter.GetBytes((Half)input.X),
..BitConverter.GetBytes((Half)input.Y),
];
private static byte[] BuildHalf4(Vector4 input)
=>
[
..BitConverter.GetBytes((Half)input.X),
..BitConverter.GetBytes((Half)input.Y),
..BitConverter.GetBytes((Half)input.Z),
..BitConverter.GetBytes((Half)input.W),
];
} }

View file

@ -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;