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.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()),
MdlFile.VertexType.UInt => reader.ReadBytes(4),
MdlFile.VertexType.ByteFloat4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f,
MdlFile.VertexType.UByte4 => reader.ReadBytes(4),
MdlFile.VertexType.NByte4 => new Vector4(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.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;
/// <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
{
private Vector3 _minimum = Vector3.Zero;
private Vector3 _maximum = Vector3.Zero;
private Vector3 _minimum = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
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>
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>
public MdlStructs.BoundingBoxStruct ToStruct()
=> new MdlStructs.BoundingBoxStruct
=> new()
{
Min = [_minimum.X, _minimum.Y, _minimum.Z, 1],
Max = [_maximum.X, _maximum.Y, _maximum.Z, 1],

View file

@ -1,4 +1,5 @@
using Lumina.Data.Parsing;
using OtterGui;
using SharpGLTF.Schema2;
namespace Penumbra.Import.Models.Import;
@ -52,7 +53,7 @@ public class MeshImporter(IEnumerable<Node> nodes, IoNotifier notifier)
private List<string>? _bones;
private readonly BoundingBox _boundingBox = new BoundingBox();
private readonly BoundingBox _boundingBox = new();
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 nodeBoneMap = CreateNodeBoneMap(node);
var subMesh = SubMeshImporter.Import(node, nodeBoneMap, notifier.WithContext($"Sub-mesh {subMeshName}"));
var subNotifier = 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;
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.
if (_vertexDeclaration == null)
_vertexDeclaration = subMesh.VertexDeclaration;
else if (VertexDeclarationMismatch(subMesh.VertexDeclaration, _vertexDeclaration.Value))
throw notifier.Exception(
$@"All sub-meshes of a mesh must have equivalent vertex declarations.
Current: {FormatVertexDeclaration(_vertexDeclaration.Value)}
Sub-mesh ""{subMeshName}"": {FormatVertexDeclaration(subMesh.VertexDeclaration)}"
);
else
Utility.EnsureVertexDeclarationMatch(_vertexDeclaration.Value, subMesh.VertexDeclaration, notifier);
// 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.
@ -173,27 +173,7 @@ public class MeshImporter(IEnumerable<Node> nodes, IoNotifier notifier)
});
}
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
);
}
private Dictionary<ushort, ushort>? CreateNodeBoneMap(Node node)
private Dictionary<ushort, ushort>? CreateNodeBoneMap(Node node, IoNotifier notifier)
{
// Unskinned assets can skip this all of this.
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")
.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>();
foreach (var joints in jointsAccessor.AsVector4Array())
foreach (var (primitive, primitiveIndex) in node.Mesh.Primitives.WithIndex())
{
for (var index = 0; index < 4; index++)
usedJoints.Add((ushort)joints[index]);
// Per glTF specification, an asset with a skin MUST contain skinning attributes on its meshes.
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.
_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>();
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 List<byte>[] Streams;
public ushort[] Indices;
public List<ushort> Indices;
public BoundingBox BoundingBox;
@ -36,86 +36,57 @@ public class SubMeshImporter
private readonly IoNotifier _notifier;
private readonly MeshPrimitive _primitive;
private readonly IDictionary<ushort, ushort>? _nodeBoneMap;
private readonly IDictionary<string, JsonElement>? _nodeExtras;
private readonly Node _node;
private readonly IDictionary<ushort, ushort>? _nodeBoneMap;
private List<VertexAttribute>? _vertexAttributes;
private string? _material;
private ushort _vertexCount;
private byte[] _strides = [0, 0, 0];
private readonly List<byte>[] _streams;
private MdlStructs.VertexDeclarationStruct? _vertexDeclaration;
private ushort _vertexCount;
private byte[]? _strides;
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 Dictionary<string, List<MdlStructs.ShapeValueStruct>>? _shapeValues;
private readonly List<string>? _morphNames;
private readonly Dictionary<string, List<MdlStructs.ShapeValueStruct>> _shapeValues = [];
private SubMeshImporter(Node node, IDictionary<ushort, ushort>? nodeBoneMap, IoNotifier notifier)
{
_notifier = notifier;
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];
_notifier = notifier;
_node = node;
_nodeBoneMap = nodeBoneMap;
try
{
_nodeExtras = node.Extras.Deserialize<Dictionary<string, JsonElement>>();
}
catch
{
_nodeExtras = null;
}
try
{
_morphNames = mesh.Extras.GetNode("targetNames").Deserialize<List<string>>();
_morphNames = node.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 streamIndex = 0; streamIndex < 3; streamIndex++)
_streams[streamIndex] = [];
}
private SubMesh Create()
{
// Build all the data we'll need.
// TODO: This structure is verging on a little silly. Reconsider.
BuildIndices();
BuildVertexAttributes();
BuildVertices();
BuildBoundingBox();
BuildMetaAttributes();
foreach (var (primitive, index) in _node.Mesh.Primitives.WithIndex())
BuildPrimitive(primitive, index);
ArgumentNullException.ThrowIfNull(_indices);
ArgumentNullException.ThrowIfNull(_vertexAttributes);
ArgumentNullException.ThrowIfNull(_vertexDeclaration);
ArgumentNullException.ThrowIfNull(_strides);
ArgumentNullException.ThrowIfNull(_shapeValues);
ArgumentNullException.ThrowIfNull(_metaAttributes);
var material = _primitive.Material.Name;
if (material == "")
material = null;
var metaAttributes = BuildMetaAttributes();
// 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 => uint.MaxValue,
< 32 => (1u << metaAttributes.Length) - 1,
32 => uint.MaxValue,
> 32 => throw _notifier.Exception("Models may utilise a maximum of 32 attributes."),
};
@ -124,158 +95,95 @@ public class SubMeshImporter
SubMeshStruct = new MdlStructs.SubmeshStruct()
{
IndexOffset = 0,
IndexCount = (uint)_indices.Length,
IndexCount = (uint)_indices.Count,
AttributeIndexMask = attributeMask,
// TODO: Flesh these out. Game doesn't seem to rely on them existing, though.
BoneStartIndex = 0,
BoneCount = 0,
},
Material = material,
VertexDeclaration = new MdlStructs.VertexDeclarationStruct()
Material = _material,
VertexDeclaration = _vertexDeclaration.Value,
VertexCount = _vertexCount,
Strides = _strides,
Streams = _streams,
Indices = _indices,
BoundingBox = _boundingBox,
MetaAttributes = metaAttributes,
ShapeValues = _shapeValues,
};
}
private void BuildPrimitive(MeshPrimitive meshPrimitive, int index)
{
var vertexOffset = _vertexCount;
var indexOffset = _indices.Count;
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())
{
// Per glTF spec, all primitives MUST have the same number of morph targets in the same order.
// As such, this lookup should be safe - a failure here is a broken glTF file.
var name = _morphNames != null ? _morphNames[morphIndex] : $"unnamed_shape_{morphIndex}";
if (!_shapeValues.TryGetValue(name, out var subMeshShapeValues))
{
VertexElements = _vertexAttributes.Select(attribute => attribute.Element).ToArray(),
},
VertexCount = _vertexCount,
Strides = _strides,
Streams = _streams,
Indices = _indices,
BoundingBox = _boundingBox,
MetaAttributes = _metaAttributes,
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 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++;
subMeshShapeValues = [];
_shapeValues.Add(name, subMeshShapeValues);
}
var name = _morphNames != null ? _morphNames[morphIndex] : $"unnamed_shape_{morphIndex}";
morphShapeValues.Add(name, shapeValues);
subMeshShapeValues.AddRange(primitiveShapeValues.Select(value => value with
{
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();
foreach (var position in positions)
_boundingBox.Merge(position);
}
Dictionary<string, JsonElement>? nodeExtras;
try
{
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.
_metaAttributes = _nodeExtras?
.Where(pair => pair.Value.ValueKind == JsonValueKind.True)
.Select(pair => pair.Key)
.ToArray() ?? [];
return nodeExtras?
.Where(pair => pair.Value.ValueKind == JsonValueKind.True)
.Select(pair => pair.Key)
.ToArray()
?? [];
}
}

View file

@ -1,12 +1,14 @@
using Lumina.Data.Parsing;
namespace Penumbra.Import.Models.Import;
public static class Utility
{
/// <summary> Merge attributes into an existing attribute array, providing an updated submesh mask. </summary>
/// <param name="oldMask"> Old submesh attribute mask. </param>
/// <summary> Merge attributes into an existing attribute array, providing an updated sub mesh mask. </summary>
/// <param name="oldMask"> Old sub mesh attribute mask. </param>
/// <param name="oldAttributes"> Old attribute array that should be merged. </param>
/// <param name="newAttributes"> New attribute array. Will be mutated. </param>
/// <returns> New submesh attribute mask, updated to match the merged attribute array. </returns>
/// <returns> New sub mesh attribute mask, updated to match the merged attribute array. </returns>
public static uint GetMergedAttributeMask(uint oldMask, IList<string> oldAttributes, List<string> newAttributes)
{
var metaAttributes = Enumerable.Range(0, 32)
@ -26,9 +28,45 @@ public static class Utility
newAttributes.Add(metaAttribute);
attributeIndex = newAttributes.Count - 1;
}
newMask |= 1u << attributeIndex;
}
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,12 +30,16 @@ public class VertexAttribute
public byte Size
=> (MdlFile.VertexType)Element.Type switch
{
MdlFile.VertexType.Single3 => 12,
MdlFile.VertexType.Single4 => 16,
MdlFile.VertexType.UInt => 4,
MdlFile.VertexType.ByteFloat4 => 4,
MdlFile.VertexType.Half2 => 4,
MdlFile.VertexType.Half4 => 8,
MdlFile.VertexType.Single1 => 4,
MdlFile.VertexType.Single2 => 8,
MdlFile.VertexType.Single3 => 12,
MdlFile.VertexType.Single4 => 16,
MdlFile.VertexType.UByte4 => 4,
MdlFile.VertexType.Short2 => 4,
MdlFile.VertexType.Short4 => 8,
MdlFile.VertexType.NByte4 => 4,
MdlFile.VertexType.Half2 => 4,
MdlFile.VertexType.Half4 => 8,
_ => throw new Exception($"Unhandled vertex type {(MdlFile.VertexType)Element.Type}"),
};
@ -126,7 +130,7 @@ public class VertexAttribute
var element = new MdlStructs.VertexElement()
{
Stream = 0,
Type = (byte)MdlFile.VertexType.ByteFloat4,
Type = (byte)MdlFile.VertexType.NByte4,
Usage = (byte)MdlFile.VertexUsage.BlendWeights,
};
@ -134,7 +138,7 @@ public class VertexAttribute
return new VertexAttribute(
element,
index => BuildByteFloat4(values[index])
index => BuildNByte4(values[index])
);
}
@ -152,7 +156,7 @@ public class VertexAttribute
var element = new MdlStructs.VertexElement()
{
Stream = 0,
Type = (byte)MdlFile.VertexType.UInt,
Type = (byte)MdlFile.VertexType.UByte4,
Usage = (byte)MdlFile.VertexUsage.BlendIndices,
};
@ -163,7 +167,7 @@ public class VertexAttribute
index =>
{
var gltfIndices = values[index];
return BuildUInt(new Vector4(
return BuildUByte4(new Vector4(
boneMap[(ushort)gltfIndices.X],
boneMap[(ushort)gltfIndices.Y],
boneMap[(ushort)gltfIndices.Z],
@ -181,7 +185,7 @@ public class VertexAttribute
var element = new MdlStructs.VertexElement()
{
Stream = 1,
Type = (byte)MdlFile.VertexType.Half4,
Type = (byte)MdlFile.VertexType.Single3,
Usage = (byte)MdlFile.VertexUsage.Normal,
};
@ -193,7 +197,7 @@ public class VertexAttribute
return new VertexAttribute(
element,
index => BuildHalf4(new Vector4(values[index], 0)),
index => BuildSingle3(values[index]),
buildMorph: (morphIndex, vertexIndex) =>
{
var value = values[vertexIndex];
@ -202,7 +206,7 @@ public class VertexAttribute
if (delta != null)
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.
if (!accessors.TryGetValue("TEXCOORD_1", out var accessor2))
return new VertexAttribute(
element with { Type = (byte)MdlFile.VertexType.Half2 },
index => BuildHalf2(values1[index])
element with { Type = (byte)MdlFile.VertexType.Single2 },
index => BuildSingle2(values1[index])
);
var values2 = accessor2.AsVector2Array();
// Two TEXCOORDs are available, repack them into xiv's vec4 [0X, 0Y, 1X, 1Y] format.
return new VertexAttribute(
element with { Type = (byte)MdlFile.VertexType.Half4 },
element with { Type = (byte)MdlFile.VertexType.Single4 },
index =>
{
var value1 = values1[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
{
Stream = 1,
Type = (byte)MdlFile.VertexType.ByteFloat4,
Type = (byte)MdlFile.VertexType.NByte4,
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.
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>
@ -402,7 +406,7 @@ public class VertexAttribute
var element = new MdlStructs.VertexElement()
{
Stream = 1,
Type = (byte)MdlFile.VertexType.ByteFloat4,
Type = (byte)MdlFile.VertexType.NByte4,
Usage = (byte)MdlFile.VertexUsage.Color,
};
@ -411,10 +415,17 @@ public class VertexAttribute
return new VertexAttribute(
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)
=>
[
@ -423,7 +434,16 @@ public class VertexAttribute
..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,
@ -432,7 +452,7 @@ public class VertexAttribute
(byte)input.W,
];
private static byte[] BuildByteFloat4(Vector4 input)
private static byte[] BuildNByte4(Vector4 input)
=>
[
(byte)Math.Round(input.X * 255f),
@ -440,20 +460,4 @@ public class VertexAttribute
(byte)Math.Round(input.Z * 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 Penumbra.GameData;
using Penumbra.GameData.Files;
using Penumbra.Import.Models;
using Penumbra.Import.Models.Export;
using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes;