From 3cd438bb5dc495e257a9f8731293010b8ea58ad6 Mon Sep 17 00:00:00 2001 From: ackwell Date: Wed, 10 Jan 2024 01:17:47 +1100 Subject: [PATCH 1/8] Export material names --- Penumbra/Import/Models/Export/MeshExporter.cs | 18 +++++++------- .../Import/Models/Export/ModelExporter.cs | 24 +++++++++++++++---- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 84628c2c..6e6169ee 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -27,9 +27,9 @@ public class MeshExporter } } - public static Mesh Export(MdlFile mdl, byte lod, ushort meshIndex, GltfSkeleton? skeleton) + public static Mesh Export(MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, GltfSkeleton? skeleton) { - var self = new MeshExporter(mdl, lod, meshIndex, skeleton?.Names); + var self = new MeshExporter(mdl, lod, meshIndex, materials, skeleton?.Names); return new Mesh(self.BuildMeshes(), skeleton?.Joints); } @@ -42,18 +42,22 @@ public class MeshExporter private MdlStructs.MeshStruct XivMesh => _mdl.Meshes[_meshIndex]; + private readonly MaterialBuilder _material; + private readonly Dictionary? _boneIndexMap; private readonly Type _geometryType; private readonly Type _materialType; private readonly Type _skinningType; - private MeshExporter(MdlFile mdl, byte lod, ushort meshIndex, IReadOnlyDictionary? boneNameMap) + private MeshExporter(MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, IReadOnlyDictionary? boneNameMap) { _mdl = mdl; _lod = lod; _meshIndex = meshIndex; + _material = materials[XivMesh.MaterialIndex]; + if (boneNameMap != null) _boneIndexMap = BuildBoneIndexMap(boneNameMap); @@ -134,13 +138,7 @@ public class MeshExporter ); var meshBuilder = (IMeshBuilder)Activator.CreateInstance(meshBuilderType, name)!; - // TODO: share materials &c - var materialBuilder = new MaterialBuilder() - .WithDoubleSide(true) - .WithMetallicRoughnessShader() - .WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, new Vector4(1, 1, 1, 1)); - - var primitiveBuilder = meshBuilder.UsePrimitive(materialBuilder); + var primitiveBuilder = meshBuilder.UsePrimitive(_material); // Store a list of the glTF indices. The list index will be equivalent to the xiv (submesh) index. var gltfIndices = new List(); diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs index 8271f266..6a25af61 100644 --- a/Penumbra/Import/Models/Export/ModelExporter.cs +++ b/Penumbra/Import/Models/Export/ModelExporter.cs @@ -1,4 +1,5 @@ using Penumbra.GameData.Files; +using SharpGLTF.Materials; using SharpGLTF.Scenes; using SharpGLTF.Transforms; @@ -14,7 +15,7 @@ public class ModelExporter var skeletonRoot = skeleton?.Root; if (skeletonRoot != null) scene.AddNode(skeletonRoot); - + // Add all the meshes to the scene. foreach (var mesh in meshes) mesh.AddToScene(scene); @@ -25,12 +26,13 @@ public class ModelExporter public static Model Export(MdlFile mdl, IEnumerable? xivSkeleton) { var gltfSkeleton = xivSkeleton != null ? ConvertSkeleton(xivSkeleton) : null; - var meshes = ConvertMeshes(mdl, gltfSkeleton); + var materials = ConvertMaterials(mdl); + var meshes = ConvertMeshes(mdl, materials, gltfSkeleton); return new Model(meshes, gltfSkeleton); } /// Convert a .mdl to a mesh (group) per LoD. - private static List ConvertMeshes(MdlFile mdl, GltfSkeleton? skeleton) + private static List ConvertMeshes(MdlFile mdl, MaterialBuilder[] materials, GltfSkeleton? skeleton) { var meshes = new List(); @@ -41,7 +43,7 @@ public class ModelExporter // TODO: consider other types of mesh? for (ushort meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++) { - var mesh = MeshExporter.Export(mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset), skeleton); + var mesh = MeshExporter.Export(mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset), materials, skeleton); meshes.Add(mesh); } } @@ -49,6 +51,18 @@ public class ModelExporter return meshes; } + // TODO: Compose textures for use with these materials + /// Build placeholder materials for each of the material slots in the .mdl. + private static MaterialBuilder[] ConvertMaterials(MdlFile mdl) + => mdl.Materials + .Select(name => + new MaterialBuilder(name) + .WithMetallicRoughnessShader() + .WithDoubleSide(true) + .WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, Vector4.One) + ) + .ToArray(); + /// Convert XIV skeleton data into a glTF-compatible node tree, with mappings. private static GltfSkeleton? ConvertSkeleton(IEnumerable skeletons) { @@ -60,7 +74,7 @@ public class ModelExporter var iterator = skeletons.SelectMany(skeleton => skeleton.Bones.Select(bone => (skeleton, bone))); foreach (var (skeleton, bone) in iterator) { - if (names.ContainsKey(bone.Name)) + if (names.ContainsKey(bone.Name)) continue; var node = new NodeBuilder(bone.Name); From d2f93f85625d789189938ad4b0200b759779e722 Mon Sep 17 00:00:00 2001 From: ackwell Date: Wed, 10 Jan 2024 20:33:24 +1100 Subject: [PATCH 2/8] Import material names --- Penumbra/Import/Models/Import/MeshImporter.cs | 8 +++++ .../Import/Models/Import/ModelImporter.cs | 33 ++++++++++++++++--- .../Import/Models/Import/SubMeshImporter.cs | 7 ++++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index 95eede2b..00663e43 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -10,6 +10,8 @@ public class MeshImporter(IEnumerable nodes) public MdlStructs.MeshStruct MeshStruct; public List SubMeshStructs; + public string? Material; + public MdlStructs.VertexDeclarationStruct VertexDeclaration; public IEnumerable VertexBuffer; @@ -35,6 +37,8 @@ public class MeshImporter(IEnumerable nodes) private readonly List _subMeshes = []; + private string? _material; + private MdlStructs.VertexDeclarationStruct? _vertexDeclaration; private byte[]? _strides; private ushort _vertexCount; @@ -74,6 +78,7 @@ public class MeshImporter(IEnumerable nodes) BoneTableIndex = 0, }, SubMeshStructs = _subMeshes, + Material = _material, VertexDeclaration = _vertexDeclaration.Value, VertexBuffer = _streams[0].Concat(_streams[1]).Concat(_streams[2]), Indices = _indices, @@ -105,6 +110,9 @@ public class MeshImporter(IEnumerable nodes) var subMeshName = node.Name ?? node.Mesh.Name; + // 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; + // 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; diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs index abe87934..d5d4bb53 100644 --- a/Penumbra/Import/Models/Import/ModelImporter.cs +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -19,6 +19,8 @@ public partial class ModelImporter(ModelRoot _model) private readonly List _meshes = []; private readonly List _subMeshes = []; + private readonly List _materials = []; + private readonly List _vertexDeclarations = []; private readonly List _vertexBuffer = []; @@ -37,6 +39,8 @@ public partial class ModelImporter(ModelRoot _model) BuildMeshForGroup(subMeshNodes); // Now that all the meshes have been built, we can build some of the model-wide metadata. + var materials = _materials.Count > 0 ? _materials : ["/NO_MATERIAL"]; + var shapes = new List(); var shapeMeshes = new List(); foreach (var (keyName, keyMeshes) in _shapeMeshes) @@ -86,8 +90,7 @@ public partial class ModelImporter(ModelRoot _model) }, ], - // TODO: Would be good to populate from gltf material names. - Materials = ["/NO_MATERIAL"], + Materials = [.. materials], // TODO: Would be good to calculate all of this up the tree. Radius = 1, @@ -130,15 +133,20 @@ public partial class ModelImporter(ModelRoot _model) var mesh = MeshImporter.Import(subMeshNodes); var meshStartIndex = (uint)(mesh.MeshStruct.StartIndex + indexOffset); + ushort materialIndex = 0; + if (mesh.Material != null) + materialIndex = GetMaterialIndex(mesh.Material); + // If no bone table is used for a mesh, the index is set to 255. - var boneTableIndex = 255; + ushort boneTableIndex = 255; if (mesh.Bones != null) boneTableIndex = BuildBoneTable(mesh.Bones); _meshes.Add(mesh.MeshStruct with { + MaterialIndex = materialIndex, SubMeshIndex = (ushort)(mesh.MeshStruct.SubMeshIndex + subMeshOffset), - BoneTableIndex = (ushort)boneTableIndex, + BoneTableIndex = boneTableIndex, StartIndex = meshStartIndex, VertexBufferOffset = mesh.MeshStruct.VertexBufferOffset .Select(offset => (uint)(offset + vertexOffset)) @@ -173,6 +181,23 @@ public partial class ModelImporter(ModelRoot _model) } } + private ushort GetMaterialIndex(string materialName) + { + // If we already have this material, grab the current one + var index = _materials.IndexOf(materialName); + if (index >= 0) + return (ushort)index; + + // If there's already 4 materials, we can't add any more. + // TODO: permit, with a warning to reduce, and validation in MdlTab. + var count = _materials.Count; + if (count >= 4) + return 0; + + _materials.Add(materialName); + return (ushort)count; + } + private ushort BuildBoneTable(List boneNames) { var boneIndices = new List(); diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs index 5dec4384..0d1dafb3 100644 --- a/Penumbra/Import/Models/Import/SubMeshImporter.cs +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -10,6 +10,8 @@ public class SubMeshImporter { public MdlStructs.SubmeshStruct SubMeshStruct; + public string? Material; + public MdlStructs.VertexDeclarationStruct VertexDeclaration; public ushort VertexCount; @@ -81,6 +83,10 @@ public class SubMeshImporter ArgumentNullException.ThrowIfNull(_attributes); ArgumentNullException.ThrowIfNull(_shapeValues); + var material = _primitive.Material.Name; + if (material == "") + material = null; + return new SubMesh() { SubMeshStruct = new MdlStructs.SubmeshStruct() @@ -93,6 +99,7 @@ public class SubMeshImporter BoneStartIndex = 0, BoneCount = 0, }, + Material = material, VertexDeclaration = new MdlStructs.VertexDeclarationStruct() { VertexElements = _attributes.Select(attribute => attribute.Element).ToArray(), From 64aed56f7c2949d9791354299deebd45b8d1cf27 Mon Sep 17 00:00:00 2001 From: ackwell Date: Wed, 10 Jan 2024 22:39:48 +1100 Subject: [PATCH 3/8] Allow keeping existing mdl materials --- .../ModEditWindow.Models.MdlTab.cs | 34 ++++++++++++++++--- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 3 +- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index f9e19599..bd599133 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -15,6 +15,8 @@ public partial class ModEditWindow public MdlFile Mdl { get; private set; } private List[] _attributes; + public bool ImportKeepMaterials; + public List? GamePaths { get; private set; } public int GamePathIndex; @@ -110,6 +112,7 @@ public partial class ModEditWindow /// Export model to an interchange format. /// Disk path to save the resulting file to. + /// .mdl game path to resolve satellite files such as skeletons relative to. public void Export(string outputPath, Utf8GamePath mdlPath) { IEnumerable skeletons; @@ -143,14 +146,37 @@ public partial class ModEditWindow { IoException = task.Exception?.ToString(); if (task is { IsCompletedSuccessfully: true, Result: not null }) - { - Initialize(task.Result); - _dirty = true; - } + FinalizeImport(task.Result); PendingIo = false; }); } + /// Finalise the import of a .mdl, applying any post-import transformations and state updates. + /// Model data to finalize. + private void FinalizeImport(MdlFile newMdl) + { + if (ImportKeepMaterials) + MergeMaterials(newMdl, Mdl); + + Initialize(newMdl); + _dirty = true; + } + + /// Merge material configuration from the source onto the target. + /// Model that will be updated. + /// Model to copy material configuration from. + public void MergeMaterials(MdlFile target, MdlFile source) + { + target.Materials = source.Materials; + + for (var meshIndex = 0; meshIndex < target.Meshes.Length; meshIndex++) + { + target.Meshes[meshIndex].MaterialIndex = meshIndex < source.Meshes.Length + ? source.Meshes[meshIndex].MaterialIndex + : (ushort)0; + } + } + /// Read a .sklb from the active collection or game. /// Game path to the .sklb to load. private SklbFile ReadSklb(string sklbPath) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 4f9303f8..9a69a4e8 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -79,6 +79,8 @@ public partial class ModEditWindow using (var frame = ImRaii.FramedGroup("Import", size, headerPreIcon: FontAwesomeIcon.FileImport)) { + ImGui.Checkbox("Keep current materials", ref tab.ImportKeepMaterials); + if (ImGuiUtil.DrawDisabledButton("Import from glTF", Vector2.Zero, "Imports a glTF file, overriding the content of this mdl.", tab.PendingIo)) _fileDialog.OpenFilePicker("Load model from glTF.", "glTF{.gltf,.glb}", (success, paths) => @@ -86,7 +88,6 @@ public partial class ModEditWindow if (success && paths.Count > 0) tab.Import(paths[0]); }, 1, _mod!.ModPath.FullName, false); - ImGui.Dummy(new Vector2(ImGui.GetFrameHeight())); } if (_dragDropManager.CreateImGuiTarget("ModelDragDrop", out var files, out _) && GetFirstModel(files, out var importFile)) From 182550ce1538bead0f263f6b2a0c858629626e06 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 11 Jan 2024 00:34:18 +1100 Subject: [PATCH 4/8] Export attributes --- Penumbra/Import/Models/Export/MeshExporter.cs | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 6e6169ee..8127f348 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -13,20 +13,31 @@ namespace Penumbra.Import.Models.Export; public class MeshExporter { - public class Mesh(IEnumerable> meshes, NodeBuilder[]? joints) + public class Mesh(IEnumerable meshes, NodeBuilder[]? joints) { public void AddToScene(SceneBuilder scene) { - foreach (var mesh in meshes) + foreach (var data in meshes) { - if (joints == null) - scene.AddRigidMesh(mesh, Matrix4x4.Identity); - else - scene.AddSkinnedMesh(mesh, Matrix4x4.Identity, joints); + var instance = joints != null + ? scene.AddSkinnedMesh(data.Mesh, Matrix4x4.Identity, joints) + : scene.AddRigidMesh(data.Mesh, Matrix4x4.Identity); + + var extras = new Dictionary(); + foreach (var attribute in data.Attributes) + extras.Add(attribute, true); + + instance.WithExtras(JsonContent.CreateFrom(extras)); } } } + public struct MeshData + { + public IMeshBuilder Mesh; + public string[] Attributes; + } + public static Mesh Export(MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, GltfSkeleton? skeleton) { var self = new MeshExporter(mdl, lod, meshIndex, materials, skeleton?.Names); @@ -103,31 +114,33 @@ public class MeshExporter } /// Build glTF meshes for this XIV mesh. - private IMeshBuilder[] BuildMeshes() + private MeshData[] BuildMeshes() { var indices = BuildIndices(); var vertices = BuildVertices(); // NOTE: Index indices are specified relative to the LOD's 0, but we're reading chunks for each mesh, so we're specifying the index base relative to the mesh's base. if (XivMesh.SubMeshCount == 0) - return [BuildMesh($"mesh {_meshIndex}", indices, vertices, 0, (int)XivMesh.IndexCount)]; + return [BuildMesh($"mesh {_meshIndex}", indices, vertices, 0, (int)XivMesh.IndexCount, 0)]; return _mdl.SubMeshes .Skip(XivMesh.SubMeshIndex) .Take(XivMesh.SubMeshCount) .WithIndex() .Select(subMesh => BuildMesh($"mesh {_meshIndex}.{subMesh.Index}", indices, vertices, - (int)(subMesh.Value.IndexOffset - XivMesh.StartIndex), (int)subMesh.Value.IndexCount)) + (int)(subMesh.Value.IndexOffset - XivMesh.StartIndex), (int)subMesh.Value.IndexCount, + subMesh.Value.AttributeIndexMask)) .ToArray(); } /// Build a mesh from the provided indices and vertices. A subset of the full indices may be built by providing an index base and count. - private IMeshBuilder BuildMesh( + private MeshData BuildMesh( string name, IReadOnlyList indices, IReadOnlyList vertices, int indexBase, - int indexCount + int indexCount, + uint attributeMask ) { var meshBuilderType = typeof(MeshBuilder<,,,>).MakeGenericType( @@ -190,12 +203,23 @@ public class MeshExporter } } + // Named morph targets aren't part of the specification, however `MESH.extras.targetNames` + // is a commonly-accepted means of providing the data. meshBuilder.Extras = JsonContent.CreateFrom(new Dictionary() { { "targetNames", shapeNames }, }); - return meshBuilder; + var attributes = Enumerable.Range(0, 32) + .Where(index => ((attributeMask >> index) & 1) == 1) + .Select(index => _mdl.Attributes[index]) + .ToArray(); + + return new MeshData + { + Mesh = meshBuilder, + Attributes = attributes, + }; } /// Read in the indices for this mesh. From dada03905f3191004a4769eb2349bfff4524b497 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 11 Jan 2024 18:54:53 +1100 Subject: [PATCH 5/8] Import attributes --- Penumbra/Import/Models/Import/MeshImporter.cs | 7 +++ .../Import/Models/Import/ModelImporter.cs | 5 ++ .../Import/Models/Import/SubMeshImporter.cs | 63 ++++++++++++++----- Penumbra/Import/Models/Import/Utility.cs | 34 ++++++++++ 4 files changed, 95 insertions(+), 14 deletions(-) create mode 100644 Penumbra/Import/Models/Import/Utility.cs diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index 00663e43..7da4d1d7 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -19,6 +19,8 @@ public class MeshImporter(IEnumerable nodes) public List? Bones; + public List MetaAttributes; + public List ShapeKeys; } @@ -48,6 +50,8 @@ public class MeshImporter(IEnumerable nodes) private List? _bones; + private readonly List _metaAttributes = []; + private readonly Dictionary> _shapeValues = []; private Mesh Create() @@ -83,6 +87,7 @@ public class MeshImporter(IEnumerable nodes) VertexBuffer = _streams[0].Concat(_streams[1]).Concat(_streams[2]), Indices = _indices, Bones = _bones, + MetaAttributes = _metaAttributes, ShapeKeys = _shapeValues .Select(pair => new MeshShapeKey() { @@ -153,6 +158,8 @@ public class MeshImporter(IEnumerable nodes) _subMeshes.Add(subMesh.SubMeshStruct with { IndexOffset = (ushort)(subMesh.SubMeshStruct.IndexOffset + indexOffset), + AttributeIndexMask = Utility.GetMergedAttributeMask( + subMesh.SubMeshStruct.AttributeIndexMask, subMesh.MetaAttributes, _metaAttributes), }); } diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs index d5d4bb53..d02d143c 100644 --- a/Penumbra/Import/Models/Import/ModelImporter.cs +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -29,6 +29,8 @@ public partial class ModelImporter(ModelRoot _model) private readonly List _bones = []; private readonly List _boneTables = []; + private readonly List _metaAttributes = []; + private readonly Dictionary> _shapeMeshes = []; private readonly List _shapeValues = []; @@ -71,6 +73,7 @@ public partial class ModelImporter(ModelRoot _model) Bones = [.. _bones], // TODO: Game doesn't seem to rely on this, but would be good to populate. SubMeshBoneMap = [], + Attributes = [.. _metaAttributes], Shapes = [.. shapes], ShapeMeshes = [.. shapeMeshes], ShapeValues = [.. _shapeValues], @@ -155,6 +158,8 @@ public partial class ModelImporter(ModelRoot _model) _subMeshes.AddRange(mesh.SubMeshStructs.Select(m => m with { + AttributeIndexMask = Utility.GetMergedAttributeMask( + m.AttributeIndexMask, mesh.MetaAttributes, _metaAttributes), IndexOffset = (uint)(m.IndexOffset + indexOffset), })); diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs index 0d1dafb3..6b12ee09 100644 --- a/Penumbra/Import/Models/Import/SubMeshImporter.cs +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using Lumina.Data.Parsing; using OtterGui; using SharpGLTF.Schema2; @@ -20,6 +21,8 @@ public class SubMeshImporter public ushort[] Indices; + public string[] MetaAttributes; + public Dictionary> ShapeValues; } @@ -29,10 +32,11 @@ public class SubMeshImporter return importer.Create(); } - private readonly MeshPrimitive _primitive; - private readonly IDictionary? _nodeBoneMap; + private readonly MeshPrimitive _primitive; + private readonly IDictionary? _nodeBoneMap; + private readonly IDictionary? _nodeExtras; - private List? _attributes; + private List? _vertexAttributes; private ushort _vertexCount; private byte[] _strides = [0, 0, 0]; @@ -40,6 +44,8 @@ public class SubMeshImporter private ushort[]? _indices; + private string[]? _metaAttributes; + private readonly List? _morphNames; private Dictionary>? _shapeValues; @@ -57,6 +63,15 @@ public class SubMeshImporter _primitive = mesh.Primitives[0]; _nodeBoneMap = nodeBoneMap; + try + { + _nodeExtras = node.Extras.Deserialize>(); + } + catch + { + _nodeExtras = null; + } + try { _morphNames = mesh.Extras.GetNode("targetNames").Deserialize>(); @@ -76,24 +91,34 @@ public class SubMeshImporter { // Build all the data we'll need. BuildIndices(); - BuildAttributes(); + BuildVertexAttributes(); BuildVertices(); + BuildMetaAttributes(); ArgumentNullException.ThrowIfNull(_indices); - ArgumentNullException.ThrowIfNull(_attributes); + ArgumentNullException.ThrowIfNull(_vertexAttributes); ArgumentNullException.ThrowIfNull(_shapeValues); + ArgumentNullException.ThrowIfNull(_metaAttributes); var material = _primitive.Material.Name; if (material == "") 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() { SubMeshStruct = new MdlStructs.SubmeshStruct() { IndexOffset = 0, IndexCount = (uint)_indices.Length, - AttributeIndexMask = 0, + AttributeIndexMask = attributeMask, // TODO: Flesh these out. Game doesn't seem to rely on them existing, though. BoneStartIndex = 0, @@ -102,12 +127,13 @@ public class SubMeshImporter Material = material, VertexDeclaration = new MdlStructs.VertexDeclarationStruct() { - VertexElements = _attributes.Select(attribute => attribute.Element).ToArray(), + VertexElements = _vertexAttributes.Select(attribute => attribute.Element).ToArray(), }, VertexCount = _vertexCount, Strides = _strides, Streams = _streams, Indices = _indices, + MetaAttributes = _metaAttributes, ShapeValues = _shapeValues, }; } @@ -117,7 +143,7 @@ public class SubMeshImporter _indices = _primitive.GetIndices().Select(idx => (ushort)idx).ToArray(); } - private void BuildAttributes() + private void BuildVertexAttributes() { var accessors = _primitive.VertexAccessors; @@ -153,14 +179,14 @@ public class SubMeshImporter offsets[attribute.Stream] += attribute.Size; } - _attributes = attributes; + _vertexAttributes = attributes; // After building the attributes, the resulting next offsets are our stream strides. _strides = offsets; } private void BuildVertices() { - ArgumentNullException.ThrowIfNull(_attributes); + ArgumentNullException.ThrowIfNull(_vertexAttributes); // Lists of vertex indices that are effected by each morph target for this primitive. var morphModifiedVertices = Enumerable.Range(0, _primitive.MorphTargetsCount) @@ -173,13 +199,13 @@ public class SubMeshImporter for (var vertexIndex = 0; vertexIndex < _vertexCount; vertexIndex++) { // 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)); // Record which morph targets have values for this vertex, if any. var changedMorphs = morphModifiedVertices .WithIndex() - .Where(pair => _attributes.Any(attribute => attribute.HasMorph(pair.Index, vertexIndex))) + .Where(pair => _vertexAttributes.Any(attribute => attribute.HasMorph(pair.Index, vertexIndex))) .Select(pair => pair.Value); foreach (var modifiedVertices in changedMorphs) modifiedVertices.Add(vertexIndex); @@ -191,7 +217,7 @@ public class SubMeshImporter private void BuildShapeValues(IEnumerable> morphModifiedVertices) { ArgumentNullException.ThrowIfNull(_indices); - ArgumentNullException.ThrowIfNull(_attributes); + ArgumentNullException.ThrowIfNull(_vertexAttributes); var morphShapeValues = new Dictionary>(); @@ -203,7 +229,7 @@ public class SubMeshImporter foreach (var vertexIndex in modifiedVertices) { // 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)); // Find any indices that target this vertex index and create a mapping. @@ -224,4 +250,13 @@ public class SubMeshImporter _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() ?? []; + } } diff --git a/Penumbra/Import/Models/Import/Utility.cs b/Penumbra/Import/Models/Import/Utility.cs new file mode 100644 index 00000000..449d19e4 --- /dev/null +++ b/Penumbra/Import/Models/Import/Utility.cs @@ -0,0 +1,34 @@ +namespace Penumbra.Import.Models.Import; + +public static class Utility +{ + /// Merge attributes into an existing attribute array, providing an updated submesh mask. + /// Old submesh attribute mask. + /// Old attribute array that should be merged. + /// New attribute array. Will be mutated. + /// New submesh attribute mask, updated to match the merged attribute array. + public static uint GetMergedAttributeMask(uint oldMask, IList oldAttributes, List 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; + } +} From edcffb9d9f7f6ee7ac5e454a15842b3c4d3ed01d Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 11 Jan 2024 21:26:34 +1100 Subject: [PATCH 6/8] Allow keeping existing mdl attributes --- .../ModEditWindow.Models.MdlTab.cs | 34 +++++++++++++++++++ .../UI/AdvancedWindow/ModEditWindow.Models.cs | 1 + 2 files changed, 35 insertions(+) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index d38d8d92..cdaf399f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -16,6 +16,7 @@ public partial class ModEditWindow private List[] _attributes; public bool ImportKeepMaterials; + public bool ImportKeepAttributes; public List? GamePaths { get; private set; } public int GamePathIndex; @@ -158,6 +159,9 @@ public partial class ModEditWindow if (ImportKeepMaterials) MergeMaterials(newMdl, Mdl); + if (ImportKeepAttributes) + MergeAttributes(newMdl, Mdl); + Initialize(newMdl); _dirty = true; } @@ -177,6 +181,36 @@ public partial class ModEditWindow } } + /// Merge attribute configuration from the source onto the target. + /// + /// Model to copy attribute configuration from. + public void MergeAttributes(MdlFile target, MdlFile source) + { + target.Attributes = source.Attributes; + + var indexEnumerator = Enumerable.Range(0, target.Meshes.Length) + .SelectMany(mi => Enumerable.Range(0, target.Meshes[mi].SubMeshCount).Select(so => (mi, so))); + foreach (var (meshIndex, subMeshOffset) in indexEnumerator) + { + var subMeshIndex = target.Meshes[meshIndex].SubMeshIndex + subMeshOffset; + + // Preemptively reset the mask in case we need to shortcut out. + target.SubMeshes[subMeshIndex].AttributeIndexMask = 0u; + + // Rather than comparing sub-meshes directly, we're grouping by parent mesh in an attempt + // to maintain semantic connection betwen mesh index and submesh attributes. + if (meshIndex >= source.Meshes.Length) + continue; + var sourceMesh = source.Meshes[meshIndex]; + + if (subMeshOffset >= sourceMesh.SubMeshCount) + continue; + var sourceSubMesh = source.SubMeshes[sourceMesh.SubMeshIndex + subMeshOffset]; + + target.SubMeshes[subMeshIndex].AttributeIndexMask = sourceSubMesh.AttributeIndexMask; + } + } + private void RecordIoExceptions(Exception? exception) { IoExceptions = exception switch { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index fbdfcc74..8c298d4f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -80,6 +80,7 @@ public partial class ModEditWindow using (var frame = ImRaii.FramedGroup("Import", size, headerPreIcon: FontAwesomeIcon.FileImport)) { ImGui.Checkbox("Keep current materials", ref tab.ImportKeepMaterials); + ImGui.Checkbox("Keep current attributes", ref tab.ImportKeepAttributes); if (ImGuiUtil.DrawDisabledButton("Import from glTF", Vector2.Zero, "Imports a glTF file, overriding the content of this mdl.", tab.PendingIo)) From 2e473a62f4587b7c67724b4d6d219847e7f846c2 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 11 Jan 2024 21:28:52 +1100 Subject: [PATCH 7/8] Sneak a small one in --- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 8c298d4f..c92e2926 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -479,5 +479,6 @@ public partial class ModEditWindow private static readonly string[] ValidModelExtensions = [ ".gltf", + ".glb", ]; } From b81f3f423c10a6bfba2c7bc00e7bcf1d221e576e Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 11 Jan 2024 21:54:52 +1100 Subject: [PATCH 8/8] Cleanup pass --- Penumbra/Import/Models/Import/ModelImporter.cs | 14 +++++++------- Penumbra/Import/Models/Import/SubMeshImporter.cs | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs index 0d8c029d..3b3d2cd0 100644 --- a/Penumbra/Import/Models/Import/ModelImporter.cs +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -136,14 +136,14 @@ public partial class ModelImporter(ModelRoot model) var mesh = MeshImporter.Import(subMeshNodes); var meshStartIndex = (uint)(mesh.MeshStruct.StartIndex + indexOffset); - ushort materialIndex = 0; - if (mesh.Material != null) - materialIndex = GetMaterialIndex(mesh.Material); + var materialIndex = mesh.Material != null + ? GetMaterialIndex(mesh.Material) + : (ushort)0; // If no bone table is used for a mesh, the index is set to 255. - ushort boneTableIndex = 255; - if (mesh.Bones != null) - boneTableIndex = BuildBoneTable(mesh.Bones); + var boneTableIndex = mesh.Bones != null + ? BuildBoneTable(mesh.Bones) + : (ushort)255; _meshes.Add(mesh.MeshStruct with { @@ -196,7 +196,7 @@ public partial class ModelImporter(ModelRoot model) private ushort GetMaterialIndex(string materialName) { - // If we already have this material, grab the current one + // If we already have this material, grab the current index. var index = _materials.IndexOf(materialName); if (index >= 0) return (ushort)index; diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs index 6b12ee09..51443f64 100644 --- a/Penumbra/Import/Models/Import/SubMeshImporter.cs +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -69,7 +69,7 @@ public class SubMeshImporter } catch { - _nodeExtras = null; + _nodeExtras = null; } try