Import attributes

This commit is contained in:
ackwell 2024-01-11 18:54:53 +11:00
parent 182550ce15
commit dada03905f
4 changed files with 95 additions and 14 deletions

View file

@ -19,6 +19,8 @@ public class MeshImporter(IEnumerable<Node> nodes)
public List<string>? Bones; public List<string>? Bones;
public List<string> MetaAttributes;
public List<MeshShapeKey> ShapeKeys; public List<MeshShapeKey> ShapeKeys;
} }
@ -48,6 +50,8 @@ public class MeshImporter(IEnumerable<Node> nodes)
private List<string>? _bones; private List<string>? _bones;
private readonly List<string> _metaAttributes = [];
private readonly Dictionary<string, List<MdlStructs.ShapeValueStruct>> _shapeValues = []; private readonly Dictionary<string, List<MdlStructs.ShapeValueStruct>> _shapeValues = [];
private Mesh Create() private Mesh Create()
@ -83,6 +87,7 @@ public class MeshImporter(IEnumerable<Node> nodes)
VertexBuffer = _streams[0].Concat(_streams[1]).Concat(_streams[2]), VertexBuffer = _streams[0].Concat(_streams[1]).Concat(_streams[2]),
Indices = _indices, Indices = _indices,
Bones = _bones, Bones = _bones,
MetaAttributes = _metaAttributes,
ShapeKeys = _shapeValues ShapeKeys = _shapeValues
.Select(pair => new MeshShapeKey() .Select(pair => new MeshShapeKey()
{ {
@ -153,6 +158,8 @@ public class MeshImporter(IEnumerable<Node> nodes)
_subMeshes.Add(subMesh.SubMeshStruct with _subMeshes.Add(subMesh.SubMeshStruct with
{ {
IndexOffset = (ushort)(subMesh.SubMeshStruct.IndexOffset + indexOffset), IndexOffset = (ushort)(subMesh.SubMeshStruct.IndexOffset + indexOffset),
AttributeIndexMask = Utility.GetMergedAttributeMask(
subMesh.SubMeshStruct.AttributeIndexMask, subMesh.MetaAttributes, _metaAttributes),
}); });
} }

View file

@ -29,6 +29,8 @@ public partial class ModelImporter(ModelRoot _model)
private readonly List<string> _bones = []; private readonly List<string> _bones = [];
private readonly List<MdlStructs.BoneTableStruct> _boneTables = []; private readonly List<MdlStructs.BoneTableStruct> _boneTables = [];
private readonly List<string> _metaAttributes = [];
private readonly Dictionary<string, List<MdlStructs.ShapeMeshStruct>> _shapeMeshes = []; private readonly Dictionary<string, List<MdlStructs.ShapeMeshStruct>> _shapeMeshes = [];
private readonly List<MdlStructs.ShapeValueStruct> _shapeValues = []; private readonly List<MdlStructs.ShapeValueStruct> _shapeValues = [];
@ -71,6 +73,7 @@ public partial class ModelImporter(ModelRoot _model)
Bones = [.. _bones], Bones = [.. _bones],
// TODO: Game doesn't seem to rely on this, but would be good to populate. // TODO: Game doesn't seem to rely on this, but would be good to populate.
SubMeshBoneMap = [], SubMeshBoneMap = [],
Attributes = [.. _metaAttributes],
Shapes = [.. shapes], Shapes = [.. shapes],
ShapeMeshes = [.. shapeMeshes], ShapeMeshes = [.. shapeMeshes],
ShapeValues = [.. _shapeValues], ShapeValues = [.. _shapeValues],
@ -155,6 +158,8 @@ public partial class ModelImporter(ModelRoot _model)
_subMeshes.AddRange(mesh.SubMeshStructs.Select(m => m with _subMeshes.AddRange(mesh.SubMeshStructs.Select(m => m with
{ {
AttributeIndexMask = Utility.GetMergedAttributeMask(
m.AttributeIndexMask, mesh.MetaAttributes, _metaAttributes),
IndexOffset = (uint)(m.IndexOffset + indexOffset), IndexOffset = (uint)(m.IndexOffset + indexOffset),
})); }));

View file

@ -1,3 +1,4 @@
using System.Text.Json;
using Lumina.Data.Parsing; using Lumina.Data.Parsing;
using OtterGui; using OtterGui;
using SharpGLTF.Schema2; using SharpGLTF.Schema2;
@ -20,6 +21,8 @@ public class SubMeshImporter
public ushort[] Indices; public ushort[] Indices;
public string[] MetaAttributes;
public Dictionary<string, List<MdlStructs.ShapeValueStruct>> ShapeValues; public Dictionary<string, List<MdlStructs.ShapeValueStruct>> ShapeValues;
} }
@ -29,10 +32,11 @@ public class SubMeshImporter
return importer.Create(); return importer.Create();
} }
private readonly MeshPrimitive _primitive; private readonly MeshPrimitive _primitive;
private readonly IDictionary<ushort, ushort>? _nodeBoneMap; private readonly IDictionary<ushort, ushort>? _nodeBoneMap;
private readonly IDictionary<string, JsonElement>? _nodeExtras;
private List<VertexAttribute>? _attributes; private List<VertexAttribute>? _vertexAttributes;
private ushort _vertexCount; private ushort _vertexCount;
private byte[] _strides = [0, 0, 0]; private byte[] _strides = [0, 0, 0];
@ -40,6 +44,8 @@ public class SubMeshImporter
private ushort[]? _indices; private ushort[]? _indices;
private string[]? _metaAttributes;
private readonly List<string>? _morphNames; private readonly List<string>? _morphNames;
private Dictionary<string, List<MdlStructs.ShapeValueStruct>>? _shapeValues; private Dictionary<string, List<MdlStructs.ShapeValueStruct>>? _shapeValues;
@ -57,6 +63,15 @@ public class SubMeshImporter
_primitive = mesh.Primitives[0]; _primitive = mesh.Primitives[0];
_nodeBoneMap = nodeBoneMap; _nodeBoneMap = nodeBoneMap;
try
{
_nodeExtras = node.Extras.Deserialize<Dictionary<string, JsonElement>>();
}
catch
{
_nodeExtras = null;
}
try try
{ {
_morphNames = mesh.Extras.GetNode("targetNames").Deserialize<List<string>>(); _morphNames = mesh.Extras.GetNode("targetNames").Deserialize<List<string>>();
@ -76,24 +91,34 @@ public class SubMeshImporter
{ {
// Build all the data we'll need. // Build all the data we'll need.
BuildIndices(); BuildIndices();
BuildAttributes(); BuildVertexAttributes();
BuildVertices(); BuildVertices();
BuildMetaAttributes();
ArgumentNullException.ThrowIfNull(_indices); ArgumentNullException.ThrowIfNull(_indices);
ArgumentNullException.ThrowIfNull(_attributes); ArgumentNullException.ThrowIfNull(_vertexAttributes);
ArgumentNullException.ThrowIfNull(_shapeValues); ArgumentNullException.ThrowIfNull(_shapeValues);
ArgumentNullException.ThrowIfNull(_metaAttributes);
var material = _primitive.Material.Name; var material = _primitive.Material.Name;
if (material == "") if (material == "")
material = null; material = null;
// At this level, we assume that attributes are wholly controlled by this sub-mesh.
var attributeMask = _metaAttributes.Length switch
{
< 32 => (1u << _metaAttributes.Length) - 1,
32 => uint.MaxValue,
> 32 => throw new Exception("Models may utilise a maximum of 32 attributes."),
};
return new SubMesh() return new SubMesh()
{ {
SubMeshStruct = new MdlStructs.SubmeshStruct() SubMeshStruct = new MdlStructs.SubmeshStruct()
{ {
IndexOffset = 0, IndexOffset = 0,
IndexCount = (uint)_indices.Length, IndexCount = (uint)_indices.Length,
AttributeIndexMask = 0, 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,
@ -102,12 +127,13 @@ public class SubMeshImporter
Material = material, Material = material,
VertexDeclaration = new MdlStructs.VertexDeclarationStruct() VertexDeclaration = new MdlStructs.VertexDeclarationStruct()
{ {
VertexElements = _attributes.Select(attribute => attribute.Element).ToArray(), VertexElements = _vertexAttributes.Select(attribute => attribute.Element).ToArray(),
}, },
VertexCount = _vertexCount, VertexCount = _vertexCount,
Strides = _strides, Strides = _strides,
Streams = _streams, Streams = _streams,
Indices = _indices, Indices = _indices,
MetaAttributes = _metaAttributes,
ShapeValues = _shapeValues, ShapeValues = _shapeValues,
}; };
} }
@ -117,7 +143,7 @@ public class SubMeshImporter
_indices = _primitive.GetIndices().Select(idx => (ushort)idx).ToArray(); _indices = _primitive.GetIndices().Select(idx => (ushort)idx).ToArray();
} }
private void BuildAttributes() private void BuildVertexAttributes()
{ {
var accessors = _primitive.VertexAccessors; var accessors = _primitive.VertexAccessors;
@ -153,14 +179,14 @@ public class SubMeshImporter
offsets[attribute.Stream] += attribute.Size; offsets[attribute.Stream] += attribute.Size;
} }
_attributes = attributes; _vertexAttributes = attributes;
// After building the attributes, the resulting next offsets are our stream strides. // After building the attributes, the resulting next offsets are our stream strides.
_strides = offsets; _strides = offsets;
} }
private void BuildVertices() private void BuildVertices()
{ {
ArgumentNullException.ThrowIfNull(_attributes); ArgumentNullException.ThrowIfNull(_vertexAttributes);
// Lists of vertex indices that are effected by each morph target for this primitive. // Lists of vertex indices that are effected by each morph target for this primitive.
var morphModifiedVertices = Enumerable.Range(0, _primitive.MorphTargetsCount) var morphModifiedVertices = Enumerable.Range(0, _primitive.MorphTargetsCount)
@ -173,13 +199,13 @@ public class SubMeshImporter
for (var vertexIndex = 0; vertexIndex < _vertexCount; vertexIndex++) for (var vertexIndex = 0; vertexIndex < _vertexCount; vertexIndex++)
{ {
// Write out vertex data to streams for each attribute. // Write out vertex data to streams for each attribute.
foreach (var attribute in _attributes) foreach (var attribute in _vertexAttributes)
_streams[attribute.Stream].AddRange(attribute.Build(vertexIndex)); _streams[attribute.Stream].AddRange(attribute.Build(vertexIndex));
// Record which morph targets have values for this vertex, if any. // Record which morph targets have values for this vertex, if any.
var changedMorphs = morphModifiedVertices var changedMorphs = morphModifiedVertices
.WithIndex() .WithIndex()
.Where(pair => _attributes.Any(attribute => attribute.HasMorph(pair.Index, vertexIndex))) .Where(pair => _vertexAttributes.Any(attribute => attribute.HasMorph(pair.Index, vertexIndex)))
.Select(pair => pair.Value); .Select(pair => pair.Value);
foreach (var modifiedVertices in changedMorphs) foreach (var modifiedVertices in changedMorphs)
modifiedVertices.Add(vertexIndex); modifiedVertices.Add(vertexIndex);
@ -191,7 +217,7 @@ public class SubMeshImporter
private void BuildShapeValues(IEnumerable<List<int>> morphModifiedVertices) private void BuildShapeValues(IEnumerable<List<int>> morphModifiedVertices)
{ {
ArgumentNullException.ThrowIfNull(_indices); ArgumentNullException.ThrowIfNull(_indices);
ArgumentNullException.ThrowIfNull(_attributes); ArgumentNullException.ThrowIfNull(_vertexAttributes);
var morphShapeValues = new Dictionary<string, List<MdlStructs.ShapeValueStruct>>(); var morphShapeValues = new Dictionary<string, List<MdlStructs.ShapeValueStruct>>();
@ -203,7 +229,7 @@ public class SubMeshImporter
foreach (var vertexIndex in modifiedVertices) foreach (var vertexIndex in modifiedVertices)
{ {
// Write out the morphed vertex to the vertex streams. // Write out the morphed vertex to the vertex streams.
foreach (var attribute in _attributes) foreach (var attribute in _vertexAttributes)
_streams[attribute.Stream].AddRange(attribute.BuildMorph(morphIndex, vertexIndex)); _streams[attribute.Stream].AddRange(attribute.BuildMorph(morphIndex, vertexIndex));
// Find any indices that target this vertex index and create a mapping. // Find any indices that target this vertex index and create a mapping.
@ -224,4 +250,13 @@ public class SubMeshImporter
_shapeValues = morphShapeValues; _shapeValues = morphShapeValues;
} }
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() ?? [];
}
} }

View file

@ -0,0 +1,34 @@
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>
/// <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>
public static uint GetMergedAttributeMask(uint oldMask, IList<string> oldAttributes, List<string> newAttributes)
{
var metaAttributes = Enumerable.Range(0, 32)
.Where(index => ((oldMask >> index) & 1) == 1)
.Select(index => oldAttributes[index]);
var newMask = 0u;
foreach (var metaAttribute in metaAttributes)
{
var attributeIndex = newAttributes.IndexOf(metaAttribute);
if (attributeIndex == -1)
{
if (newAttributes.Count >= 32)
throw new Exception("Models may utilise a maximum of 32 attributes.");
newAttributes.Add(metaAttribute);
attributeIndex = newAttributes.Count - 1;
}
newMask |= 1u << attributeIndex;
}
return newMask;
}
}