From 13d594ca878c6a3cd7e12beb4ea7e8601aa5ad93 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 6 Jan 2024 23:13:34 +1100 Subject: [PATCH] Clean up models --- .../Import/Models/Import/ModelImporter.cs | 224 ++++++++++++++++ Penumbra/Import/Models/ModelManager.cs | 252 +----------------- 2 files changed, 226 insertions(+), 250 deletions(-) create mode 100644 Penumbra/Import/Models/Import/ModelImporter.cs diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs new file mode 100644 index 00000000..f53d2b64 --- /dev/null +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -0,0 +1,224 @@ +using Lumina.Data.Parsing; +using Penumbra.GameData.Files; +using SharpGLTF.Schema2; + +namespace Penumbra.Import.Models.Import; + +public partial class ModelImporter +{ + public static MdlFile Import(ModelRoot model) + { + var importer = new ModelImporter(model); + return importer.Create(); + } + + // NOTE: This is intended to match TexTool's grouping regex, ".*[_ ^]([0-9]+)[\\.\\-]?([0-9]+)?$" + [GeneratedRegex(@"[_ ^](?'Mesh'[0-9]+)[.-]?(?'SubMesh'[0-9]+)?$", RegexOptions.Compiled)] + private static partial Regex MeshNameGroupingRegex(); + + private readonly ModelRoot _model; + + private List _meshes = new(); + private List _subMeshes = new(); + + private List _vertexDeclarations = new(); + private List _vertexBuffer = new(); + + private List _indices = new(); + + private List _bones = new(); + private List _boneTables = new(); + + private Dictionary> _shapeMeshes = new(); + private List _shapeValues = new(); + + private ModelImporter(ModelRoot model) + { + _model = model; + } + + private MdlFile Create() + { + // Group and build out meshes in this model. + foreach (var subMeshNodes in GroupedMeshNodes()) + BuildMeshForGroup(subMeshNodes); + + // Now that all of the meshes have been built, we can build some of the model-wide metadata. + var shapes = new List(); + var shapeMeshes = new List(); + foreach (var (keyName, keyMeshes) in _shapeMeshes) + { + shapes.Add(new MdlFile.Shape() + { + ShapeName = keyName, + // NOTE: these values are per-LoD. + ShapeMeshStartIndex = [(ushort)shapeMeshes.Count, 0, 0], + ShapeMeshCount = [(ushort)keyMeshes.Count, 0, 0], + }); + shapeMeshes.AddRange(keyMeshes); + } + + var indexBuffer = _indices.SelectMany(BitConverter.GetBytes).ToArray(); + + var emptyBoundingBox = new MdlStructs.BoundingBoxStruct() + { + Min = [0, 0, 0, 0], + Max = [0, 0, 0, 0], + }; + + // And finally, the MdlFile itself. + return new MdlFile() + { + VertexOffset = [0, 0, 0], + VertexBufferSize = [(uint)_vertexBuffer.Count, 0, 0], + IndexOffset = [(uint)_vertexBuffer.Count, 0, 0], + IndexBufferSize = [(uint)indexBuffer.Length, 0, 0], + + VertexDeclarations = _vertexDeclarations.ToArray(), + Meshes = _meshes.ToArray(), + SubMeshes = _subMeshes.ToArray(), + + BoneTables = _boneTables.ToArray(), + Bones = _bones.ToArray(), + // TODO: Game doesn't seem to rely on this, but would be good to populate. + SubMeshBoneMap = [], + + Shapes = shapes.ToArray(), + ShapeMeshes = shapeMeshes.ToArray(), + ShapeValues = _shapeValues.ToArray(), + + LodCount = 1, + + Lods = [new MdlStructs.LodStruct() + { + MeshIndex = 0, + MeshCount = (ushort)_meshes.Count, + + ModelLodRange = 0, + TextureLodRange = 0, + + VertexDataOffset = 0, + VertexBufferSize = (uint)_vertexBuffer.Count, + IndexDataOffset = (uint)_vertexBuffer.Count, + IndexBufferSize = (uint)indexBuffer.Length, + }], + + // TODO: Would be good to populate from gltf material names. + Materials = ["/NO_MATERIAL"], + + // TODO: Would be good to calculate all of this up the tree. + Radius = 1, + BoundingBoxes = emptyBoundingBox, + BoneBoundingBoxes = Enumerable.Repeat(emptyBoundingBox, _bones.Count).ToArray(), + + RemainingData = [.._vertexBuffer, ..indexBuffer], + }; + } + + /// Returns an iterator over sorted, grouped mesh nodes. + private IEnumerable> GroupedMeshNodes() => + _model.LogicalNodes + .Where(node => node.Mesh != null) + .Select(node => + { + var name = node.Name ?? node.Mesh.Name ?? "NOMATCH"; + var match = MeshNameGroupingRegex().Match(name); + return (node, match); + }) + .Where(pair => pair.match.Success) + .OrderBy(pair => + { + var subMeshGroup = pair.match.Groups["SubMesh"]; + return subMeshGroup.Success ? int.Parse(subMeshGroup.Value) : 0; + }) + .GroupBy( + pair => int.Parse(pair.match.Groups["Mesh"].Value), + pair => pair.node + ) + .OrderBy(group => group.Key); + + private void BuildMeshForGroup(IEnumerable subMeshNodes) + { + // Record some offsets we'll be using later, before they get mutated with mesh values. + var subMeshOffset = _subMeshes.Count; + var vertexOffset = _vertexBuffer.Count; + var indexOffset = _indices.Count; + var shapeValueOffset = _shapeValues.Count; + + var mesh = MeshImporter.Import(subMeshNodes); + var meshStartIndex = (uint)(mesh.MeshStruct.StartIndex + indexOffset); + + // If no bone table is used for a mesh, the index is set to 255. + var boneTableIndex = 255; + if (mesh.Bones != null) + boneTableIndex = BuildBoneTable(mesh.Bones); + + _meshes.Add(mesh.MeshStruct with + { + SubMeshIndex = (ushort)(mesh.MeshStruct.SubMeshIndex + subMeshOffset), + BoneTableIndex = (ushort)boneTableIndex, + StartIndex = meshStartIndex, + VertexBufferOffset = mesh.MeshStruct.VertexBufferOffset + .Select(offset => (uint)(offset + vertexOffset)) + .ToArray(), + }); + + foreach (var subMesh in mesh.SubMeshStructs) + _subMeshes.Add(subMesh with + { + IndexOffset = (uint)(subMesh.IndexOffset + indexOffset), + }); + + _vertexDeclarations.Add(mesh.VertexDeclaration); + _vertexBuffer.AddRange(mesh.VertexBuffer); + + _indices.AddRange(mesh.Indicies); + + foreach (var meshShapeKey in mesh.ShapeKeys) + { + if (!_shapeMeshes.TryGetValue(meshShapeKey.Name, out var shapeMeshes)) + { + shapeMeshes = new(); + _shapeMeshes.Add(meshShapeKey.Name, shapeMeshes); + } + + shapeMeshes.Add(meshShapeKey.ShapeMesh with + { + MeshIndexOffset = meshStartIndex, + ShapeValueOffset = (uint)shapeValueOffset, + }); + + _shapeValues.AddRange(meshShapeKey.ShapeValues); + } + } + + private ushort BuildBoneTable(List boneNames) + { + var boneIndices = new List(); + foreach (var boneName in boneNames) + { + var boneIndex = _bones.IndexOf(boneName); + if (boneIndex == -1) + { + boneIndex = _bones.Count; + _bones.Add(boneName); + } + boneIndices.Add((ushort)boneIndex); + } + + if (boneIndices.Count > 64) + throw new Exception("XIV does not support meshes weighted to more than 64 bones."); + + var boneIndicesArray = new ushort[64]; + Array.Copy(boneIndices.ToArray(), boneIndicesArray, boneIndices.Count); + + var boneTableIndex = _boneTables.Count; + _boneTables.Add(new MdlStructs.BoneTableStruct() + { + BoneIndex = boneIndicesArray, + BoneCount = (byte)boneIndices.Count, + }); + + return (ushort)boneTableIndex; + } +} diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 65067242..ebbe0411 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -1,20 +1,15 @@ using Dalamud.Plugin.Services; -using Lumina.Data.Parsing; -using OtterGui; using OtterGui.Tasks; using Penumbra.Collections.Manager; using Penumbra.GameData.Files; using Penumbra.Import.Models.Export; using Penumbra.Import.Models.Import; -using SharpGLTF.Geometry; -using SharpGLTF.Geometry.VertexTypes; -using SharpGLTF.Materials; using SharpGLTF.Scenes; using SharpGLTF.Schema2; namespace Penumbra.Import.Models; -public sealed partial class ModelManager : SingleTaskQueue, IDisposable +public sealed class ModelManager : SingleTaskQueue, IDisposable { private readonly IFramework _framework; private readonly IDataManager _gameData; @@ -125,10 +120,6 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable private partial class ImportGltfAction : IAction { - // TODO: clean this up a bit, i don't actually need all of it. - [GeneratedRegex(@".*[_ ^](?'Mesh'[0-9]+)[\\.\\-]?([0-9]+)?$", RegexOptions.Compiled)] - private static partial Regex MeshNameGroupingRegex(); - public MdlFile? Out; public ImportGltfAction() @@ -136,250 +127,11 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable // } - private ModelRoot Build() - { - // Build a super simple plane as a fake gltf input. - var material = new MaterialBuilder(); - var mesh = new MeshBuilder("mesh 0.0"); - var prim = mesh.UsePrimitive(material); - var tangent = new Vector4(.5f, .5f, 0, 1); - var vert1 = new VertexBuilder( - new VertexPositionNormalTangent(new Vector3(-1, 0, 1), Vector3.UnitY, tangent), - new VertexColor1Texture2(Vector4.One, Vector2.UnitY, Vector2.Zero), - new VertexJoints4([(0, 1), (0, 0), (0, 0), (0, 0)]) - ); - var vert2 = new VertexBuilder( - new VertexPositionNormalTangent(new Vector3(1, 0, 1), Vector3.UnitY, tangent), - new VertexColor1Texture2(Vector4.One, Vector2.One, Vector2.Zero), - new VertexJoints4([(0, 1), (0, 0), (0, 0), (0, 0)]) - ); - var vert3 = new VertexBuilder( - new VertexPositionNormalTangent(new Vector3(-1, 0, -1), Vector3.UnitY, tangent), - new VertexColor1Texture2(Vector4.One, Vector2.Zero, Vector2.Zero), - new VertexJoints4([(0, 1), (0, 0), (0, 0), (0, 0)]) - ); - var vert4 = new VertexBuilder( - new VertexPositionNormalTangent(new Vector3(1, 0, -1), Vector3.UnitY, tangent), - new VertexColor1Texture2(Vector4.One, Vector2.UnitX, Vector2.Zero), - new VertexJoints4([(0, 1), (0, 0), (0, 0), (0, 0)]) - ); - prim.AddTriangle(vert2, vert3, vert1); - prim.AddTriangle(vert2, vert4, vert3); - var jKosi = new NodeBuilder("j_kosi"); - var scene = new SceneBuilder(); - scene.AddNode(jKosi); - scene.AddSkinnedMesh(mesh, Matrix4x4.Identity, [jKosi]); - var model = scene.ToGltf2(); - - return model; - } - public void Execute(CancellationToken cancel) { var model = ModelRoot.Load("C:\\Users\\ackwell\\blender\\gltf-tests\\c0201e6180_top.gltf"); - // TODO: for grouping, should probably use `node.name ?? mesh.name`, as which are set seems to depend on the exporter. - // var nodes = model.LogicalNodes - // .Where(node => node.Mesh != null) - // // TODO: I'm just grabbing the first 3, as that will contain 0.0, 0.1, and 1.0. testing, and all that. - // .Take(3); - - // tt uses this - // ".*[_ ^]([0-9]+)[\\.\\-]?([0-9]+)?$" - var nodes = model.LogicalNodes - .Where(node => node.Mesh != null) - .Take(6) // this model has all 3 lods in it - the first 6 are the real lod0 - .SelectWhere(node => - { - var name = node.Name ?? node.Mesh.Name; - var match = MeshNameGroupingRegex().Match(name); - return match.Success - ? (true, (node, int.Parse(match.Groups["Mesh"].Value))) - : (false, (node, -1)); - }) - .GroupBy(pair => pair.Item2, pair => pair.node) - .OrderBy(group => group.Key); - - // this is a representation of a single LoD - var vertexDeclarations = new List(); - var bones = new List(); - var boneTables = new List(); - var meshes = new List(); - var submeshes = new List(); - var vertexBuffer = new List(); - var indices = new List(); - - var shapeData = new Dictionary>(); - var shapeValues = new List(); - - foreach (var submeshnodes in nodes) - { - var boneTableOffset = boneTables.Count; - var meshOffset = meshes.Count; - var subOffset = submeshes.Count; - var vertOffset = vertexBuffer.Count; - var idxOffset = indices.Count; - var shapeValueOffset = shapeValues.Count; - - var meshthing = MeshImporter.Import(submeshnodes); - - var boneTableIndex = 255; - if (meshthing.Bones != null) - { - var boneIndices = new List(); - foreach (var mb in meshthing.Bones) - { - var boneIndex = bones.IndexOf(mb); - if (boneIndex == -1) - { - boneIndex = bones.Count; - bones.Add(mb); - } - boneIndices.Add((ushort)boneIndex); - } - - if (boneIndices.Count > 64) - throw new Exception("One mesh cannot be weighted to more than 64 bones."); - - var boneIndicesArray = new ushort[64]; - Array.Copy(boneIndices.ToArray(), boneIndicesArray, boneIndices.Count); - - boneTableIndex = boneTableOffset; - boneTables.Add(new MdlStructs.BoneTableStruct() - { - BoneCount = (byte)boneIndices.Count, - BoneIndex = boneIndicesArray, - }); - } - - vertexDeclarations.Add(meshthing.VertexDeclaration); - var meshStartIndex = (uint)(meshthing.MeshStruct.StartIndex + idxOffset / sizeof(ushort)); - meshes.Add(meshthing.MeshStruct with - { - SubMeshIndex = (ushort)(meshthing.MeshStruct.SubMeshIndex + subOffset), - // TODO: should probably define a type for index type hey. - BoneTableIndex = (ushort)boneTableIndex, - StartIndex = meshStartIndex, - VertexBufferOffset = meshthing.MeshStruct.VertexBufferOffset - .Select(offset => (uint)(offset + vertOffset)) - .ToArray(), - }); - // TODO: could probably do this with linq cleaner - foreach (var xivSubmesh in meshthing.SubMeshStructs) - submeshes.Add(xivSubmesh with - { - // TODO: this will need to keep ticking up for each submesh in the same mesh - IndexOffset = (uint)(xivSubmesh.IndexOffset + idxOffset / sizeof(ushort)) - }); - vertexBuffer.AddRange(meshthing.VertexBuffer); - indices.AddRange(meshthing.Indicies.SelectMany(index => BitConverter.GetBytes((ushort)index))); - foreach (var shapeKey in meshthing.ShapeKeys) - { - List keyshapedata; - if (!shapeData.TryGetValue(shapeKey.Name, out keyshapedata)) - { - keyshapedata = new(); - shapeData.Add(shapeKey.Name, keyshapedata); - } - - keyshapedata.Add(shapeKey.ShapeMesh with - { - MeshIndexOffset = meshStartIndex, - ShapeValueOffset = (uint)shapeValueOffset, - }); - - shapeValues.AddRange(shapeKey.ShapeValues); - } - } - - var shapes = new List(); - var shapeMeshes = new List(); - - foreach (var (name, sms) in shapeData) - { - var smOff = shapeMeshes.Count; - - shapeMeshes.AddRange(sms); - shapes.Add(new MdlFile.Shape() - { - ShapeName = name, - // TODO: THESE IS PER LOD - ShapeMeshStartIndex = [(ushort)smOff, 0, 0], - ShapeMeshCount = [(ushort)sms.Count, 0, 0], - }); - } - - var mdl = new MdlFile() - { - Radius = 1, - // todo: lod calcs... probably handled in penum? we probably only need to think about lod0 for actual import workflow. - VertexOffset = [0, 0, 0], - IndexOffset = [(uint)vertexBuffer.Count, 0, 0], - VertexBufferSize = [(uint)vertexBuffer.Count, 0, 0], - IndexBufferSize = [(uint)indices.Count, 0, 0], - LodCount = 1, - BoundingBoxes = new MdlStructs.BoundingBoxStruct() - { - Min = [-1, 0, -1, 1], - Max = [1, 0, 1, 1], - }, - VertexDeclarations = vertexDeclarations.ToArray(), - Meshes = meshes.ToArray(), - BoneTables = boneTables.ToArray(), - BoneBoundingBoxes = [ - // new MdlStructs.BoundingBoxStruct() - // { - // Min = [ - // -0.081672676f, - // -0.113717034f, - // -0.11905348f, - // 1.0f, - // ], - // Max = [ - // 0.03941727f, - // 0.09845419f, - // 0.107391916f, - // 1.0f, - // ], - // }, - - // _would_ be nice if i didn't need to fill out this - new MdlStructs.BoundingBoxStruct() - { - Min = [0, 0, 0, 0], - Max = [0, 0, 0, 0], - } - ], - SubMeshes = submeshes.ToArray(), - - // TODO pretty sure this is garbage data as far as textools functions - // game clearly doesn't rely on this, but the "correct" values are a listing of the bones used by each submesh - SubMeshBoneMap = [0], - - Shapes = shapes.ToArray(), - ShapeMeshes = shapeMeshes.ToArray(), - ShapeValues = shapeValues.ToArray(), - - Lods = [new MdlStructs.LodStruct() - { - MeshIndex = 0, - MeshCount = (ushort)meshes.Count, - ModelLodRange = 0, - TextureLodRange = 0, - VertexBufferSize = (uint)vertexBuffer.Count, - VertexDataOffset = 0, - IndexBufferSize = (uint)indices.Count, - IndexDataOffset = (uint)vertexBuffer.Count, - }, - ], - Bones = bones.ToArray(), - Materials = [ - "/mt_c0201e6180_top_a.mtrl", - ], - RemainingData = vertexBuffer.Concat(indices).ToArray(), - }; - - Out = mdl; + Out = ModelImporter.Import(model); } public bool Equals(IAction? other)