From b7edf521b62d205e43d82eef66c746a4b4e0f4f3 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 4 Jan 2024 19:27:04 +1100 Subject: [PATCH 01/15] SuzanneWalker --- Penumbra.GameData | 2 +- Penumbra/Import/Models/ModelManager.cs | 437 ++++++++++++++++++ .../ModEditWindow.Models.MdlTab.cs | 22 +- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 6 + 4 files changed, 461 insertions(+), 6 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index db421413..821194d0 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit db421413a15c48c63eb883dbfc2ac863c579d4c6 +Subproject commit 821194d0650a2dac98b7cbba9ff4a79e32b32d4d diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 35a5e53e..243390a7 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -1,9 +1,14 @@ using Dalamud.Plugin.Services; +using Lumina.Data.Parsing; using OtterGui.Tasks; using Penumbra.Collections.Manager; using Penumbra.GameData.Files; using Penumbra.Import.Models.Export; +using SharpGLTF.Geometry; +using SharpGLTF.Geometry.VertexTypes; +using SharpGLTF.Materials; using SharpGLTF.Scenes; +using SharpGLTF.Schema2; namespace Penumbra.Import.Models; @@ -54,6 +59,12 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable public Task ExportToGltf(MdlFile mdl, SklbFile? sklb, string outputPath) => Enqueue(new ExportToGltfAction(this, mdl, sklb, outputPath)); + public Task ImportGltf() + { + var action = new ImportGltfAction(); + return Enqueue(action).ContinueWith(_ => action.Out!); + } + private class ExportToGltfAction : IAction { private readonly ModelManager _manager; @@ -109,4 +120,430 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable return true; } } + + private class ImportGltfAction : IAction + { + public MdlFile? Out; + + public ImportGltfAction() + { + // + } + + 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; + } + + private (MdlStructs.VertexElement, Action>) GetPositionWriter(IReadOnlyDictionary accessors) + { + if (!accessors.TryGetValue("POSITION", out var accessor)) + throw new Exception("todo: some error about position being hard required"); + + var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.Single3, + Usage = (byte)MdlFile.VertexUsage.Position, + }; + + IList values = accessor.AsVector3Array(); + + return ( + element, + (index, bytes) => WriteSingle3(values[index], bytes) + ); + } + + // TODO: probably should sanity check that if there's weights or indexes, both are available? game is always symmetric + private (MdlStructs.VertexElement, Action>)? GetBlendWeightWriter(IReadOnlyDictionary accessors) + { + if (!accessors.TryGetValue("WEIGHTS_0", out var accessor)) + return null; + + var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.ByteFloat4, + Usage = (byte)MdlFile.VertexUsage.BlendWeights, + }; + + var values = accessor.AsVector4Array(); + + return ( + element, + (index, bytes) => WriteByteFloat4(values[index], bytes) + ); + } + + // TODO: this will need to take in a skeleton mapping of some kind so i can persist the bones used and wire up the joints correctly. hopefully by the "write vertex buffer" stage of building, we already know something about the skeleton. + private (MdlStructs.VertexElement, Action>)? GetBlendIndexWriter(IReadOnlyDictionary accessors) + { + if (!accessors.TryGetValue("JOINTS_0", out var accessor)) + return null; + + var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.UInt, + Usage = (byte)MdlFile.VertexUsage.BlendIndices, + }; + + var values = accessor.AsVector4Array(); + + return ( + element, + (index, bytes) => WriteUInt(values[index], bytes) + ); + } + + private (MdlStructs.VertexElement, Action>)? GetNormalWriter(IReadOnlyDictionary accessors) + { + if (!accessors.TryGetValue("NORMAL", out var accessor)) + return null; + + var element = new MdlStructs.VertexElement() + { + Stream = 1, + Type = (byte)MdlFile.VertexType.Half4, + Usage = (byte)MdlFile.VertexUsage.Normal, + }; + + var values = accessor.AsVector3Array(); + + return ( + element, + (index, bytes) => WriteHalf4(new Vector4(values[index], 0), bytes) + ); + } + + private (MdlStructs.VertexElement, Action>)? GetUvWriter(IReadOnlyDictionary accessors) + { + if (!accessors.TryGetValue("TEXCOORD_0", out var accessor1)) + return null; + + // We're omitting type here, and filling it in on return, as there's two different types we might use. + var element = new MdlStructs.VertexElement() + { + Stream = 1, + Usage = (byte)MdlFile.VertexUsage.UV, + }; + + var values1 = accessor1.AsVector2Array(); + + if (!accessors.TryGetValue("TEXCOORD_1", out var accessor2)) + return ( + element with {Type = (byte)MdlFile.VertexType.Half2}, + (index, bytes) => WriteHalf2(values1[index], bytes) + ); + + var values2 = accessor2.AsVector2Array(); + + return ( + element with {Type = (byte)MdlFile.VertexType.Half4}, + (index, bytes) => { + var value1 = values1[index]; + var value2 = values2[index]; + WriteHalf4(new Vector4(value1.X, value1.Y, value2.X, value2.Y), bytes); + } + ); + } + + private (MdlStructs.VertexElement, Action>)? GetTangent1Writer(IReadOnlyDictionary accessors) + { + if (!accessors.TryGetValue("TANGENT", out var accessor)) + return null; + + var element = new MdlStructs.VertexElement() + { + Stream = 1, + Type = (byte)MdlFile.VertexType.ByteFloat4, + Usage = (byte)MdlFile.VertexUsage.Tangent1, + }; + + var values = accessor.AsVector4Array(); + + return ( + element, + (index, bytes) => WriteByteFloat4(values[index], bytes) + ); + } + + private (MdlStructs.VertexElement, Action>)? GetColorWriter(IReadOnlyDictionary accessors) + { + if (!accessors.TryGetValue("COLOR_0", out var accessor)) + return null; + + var element = new MdlStructs.VertexElement() + { + Stream = 1, + Type = (byte)MdlFile.VertexType.ByteFloat4, + Usage = (byte)MdlFile.VertexUsage.Color, + }; + + var values = accessor.AsVector4Array(); + + return ( + element, + (index, bytes) => WriteByteFloat4(values[index], bytes) + ); + } + + private void WriteSingle3(Vector3 input, List bytes) + { + bytes.AddRange(BitConverter.GetBytes(input.X)); + bytes.AddRange(BitConverter.GetBytes(input.Y)); + bytes.AddRange(BitConverter.GetBytes(input.Z)); + } + + private void WriteUInt(Vector4 input, List bytes) + { + bytes.Add((byte)input.X); + bytes.Add((byte)input.Y); + bytes.Add((byte)input.Z); + bytes.Add((byte)input.W); + } + + private void WriteByteFloat4(Vector4 input, List bytes) + { + bytes.Add((byte)Math.Round(input.X * 255f)); + bytes.Add((byte)Math.Round(input.Y * 255f)); + bytes.Add((byte)Math.Round(input.Z * 255f)); + bytes.Add((byte)Math.Round(input.W * 255f)); + } + + private void WriteHalf2(Vector2 input, List bytes) + { + bytes.AddRange(BitConverter.GetBytes((Half)input.X)); + bytes.AddRange(BitConverter.GetBytes((Half)input.Y)); + } + + private void WriteHalf4(Vector4 input, List bytes) + { + bytes.AddRange(BitConverter.GetBytes((Half)input.X)); + bytes.AddRange(BitConverter.GetBytes((Half)input.Y)); + bytes.AddRange(BitConverter.GetBytes((Half)input.Z)); + bytes.AddRange(BitConverter.GetBytes((Half)input.W)); + } + + private byte TypeSize(MdlFile.VertexType type) + { + return 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, + + _ => throw new Exception($"Unhandled vertex type {type}"), + }; + } + + public void Execute(CancellationToken cancel) + { + var model = Build(); + + // --- + + // todo this'll need to check names and such. also loop. i'm relying on a single mesh here which is Wrong:tm: + var mesh = model.LogicalNodes + .Where(node => node.Mesh != null) + .Select(node => node.Mesh) + .First(); + + // todo check how many prims there are - maybe throw if more than one? not sure + var prim = mesh.Primitives[0]; + + var accessors = prim.VertexAccessors; + + var rawWriters = new[] { + GetPositionWriter(accessors), + GetBlendWeightWriter(accessors), + GetBlendIndexWriter(accessors), + GetNormalWriter(accessors), + GetTangent1Writer(accessors), + GetColorWriter(accessors), + GetUvWriter(accessors), + }; + + var writers = new List<(MdlStructs.VertexElement, Action>)>(); + var offsets = new byte[] {0, 0, 0}; + foreach (var writer in rawWriters) + { + if (writer == null) continue; + var element = writer.Value.Item1; + writers.Add(( + element with {Offset = offsets[element.Stream]}, + writer.Value.Item2 + )); + offsets[element.Stream] += TypeSize((MdlFile.VertexType)element.Type); + } + var strides = offsets; + + var streams = new List[3]; + for (var i = 0; i < 3; i++) + streams[i] = new List(); + + // todo: this is a bit lmao but also... probably the most sane option? getting the count that is + var vertexCount = prim.VertexAccessors["POSITION"].Count; + for (var vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) + { + foreach (var (element, writer) in writers) + { + writer(vertexIndex, streams[element.Stream]); + } + } + + // indices + var indexCount = prim.GetIndexAccessor().Count; + var indices = prim.GetIndices() + .SelectMany(index => BitConverter.GetBytes((ushort)index)) + .ToArray(); + + var dataBuffer = streams[0].Concat(streams[1]).Concat(streams[2]).Concat(indices); + + var lod1VertLen = (uint)(streams[0].Count + streams[1].Count + streams[2].Count); + + 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 = [lod1VertLen, 0, 0], + VertexBufferSize = [lod1VertLen, 0, 0], + IndexBufferSize = [(uint)indices.Length, 0, 0], + LodCount = 1, + BoundingBoxes = new MdlStructs.BoundingBoxStruct() + { + Min = [-1, 0, -1, 1], + Max = [1, 0, 1, 1], + }, + VertexDeclarations = [new MdlStructs.VertexDeclarationStruct() + { + VertexElements = writers.Select(x => x.Item1).ToArray(), + }], + Meshes = [new MdlStructs.MeshStruct() + { + VertexCount = (ushort)vertexCount, + IndexCount = (uint)indexCount, + MaterialIndex = 0, + SubMeshIndex = 0, + SubMeshCount = 1, + BoneTableIndex = 0, + StartIndex = 0, + // todo: this will need to be composed down across multiple submeshes. given submeshes store contiguous buffers + VertexBufferOffset = [0, (uint)streams[0].Count, (uint)(streams[0].Count + streams[1].Count)], + VertexBufferStride = strides, + VertexStreamCount = 2, + }], + BoneTables = [new MdlStructs.BoneTableStruct() + { + BoneCount = 1, + // this needs to be the full 64. this should be fine _here_ with 0s because i only have one bone, but will need to be fully populated properly. in real files. + BoneIndex = new ushort[64], + }], + 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 = [new MdlStructs.SubmeshStruct() + { + IndexOffset = 0, + IndexCount = (uint)indexCount, + AttributeIndexMask = 0, + BoneStartIndex = 0, + BoneCount = 1, + }], + + // 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], + + Lods = [new MdlStructs.LodStruct() + { + MeshIndex = 0, + MeshCount = 1, + ModelLodRange = 0, + TextureLodRange = 0, + VertexBufferSize = lod1VertLen, + VertexDataOffset = 0, + IndexBufferSize = (uint)indexCount, + IndexDataOffset = lod1VertLen, + }, + ], + Bones = [ + "j_kosi", + ], + Materials = [ + "/mt_c0201e6180_top_b.mtrl", + ], + RemainingData = dataBuffer.ToArray(), + }; + + Out = mdl; + } + + public bool Equals(IAction? other) + { + if (other is not ImportGltfAction rhs) + return false; + + return true; + } + } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 20a4129d..5d9abda6 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -12,10 +12,10 @@ public partial class ModEditWindow { private ModEditWindow _edit; - public readonly MdlFile Mdl; - private readonly List[] _attributes; + public MdlFile Mdl { get; private set; } + private List[] _attributes; - public List? GamePaths { get; private set ;} + public List? GamePaths { get; private set; } public int GamePathIndex; public bool PendingIo { get; private set; } = false; @@ -34,13 +34,19 @@ public partial class ModEditWindow { _edit = edit; - Mdl = new MdlFile(bytes); - _attributes = CreateAttributes(Mdl); + Initialize(new MdlFile(bytes)); if (mod != null) FindGamePaths(path, mod); } + [MemberNotNull(nameof(Mdl), nameof(_attributes))] + private void Initialize(MdlFile mdl) + { + Mdl = mdl; + _attributes = CreateAttributes(Mdl); + } + /// public bool Valid => Mdl.Valid; @@ -72,6 +78,12 @@ public partial class ModEditWindow }); } + public void Import() + { + // TODO: this needs to be fleshed out a bunch. + _edit._models.ImportGltf().ContinueWith(v => Initialize(v.Result)); + } + /// Export model to an interchange format. /// Disk path to save the resulting file to. public void Export(string outputPath, Utf8GamePath mdlPath) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index aa69953b..d59cf1e5 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -37,6 +37,12 @@ public partial class ModEditWindow DrawExport(tab, disabled); var ret = false; + + if (ImGui.Button("import test")) + { + tab.Import(); + ret |= true; + } ret |= DrawModelMaterialDetails(tab, disabled); From b3fe538219bf3e7169a020719c4c44046a075e2b Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 4 Jan 2024 21:47:48 +1100 Subject: [PATCH 02/15] Split vertex attribute logic into seperate file --- .../Import/Models/Import/VertexAttribute.cs | 232 ++++++++++++++++ Penumbra/Import/Models/ModelManager.cs | 247 ++---------------- 2 files changed, 253 insertions(+), 226 deletions(-) create mode 100644 Penumbra/Import/Models/Import/VertexAttribute.cs diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs new file mode 100644 index 00000000..7c605ba8 --- /dev/null +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -0,0 +1,232 @@ +using Lumina.Data.Parsing; +using Penumbra.GameData.Files; +using SharpGLTF.Schema2; + +namespace Penumbra.Import.Models.Import; + +using Writer = Action>; +using Accessors = IReadOnlyDictionary; + +public class VertexAttribute +{ + /// XIV vertex element metadata structure. + public readonly MdlStructs.VertexElement Element; + /// Write this vertex attribute's value at the specified index to the provided byte array. + public readonly Writer Write; + + /// Size in bytes of a single vertex's attribute value. + 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, + + _ => throw new Exception($"Unhandled vertex type {(MdlFile.VertexType)Element.Type}"), + }; + + public VertexAttribute(MdlStructs.VertexElement element, Writer write) + { + Element = element; + Write = write; + } + + public static VertexAttribute Position(Accessors accessors) + { + if (!accessors.TryGetValue("POSITION", out var accessor)) + throw new Exception("Meshes must contain a POSITION attribute."); + + var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.Single3, + Usage = (byte)MdlFile.VertexUsage.Position, + }; + + var values = accessor.AsVector3Array(); + + return new VertexAttribute( + element, + (index, bytes) => WriteSingle3(values[index], bytes) + ); + } + + public static VertexAttribute? BlendWeight(Accessors accessors) + { + if (!accessors.TryGetValue("WEIGHTS_0", out var accessor)) + return null; + + if (!accessors.ContainsKey("JOINTS_0")) + throw new Exception("Mesh contained WEIGHTS_0 attribute but no corresponding JOINTS_0 attribute."); + + var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.ByteFloat4, + Usage = (byte)MdlFile.VertexUsage.BlendWeights, + }; + + var values = accessor.AsVector4Array(); + + return new VertexAttribute( + element, + (index, bytes) => WriteByteFloat4(values[index], bytes) + ); + } + + // TODO: this will need to take in a skeleton mapping of some kind so i can persist the bones used and wire up the joints correctly. hopefully by the "write vertex buffer" stage of building, we already know something about the skeleton. + public static VertexAttribute? BlendIndex(Accessors accessors) + { + if (!accessors.TryGetValue("JOINTS_0", out var accessor)) + return null; + + if (!accessors.ContainsKey("WEIGHTS_0")) + throw new Exception("Mesh contained JOINTS_0 attribute but no corresponding WEIGHTS_0 attribute."); + + var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.UInt, + Usage = (byte)MdlFile.VertexUsage.BlendIndices, + }; + + var values = accessor.AsVector4Array(); + + return new VertexAttribute( + element, + (index, bytes) => WriteUInt(values[index], bytes) + ); + } + + public static VertexAttribute? Normal(Accessors accessors) + { + if (!accessors.TryGetValue("NORMAL", out var accessor)) + return null; + + var element = new MdlStructs.VertexElement() + { + Stream = 1, + Type = (byte)MdlFile.VertexType.Half4, + Usage = (byte)MdlFile.VertexUsage.Normal, + }; + + var values = accessor.AsVector3Array(); + + return new VertexAttribute( + element, + (index, bytes) => WriteHalf4(new Vector4(values[index], 0), bytes) + ); + } + + public static VertexAttribute? Uv(Accessors accessors) + { + if (!accessors.TryGetValue("TEXCOORD_0", out var accessor1)) + return null; + + // We're omitting type here, and filling it in on return, as there's two different types we might use. + var element = new MdlStructs.VertexElement() + { + Stream = 1, + Usage = (byte)MdlFile.VertexUsage.UV, + }; + + var values1 = accessor1.AsVector2Array(); + + if (!accessors.TryGetValue("TEXCOORD_1", out var accessor2)) + return new VertexAttribute( + element with { Type = (byte)MdlFile.VertexType.Half2 }, + (index, bytes) => WriteHalf2(values1[index], bytes) + ); + + var values2 = accessor2.AsVector2Array(); + + return new VertexAttribute( + element with { Type = (byte)MdlFile.VertexType.Half4 }, + (index, bytes) => + { + var value1 = values1[index]; + var value2 = values2[index]; + WriteHalf4(new Vector4(value1.X, value1.Y, value2.X, value2.Y), bytes); + } + ); + } + + public static VertexAttribute? Tangent1(Accessors accessors) + { + if (!accessors.TryGetValue("TANGENT", out var accessor)) + return null; + + var element = new MdlStructs.VertexElement() + { + Stream = 1, + Type = (byte)MdlFile.VertexType.ByteFloat4, + Usage = (byte)MdlFile.VertexUsage.Tangent1, + }; + + var values = accessor.AsVector4Array(); + + return new VertexAttribute( + element, + (index, bytes) => WriteByteFloat4(values[index], bytes) + ); + } + + public static VertexAttribute? Color(Accessors accessors) + { + if (!accessors.TryGetValue("COLOR_0", out var accessor)) + return null; + + var element = new MdlStructs.VertexElement() + { + Stream = 1, + Type = (byte)MdlFile.VertexType.ByteFloat4, + Usage = (byte)MdlFile.VertexUsage.Color, + }; + + var values = accessor.AsVector4Array(); + + return new VertexAttribute( + element, + (index, bytes) => WriteByteFloat4(values[index], bytes) + ); + } + + private static void WriteSingle3(Vector3 input, List bytes) + { + bytes.AddRange(BitConverter.GetBytes(input.X)); + bytes.AddRange(BitConverter.GetBytes(input.Y)); + bytes.AddRange(BitConverter.GetBytes(input.Z)); + } + + private static void WriteUInt(Vector4 input, List bytes) + { + bytes.Add((byte)input.X); + bytes.Add((byte)input.Y); + bytes.Add((byte)input.Z); + bytes.Add((byte)input.W); + } + + private static void WriteByteFloat4(Vector4 input, List bytes) + { + bytes.Add((byte)Math.Round(input.X * 255f)); + bytes.Add((byte)Math.Round(input.Y * 255f)); + bytes.Add((byte)Math.Round(input.Z * 255f)); + bytes.Add((byte)Math.Round(input.W * 255f)); + } + + private static void WriteHalf2(Vector2 input, List bytes) + { + bytes.AddRange(BitConverter.GetBytes((Half)input.X)); + bytes.AddRange(BitConverter.GetBytes((Half)input.Y)); + } + + private static void WriteHalf4(Vector4 input, List bytes) + { + bytes.AddRange(BitConverter.GetBytes((Half)input.X)); + bytes.AddRange(BitConverter.GetBytes((Half)input.Y)); + bytes.AddRange(BitConverter.GetBytes((Half)input.Z)); + bytes.AddRange(BitConverter.GetBytes((Half)input.W)); + } +} diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 243390a7..e5349308 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -4,6 +4,7 @@ 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; @@ -127,7 +128,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable public ImportGltfAction() { - // + // } private ModelRoot Build() @@ -168,212 +169,6 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable return model; } - private (MdlStructs.VertexElement, Action>) GetPositionWriter(IReadOnlyDictionary accessors) - { - if (!accessors.TryGetValue("POSITION", out var accessor)) - throw new Exception("todo: some error about position being hard required"); - - var element = new MdlStructs.VertexElement() - { - Stream = 0, - Type = (byte)MdlFile.VertexType.Single3, - Usage = (byte)MdlFile.VertexUsage.Position, - }; - - IList values = accessor.AsVector3Array(); - - return ( - element, - (index, bytes) => WriteSingle3(values[index], bytes) - ); - } - - // TODO: probably should sanity check that if there's weights or indexes, both are available? game is always symmetric - private (MdlStructs.VertexElement, Action>)? GetBlendWeightWriter(IReadOnlyDictionary accessors) - { - if (!accessors.TryGetValue("WEIGHTS_0", out var accessor)) - return null; - - var element = new MdlStructs.VertexElement() - { - Stream = 0, - Type = (byte)MdlFile.VertexType.ByteFloat4, - Usage = (byte)MdlFile.VertexUsage.BlendWeights, - }; - - var values = accessor.AsVector4Array(); - - return ( - element, - (index, bytes) => WriteByteFloat4(values[index], bytes) - ); - } - - // TODO: this will need to take in a skeleton mapping of some kind so i can persist the bones used and wire up the joints correctly. hopefully by the "write vertex buffer" stage of building, we already know something about the skeleton. - private (MdlStructs.VertexElement, Action>)? GetBlendIndexWriter(IReadOnlyDictionary accessors) - { - if (!accessors.TryGetValue("JOINTS_0", out var accessor)) - return null; - - var element = new MdlStructs.VertexElement() - { - Stream = 0, - Type = (byte)MdlFile.VertexType.UInt, - Usage = (byte)MdlFile.VertexUsage.BlendIndices, - }; - - var values = accessor.AsVector4Array(); - - return ( - element, - (index, bytes) => WriteUInt(values[index], bytes) - ); - } - - private (MdlStructs.VertexElement, Action>)? GetNormalWriter(IReadOnlyDictionary accessors) - { - if (!accessors.TryGetValue("NORMAL", out var accessor)) - return null; - - var element = new MdlStructs.VertexElement() - { - Stream = 1, - Type = (byte)MdlFile.VertexType.Half4, - Usage = (byte)MdlFile.VertexUsage.Normal, - }; - - var values = accessor.AsVector3Array(); - - return ( - element, - (index, bytes) => WriteHalf4(new Vector4(values[index], 0), bytes) - ); - } - - private (MdlStructs.VertexElement, Action>)? GetUvWriter(IReadOnlyDictionary accessors) - { - if (!accessors.TryGetValue("TEXCOORD_0", out var accessor1)) - return null; - - // We're omitting type here, and filling it in on return, as there's two different types we might use. - var element = new MdlStructs.VertexElement() - { - Stream = 1, - Usage = (byte)MdlFile.VertexUsage.UV, - }; - - var values1 = accessor1.AsVector2Array(); - - if (!accessors.TryGetValue("TEXCOORD_1", out var accessor2)) - return ( - element with {Type = (byte)MdlFile.VertexType.Half2}, - (index, bytes) => WriteHalf2(values1[index], bytes) - ); - - var values2 = accessor2.AsVector2Array(); - - return ( - element with {Type = (byte)MdlFile.VertexType.Half4}, - (index, bytes) => { - var value1 = values1[index]; - var value2 = values2[index]; - WriteHalf4(new Vector4(value1.X, value1.Y, value2.X, value2.Y), bytes); - } - ); - } - - private (MdlStructs.VertexElement, Action>)? GetTangent1Writer(IReadOnlyDictionary accessors) - { - if (!accessors.TryGetValue("TANGENT", out var accessor)) - return null; - - var element = new MdlStructs.VertexElement() - { - Stream = 1, - Type = (byte)MdlFile.VertexType.ByteFloat4, - Usage = (byte)MdlFile.VertexUsage.Tangent1, - }; - - var values = accessor.AsVector4Array(); - - return ( - element, - (index, bytes) => WriteByteFloat4(values[index], bytes) - ); - } - - private (MdlStructs.VertexElement, Action>)? GetColorWriter(IReadOnlyDictionary accessors) - { - if (!accessors.TryGetValue("COLOR_0", out var accessor)) - return null; - - var element = new MdlStructs.VertexElement() - { - Stream = 1, - Type = (byte)MdlFile.VertexType.ByteFloat4, - Usage = (byte)MdlFile.VertexUsage.Color, - }; - - var values = accessor.AsVector4Array(); - - return ( - element, - (index, bytes) => WriteByteFloat4(values[index], bytes) - ); - } - - private void WriteSingle3(Vector3 input, List bytes) - { - bytes.AddRange(BitConverter.GetBytes(input.X)); - bytes.AddRange(BitConverter.GetBytes(input.Y)); - bytes.AddRange(BitConverter.GetBytes(input.Z)); - } - - private void WriteUInt(Vector4 input, List bytes) - { - bytes.Add((byte)input.X); - bytes.Add((byte)input.Y); - bytes.Add((byte)input.Z); - bytes.Add((byte)input.W); - } - - private void WriteByteFloat4(Vector4 input, List bytes) - { - bytes.Add((byte)Math.Round(input.X * 255f)); - bytes.Add((byte)Math.Round(input.Y * 255f)); - bytes.Add((byte)Math.Round(input.Z * 255f)); - bytes.Add((byte)Math.Round(input.W * 255f)); - } - - private void WriteHalf2(Vector2 input, List bytes) - { - bytes.AddRange(BitConverter.GetBytes((Half)input.X)); - bytes.AddRange(BitConverter.GetBytes((Half)input.Y)); - } - - private void WriteHalf4(Vector4 input, List bytes) - { - bytes.AddRange(BitConverter.GetBytes((Half)input.X)); - bytes.AddRange(BitConverter.GetBytes((Half)input.Y)); - bytes.AddRange(BitConverter.GetBytes((Half)input.Z)); - bytes.AddRange(BitConverter.GetBytes((Half)input.W)); - } - - private byte TypeSize(MdlFile.VertexType type) - { - return 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, - - _ => throw new Exception($"Unhandled vertex type {type}"), - }; - } - public void Execute(CancellationToken cancel) { var model = Build(); @@ -391,27 +186,27 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable var accessors = prim.VertexAccessors; - var rawWriters = new[] { - GetPositionWriter(accessors), - GetBlendWeightWriter(accessors), - GetBlendIndexWriter(accessors), - GetNormalWriter(accessors), - GetTangent1Writer(accessors), - GetColorWriter(accessors), - GetUvWriter(accessors), + var rawAttributes = new[] { + VertexAttribute.Position(accessors), + VertexAttribute.BlendWeight(accessors), + VertexAttribute.BlendIndex(accessors), + VertexAttribute.Normal(accessors), + VertexAttribute.Tangent1(accessors), + VertexAttribute.Color(accessors), + VertexAttribute.Uv(accessors), }; - var writers = new List<(MdlStructs.VertexElement, Action>)>(); + var attributes = new List(); var offsets = new byte[] {0, 0, 0}; - foreach (var writer in rawWriters) + foreach (var attribute in rawAttributes) { - if (writer == null) continue; - var element = writer.Value.Item1; - writers.Add(( + if (attribute == null) continue; + var element = attribute.Element; + attributes.Add(new VertexAttribute( element with {Offset = offsets[element.Stream]}, - writer.Value.Item2 + attribute.Write )); - offsets[element.Stream] += TypeSize((MdlFile.VertexType)element.Type); + offsets[element.Stream] += attribute.Size; } var strides = offsets; @@ -423,9 +218,9 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable var vertexCount = prim.VertexAccessors["POSITION"].Count; for (var vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) { - foreach (var (element, writer) in writers) + foreach (var attribute in attributes) { - writer(vertexIndex, streams[element.Stream]); + attribute.Write(vertexIndex, streams[attribute.Element.Stream]); } } @@ -455,7 +250,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable }, VertexDeclarations = [new MdlStructs.VertexDeclarationStruct() { - VertexElements = writers.Select(x => x.Item1).ToArray(), + VertexElements = attributes.Select(attribute => attribute.Element).ToArray(), }], Meshes = [new MdlStructs.MeshStruct() { @@ -530,7 +325,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable "j_kosi", ], Materials = [ - "/mt_c0201e6180_top_b.mtrl", + "/mt_c0201e6180_top_a.mtrl", ], RemainingData = dataBuffer.ToArray(), }; From 79de6f1714730efcde6cbf861d43d171d51d9f35 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 4 Jan 2024 23:33:54 +1100 Subject: [PATCH 03/15] Basic multi mesh handling --- .../Import/Models/Import/VertexAttribute.cs | 8 +- Penumbra/Import/Models/ModelManager.cs | 285 ++++++++++++------ 2 files changed, 194 insertions(+), 99 deletions(-) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 7c605ba8..430bc422 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -72,7 +72,9 @@ public class VertexAttribute return new VertexAttribute( element, - (index, bytes) => WriteByteFloat4(values[index], bytes) + // TODO: TEMP TESTING PINNED TO BONE 0 UNTIL I SET UP BONE MAPPINGS + // (index, bytes) => WriteByteFloat4(values[index], bytes) + (index, bytes) => WriteByteFloat4(Vector4.UnitX, bytes) ); } @@ -96,7 +98,9 @@ public class VertexAttribute return new VertexAttribute( element, - (index, bytes) => WriteUInt(values[index], bytes) + // TODO: TEMP TESTING PINNED TO BONE 0 UNTIL I SET UP BONE MAPPINGS + // (index, bytes) => WriteUInt(values[index], bytes) + (index, bytes) => WriteUInt(Vector4.Zero, bytes) ); } diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index e5349308..66f5202e 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -171,107 +171,75 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable public void Execute(CancellationToken cancel) { - var model = Build(); + var model = ModelRoot.Load("C:\\Users\\ackwell\\blender\\gltf-tests\\c0201e6180_top.gltf"); - // --- - - // todo this'll need to check names and such. also loop. i'm relying on a single mesh here which is Wrong:tm: - var mesh = model.LogicalNodes + // 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) - .Select(node => node.Mesh) - .First(); + // 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); - // todo check how many prims there are - maybe throw if more than one? not sure - var prim = mesh.Primitives[0]; - - var accessors = prim.VertexAccessors; - - var rawAttributes = new[] { - VertexAttribute.Position(accessors), - VertexAttribute.BlendWeight(accessors), - VertexAttribute.BlendIndex(accessors), - VertexAttribute.Normal(accessors), - VertexAttribute.Tangent1(accessors), - VertexAttribute.Color(accessors), - VertexAttribute.Uv(accessors), - }; - - var attributes = new List(); - var offsets = new byte[] {0, 0, 0}; - foreach (var attribute in rawAttributes) + // this is a representation of a single LoD + var vertexDeclarations = new List(); + var boneTables = new List(); + var meshes = new List(); + var submeshes = new List(); + var vertexBuffer = new List(); + var indices = new List(); + + foreach (var node in nodes) { - if (attribute == null) continue; - var element = attribute.Element; - attributes.Add(new VertexAttribute( - element with {Offset = offsets[element.Stream]}, - attribute.Write - )); - offsets[element.Stream] += attribute.Size; + var boneTableOffset = boneTables.Count; + var meshOffset = meshes.Count; + var subOffset = submeshes.Count; + var vertOffset = vertexBuffer.Count; + var idxOffset = indices.Count; + + var ( + vertexDeclaration, + boneTable, + xivMesh, + xivSubmesh, + meshVertexBuffer, + meshIndices + ) = MeshThing(node); + + vertexDeclarations.Add(vertexDeclaration); + boneTables.Add(boneTable); + meshes.Add(xivMesh with { + SubMeshIndex = (ushort)(xivMesh.SubMeshIndex + subOffset), + // TODO: should probably define a type for index type hey. + BoneTableIndex = (ushort)(xivMesh.BoneTableIndex + boneTableOffset), + StartIndex = (uint)(xivMesh.StartIndex + idxOffset / sizeof(ushort)), + VertexBufferOffset = xivMesh.VertexBufferOffset + .Select(offset => (uint)(offset + vertOffset)) + .ToArray(), + }); + 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(meshVertexBuffer); + indices.AddRange(meshIndices); } - var strides = offsets; - - var streams = new List[3]; - for (var i = 0; i < 3; i++) - streams[i] = new List(); - - // todo: this is a bit lmao but also... probably the most sane option? getting the count that is - var vertexCount = prim.VertexAccessors["POSITION"].Count; - for (var vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) - { - foreach (var attribute in attributes) - { - attribute.Write(vertexIndex, streams[attribute.Element.Stream]); - } - } - - // indices - var indexCount = prim.GetIndexAccessor().Count; - var indices = prim.GetIndices() - .SelectMany(index => BitConverter.GetBytes((ushort)index)) - .ToArray(); - - var dataBuffer = streams[0].Concat(streams[1]).Concat(streams[2]).Concat(indices); - - var lod1VertLen = (uint)(streams[0].Count + streams[1].Count + streams[2].Count); 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 = [lod1VertLen, 0, 0], - VertexBufferSize = [lod1VertLen, 0, 0], - IndexBufferSize = [(uint)indices.Length, 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 = [new MdlStructs.VertexDeclarationStruct() - { - VertexElements = attributes.Select(attribute => attribute.Element).ToArray(), - }], - Meshes = [new MdlStructs.MeshStruct() - { - VertexCount = (ushort)vertexCount, - IndexCount = (uint)indexCount, - MaterialIndex = 0, - SubMeshIndex = 0, - SubMeshCount = 1, - BoneTableIndex = 0, - StartIndex = 0, - // todo: this will need to be composed down across multiple submeshes. given submeshes store contiguous buffers - VertexBufferOffset = [0, (uint)streams[0].Count, (uint)(streams[0].Count + streams[1].Count)], - VertexBufferStride = strides, - VertexStreamCount = 2, - }], - BoneTables = [new MdlStructs.BoneTableStruct() - { - BoneCount = 1, - // this needs to be the full 64. this should be fine _here_ with 0s because i only have one bone, but will need to be fully populated properly. in real files. - BoneIndex = new ushort[64], - }], + VertexDeclarations = vertexDeclarations.ToArray(), + Meshes = meshes.ToArray(), + BoneTables = boneTables.ToArray(), BoneBoundingBoxes = [ // new MdlStructs.BoundingBoxStruct() // { @@ -296,14 +264,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable Max = [0, 0, 0, 0], } ], - SubMeshes = [new MdlStructs.SubmeshStruct() - { - IndexOffset = 0, - IndexCount = (uint)indexCount, - AttributeIndexMask = 0, - BoneStartIndex = 0, - BoneCount = 1, - }], + 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 @@ -312,13 +273,13 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable Lods = [new MdlStructs.LodStruct() { MeshIndex = 0, - MeshCount = 1, + MeshCount = (ushort)meshes.Count, ModelLodRange = 0, TextureLodRange = 0, - VertexBufferSize = lod1VertLen, + VertexBufferSize = (uint)vertexBuffer.Count, VertexDataOffset = 0, - IndexBufferSize = (uint)indexCount, - IndexDataOffset = lod1VertLen, + IndexBufferSize = (uint)indices.Count, + IndexDataOffset = (uint)vertexBuffer.Count, }, ], Bones = [ @@ -327,12 +288,142 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable Materials = [ "/mt_c0201e6180_top_a.mtrl", ], - RemainingData = dataBuffer.ToArray(), + RemainingData = vertexBuffer.Concat(indices).ToArray(), }; Out = mdl; } + // this return type is an absolute meme, class that shit up. + public ( + MdlStructs.VertexDeclarationStruct, + MdlStructs.BoneTableStruct, + MdlStructs.MeshStruct, + MdlStructs.SubmeshStruct, + IEnumerable, + IEnumerable + ) MeshThing(Node node) + { + // BoneTable (mesh.btidx = 255 means unskinned) + // vertexdecl + + var mesh = node.Mesh; + + // TODO: should probably say _what_ mesh + // TODO: would be cool to support >1 primitive (esp. given they're effectively what submeshes are modeled as), but blender doesn't really use them, so not going to prio that at all. + if (mesh.Primitives.Count != 1) + throw new Exception($"Mesh has {mesh.Primitives.Count} primitives, expected 1."); + var primitive = mesh.Primitives[0]; + + var accessors = primitive.VertexAccessors; + + var rawAttributes = new[] { + VertexAttribute.Position(accessors), + VertexAttribute.BlendWeight(accessors), + VertexAttribute.BlendIndex(accessors), + VertexAttribute.Normal(accessors), + VertexAttribute.Tangent1(accessors), + VertexAttribute.Color(accessors), + VertexAttribute.Uv(accessors), + }; + + var attributes = new List(); + var offsets = new byte[] {0, 0, 0}; + foreach (var attribute in rawAttributes) + { + if (attribute == null) continue; + var element = attribute.Element; + attributes.Add(new VertexAttribute( + element with {Offset = offsets[element.Stream]}, + attribute.Write + )); + offsets[element.Stream] += attribute.Size; + } + var strides = offsets; + + // TODO: when merging submeshes, i'll need to check that vert els are the same for all of them, as xiv only stores verts at the mesh level and shares them. + + var streams = new List[3]; + for (var i = 0; i < 3; i++) + streams[i] = new List(); + + // todo: this is a bit lmao but also... probably the most sane option? getting the count that is + var vertexCount = primitive.VertexAccessors["POSITION"].Count; + for (var vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) + { + foreach (var attribute in attributes) + { + attribute.Write(vertexIndex, streams[attribute.Element.Stream]); + } + } + + // indices + var indexCount = primitive.GetIndexAccessor().Count; + var indices = primitive.GetIndices() + .SelectMany(index => BitConverter.GetBytes((ushort)index)) + .ToArray(); + + // one of these per mesh + var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() + { + VertexElements = attributes.Select(attribute => attribute.Element).ToArray(), + }; + + // one of these per skinned mesh. + // TODO: check if mesh has skinning at all. + var boneTable = new MdlStructs.BoneTableStruct() + { + BoneCount = 1, + // this needs to be the full 64. this should be fine _here_ with 0s because i only have one bone, but will need to be fully populated properly. in real files. + BoneIndex = new ushort[64], + }; + + // mesh + var xivMesh = new MdlStructs.MeshStruct() + { + // TODO: sum across submeshes. + // TODO: would be cool to share verts on submesh boundaries but that's way out of scope for now. + VertexCount = (ushort)vertexCount, + IndexCount = (uint)indexCount, + // TODO: will have to think about how to represent this - materials can be named, so maybe adjust in parent? + MaterialIndex = 0, + // TODO: this will need adjusting by parent + SubMeshIndex = 0, + SubMeshCount = 1, + // TODO: update in parent + BoneTableIndex = 0, + // TODO: this is relative to the lod's index buffer, and is an index, not byte offset + StartIndex = 0, + // TODO: these are relative to the lod vertex buffer. these values are accurate for a 0 offset, but lod will need to adjust + VertexBufferOffset = [0, (uint)streams[0].Count, (uint)(streams[0].Count + streams[1].Count)], + VertexBufferStride = strides, + VertexStreamCount = /* 2 */ (byte)(attributes.Select(attribute => attribute.Element.Stream).Max() + 1), + }; + + // submesh + // TODO: once we have multiple submeshes, the _first_ should probably set an index offset of 0, and then further ones delta from there - and then they can be blindly adjusted by the parent that's laying out the meshes. + var xivSubmesh = new MdlStructs.SubmeshStruct() + { + IndexOffset = 0, + IndexCount = (uint)indexCount, + AttributeIndexMask = 0, + // TODO: not sure how i want to handle these ones + BoneStartIndex = 0, + BoneCount = 1, + }; + + var vertexBuffer = streams[0].Concat(streams[1]).Concat(streams[2]); + + return ( + vertexDeclaration, + boneTable, + xivMesh, + xivSubmesh, + vertexBuffer, + indices + ); + } + public bool Equals(IAction? other) { if (other is not ImportGltfAction rhs) From 4e8695e7a4e1e6c18b13b93773eb391fa09d06ef Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 5 Jan 2024 01:03:54 +1100 Subject: [PATCH 04/15] Spike submeshes --- Penumbra/Import/Models/ModelManager.cs | 209 ++++++++++++++++++------- 1 file changed, 155 insertions(+), 54 deletions(-) diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 66f5202e..43630f6e 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -1,5 +1,6 @@ using Dalamud.Plugin.Services; using Lumina.Data.Parsing; +using OtterGui; using OtterGui.Tasks; using Penumbra.Collections.Manager; using Penumbra.GameData.Files; @@ -13,7 +14,7 @@ using SharpGLTF.Schema2; namespace Penumbra.Import.Models; -public sealed class ModelManager : SingleTaskQueue, IDisposable +public sealed partial class ModelManager : SingleTaskQueue, IDisposable { private readonly IFramework _framework; private readonly IDataManager _gameData; @@ -122,8 +123,12 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable } } - private class ImportGltfAction : IAction + 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() @@ -174,10 +179,25 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable 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) - // 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); + .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(); @@ -187,7 +207,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable var vertexBuffer = new List(); var indices = new List(); - foreach (var node in nodes) + foreach (var submeshnodes in nodes) { var boneTableOffset = boneTables.Count; var meshOffset = meshes.Count; @@ -199,10 +219,10 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable vertexDeclaration, boneTable, xivMesh, - xivSubmesh, + xivSubmeshes, meshVertexBuffer, meshIndices - ) = MeshThing(node); + ) = MeshThing(submeshnodes); vertexDeclarations.Add(vertexDeclaration); boneTables.Add(boneTable); @@ -215,12 +235,14 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable .Select(offset => (uint)(offset + vertOffset)) .ToArray(), }); - 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)) - }); + // TODO: could probably do this with linq cleaner + foreach (var xivSubmesh in xivSubmeshes) + 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(meshVertexBuffer); - indices.AddRange(meshIndices); + indices.AddRange(meshIndices.SelectMany(index => BitConverter.GetBytes((ushort)index))); } var mdl = new MdlFile() @@ -295,14 +317,99 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable } // this return type is an absolute meme, class that shit up. - public ( + private ( MdlStructs.VertexDeclarationStruct, MdlStructs.BoneTableStruct, MdlStructs.MeshStruct, - MdlStructs.SubmeshStruct, + IEnumerable, IEnumerable, - IEnumerable - ) MeshThing(Node node) + IEnumerable + ) MeshThing(IEnumerable nodes) + { + var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() { VertexElements = Array.Empty()}; + var vertexCount = (ushort)0; + // there's gotta be a better way to do this with streams or enumerables or something, surely + var streams = new List[3]; + for (var i = 0; i < 3; i++) + streams[i] = new List(); + var indexCount = (uint)0; + var indices = new List(); + var strides = new byte[] {0, 0, 0}; + var submeshes = new List(); + + // TODO: check that attrs/elems/strides match - we should be generating per-mesh stuff for sanity's sake, but we need to make sure they match if there's >1 node mesh in a mesh. + foreach (var node in nodes) + { + var vertOff = vertexCount; + var idxOff = indexCount; + + var (vertDecl, newStrides, submesh, vertCount, vertStreams, idxCount, idxs) = NodeMeshThing(node); + vertexDeclaration = vertDecl; // TODO: CHECK EQUAL AFTER FIRST + strides = newStrides; // ALSO CHECK EQUAL + vertexCount += vertCount; + for (var i = 0; i < 3; i++) + streams[i].AddRange(vertStreams[i]); + indexCount += idxCount; + // we need to offset the indexes to point into the new stuff + indices.AddRange(idxs.Select(idx => (ushort)(idx + vertOff))); + submeshes.Add(submesh with { + IndexOffset = submesh.IndexOffset + idxOff + // TODO: bone stuff probably + }); + } + + // one of these per skinned mesh. + // TODO: check if mesh has skinning at all. (err if mixed?) + var boneTable = new MdlStructs.BoneTableStruct() + { + BoneCount = 1, + // this needs to be the full 64. this should be fine _here_ with 0s because i only have one bone, but will need to be fully populated properly. in real files. + BoneIndex = new ushort[64], + }; + + // mesh + var xivMesh = new MdlStructs.MeshStruct() + { + // TODO: sum across submeshes. + // TODO: would be cool to share verts on submesh boundaries but that's way out of scope for now. + VertexCount = vertexCount, + IndexCount = indexCount, + // TODO: will have to think about how to represent this - materials can be named, so maybe adjust in parent? + MaterialIndex = 0, + // TODO: this will need adjusting by parent + SubMeshIndex = 0, + SubMeshCount = (ushort)submeshes.Count, + // TODO: update in parent + BoneTableIndex = 0, + // TODO: this is relative to the lod's index buffer, and is an index, not byte offset + StartIndex = 0, + // TODO: these are relative to the lod vertex buffer. these values are accurate for a 0 offset, but lod will need to adjust + VertexBufferOffset = [0, (uint)streams[0].Count, (uint)(streams[0].Count + streams[1].Count)], + VertexBufferStride = strides, + // VertexStreamCount = /* 2 */ (byte)(attributes.Select(attribute => attribute.Element.Stream).Max() + 1), + VertexStreamCount = (byte)(vertexDeclaration.VertexElements.Select(element => element.Stream).Max() + 1) + }; + + return ( + vertexDeclaration, + boneTable, + xivMesh, + submeshes, + streams[0].Concat(streams[1]).Concat(streams[2]), + indices + ); + } + + private ( + MdlStructs.VertexDeclarationStruct, + byte[], + // MdlStructs.MeshStruct, + MdlStructs.SubmeshStruct, + ushort, + IEnumerable[], + uint, + IEnumerable + ) NodeMeshThing(Node node) { // BoneTable (mesh.btidx = 255 means unskinned) // vertexdecl @@ -358,10 +465,11 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable } // indices - var indexCount = primitive.GetIndexAccessor().Count; - var indices = primitive.GetIndices() - .SelectMany(index => BitConverter.GetBytes((ushort)index)) - .ToArray(); + // var indexCount = primitive.GetIndexAccessor().Count; + // var indices = primitive.GetIndices() + // .SelectMany(index => BitConverter.GetBytes((ushort)index)) + // .ToArray(); + var indices = primitive.GetIndices().Select(idx => (ushort)idx).ToArray(); // one of these per mesh var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() @@ -369,57 +477,50 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable VertexElements = attributes.Select(attribute => attribute.Element).ToArray(), }; - // one of these per skinned mesh. - // TODO: check if mesh has skinning at all. - var boneTable = new MdlStructs.BoneTableStruct() - { - BoneCount = 1, - // this needs to be the full 64. this should be fine _here_ with 0s because i only have one bone, but will need to be fully populated properly. in real files. - BoneIndex = new ushort[64], - }; - // mesh - var xivMesh = new MdlStructs.MeshStruct() - { - // TODO: sum across submeshes. - // TODO: would be cool to share verts on submesh boundaries but that's way out of scope for now. - VertexCount = (ushort)vertexCount, - IndexCount = (uint)indexCount, - // TODO: will have to think about how to represent this - materials can be named, so maybe adjust in parent? - MaterialIndex = 0, - // TODO: this will need adjusting by parent - SubMeshIndex = 0, - SubMeshCount = 1, - // TODO: update in parent - BoneTableIndex = 0, - // TODO: this is relative to the lod's index buffer, and is an index, not byte offset - StartIndex = 0, - // TODO: these are relative to the lod vertex buffer. these values are accurate for a 0 offset, but lod will need to adjust - VertexBufferOffset = [0, (uint)streams[0].Count, (uint)(streams[0].Count + streams[1].Count)], - VertexBufferStride = strides, - VertexStreamCount = /* 2 */ (byte)(attributes.Select(attribute => attribute.Element.Stream).Max() + 1), - }; + // var xivMesh = new MdlStructs.MeshStruct() + // { + // // TODO: sum across submeshes. + // // TODO: would be cool to share verts on submesh boundaries but that's way out of scope for now. + // VertexCount = (ushort)vertexCount, + // IndexCount = (uint)indexCount, + // // TODO: will have to think about how to represent this - materials can be named, so maybe adjust in parent? + // MaterialIndex = 0, + // // TODO: this will need adjusting by parent + // SubMeshIndex = 0, + // SubMeshCount = 1, + // // TODO: update in parent + // BoneTableIndex = 0, + // // TODO: this is relative to the lod's index buffer, and is an index, not byte offset + // StartIndex = 0, + // // TODO: these are relative to the lod vertex buffer. these values are accurate for a 0 offset, but lod will need to adjust + // VertexBufferOffset = [0, (uint)streams[0].Count, (uint)(streams[0].Count + streams[1].Count)], + // VertexBufferStride = strides, + // VertexStreamCount = /* 2 */ (byte)(attributes.Select(attribute => attribute.Element.Stream).Max() + 1), + // }; // submesh // TODO: once we have multiple submeshes, the _first_ should probably set an index offset of 0, and then further ones delta from there - and then they can be blindly adjusted by the parent that's laying out the meshes. var xivSubmesh = new MdlStructs.SubmeshStruct() { IndexOffset = 0, - IndexCount = (uint)indexCount, + IndexCount = (uint)indices.Length, AttributeIndexMask = 0, // TODO: not sure how i want to handle these ones BoneStartIndex = 0, BoneCount = 1, }; - var vertexBuffer = streams[0].Concat(streams[1]).Concat(streams[2]); + // var vertexBuffer = streams[0].Concat(streams[1]).Concat(streams[2]); return ( vertexDeclaration, - boneTable, - xivMesh, + strides, + // xivMesh, xivSubmesh, - vertexBuffer, + (ushort)vertexCount, + streams, + (uint)indices.Length, indices ); } From acaa49fec5520d5b0d8ce32b84b0cd9b14fb4eed Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 5 Jan 2024 15:32:31 +1100 Subject: [PATCH 05/15] Add shape key support --- .../Import/Models/Import/VertexAttribute.cs | 132 +++++++++---- Penumbra/Import/Models/ModelManager.cs | 176 +++++++++++++++++- 2 files changed, 260 insertions(+), 48 deletions(-) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 430bc422..9fd50513 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -4,7 +4,9 @@ using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Import; -using Writer = Action>; +using BuildFn = Func; +using HasMorphFn = Func; +using BuildMorphFn = Func; using Accessors = IReadOnlyDictionary; public class VertexAttribute @@ -12,7 +14,11 @@ public class VertexAttribute /// XIV vertex element metadata structure. public readonly MdlStructs.VertexElement Element; /// Write this vertex attribute's value at the specified index to the provided byte array. - public readonly Writer Write; + public readonly BuildFn Build; + + public readonly HasMorphFn HasMorph; + + public readonly BuildMorphFn BuildMorph; /// Size in bytes of a single vertex's attribute value. public byte Size => (MdlFile.VertexType)Element.Type switch @@ -27,13 +33,32 @@ public class VertexAttribute _ => throw new Exception($"Unhandled vertex type {(MdlFile.VertexType)Element.Type}"), }; - public VertexAttribute(MdlStructs.VertexElement element, Writer write) + public VertexAttribute( + MdlStructs.VertexElement element, + BuildFn write, + HasMorphFn? hasMorph = null, + BuildMorphFn? buildMorph = null + ) { Element = element; - Write = write; + Build = write; + HasMorph = hasMorph ?? DefaultHasMorph; + BuildMorph = buildMorph ?? DefaultBuildMorph; } - public static VertexAttribute Position(Accessors accessors) + // todo: this is per-shape at the moment - consider if it should do them all at once (i mean we always want to check all of them, it's mostly a semantics question on who owns the loop) + private static bool DefaultHasMorph(int morphIndex, int vertexIndex) + { + return false; + } + + // xiv stores shapes as full vertex replacements, so the default value for a morph attribute is simply it's built state (rather than a delta or w/e) + private byte[] DefaultBuildMorph(int morphIndex, int vertexIndex) + { + return Build(vertexIndex); + } + + public static VertexAttribute Position(Accessors accessors, IEnumerable morphAccessors) { if (!accessors.TryGetValue("POSITION", out var accessor)) throw new Exception("Meshes must contain a POSITION attribute."); @@ -47,9 +72,32 @@ public class VertexAttribute var values = accessor.AsVector3Array(); + var foo = morphAccessors + .Select(ma => ma.GetValueOrDefault("POSITION")?.AsVector3Array()) + .ToArray(); + return new VertexAttribute( element, - (index, bytes) => WriteSingle3(values[index], bytes) + index => BuildSingle3(values[index]), + // TODO: at the moment this is only defined for position - is it worth setting one up for normal, too? + (morphIndex, vertexIndex) => + { + var deltas = foo[morphIndex]; + if (deltas == null) return false; + var delta = deltas[vertexIndex]; + return delta != Vector3.Zero; + }, + // TODO: this will _need_ to be defined for any values that appear in morphs, i.e. geom and maybe mats + (morphIndex, vertexIndex) => + { + var value = values[vertexIndex]; + + var delta = foo[morphIndex]?[vertexIndex]; + if (delta != null) + value += delta.Value; + + return BuildSingle3(value); + } ); } @@ -73,8 +121,7 @@ public class VertexAttribute return new VertexAttribute( element, // TODO: TEMP TESTING PINNED TO BONE 0 UNTIL I SET UP BONE MAPPINGS - // (index, bytes) => WriteByteFloat4(values[index], bytes) - (index, bytes) => WriteByteFloat4(Vector4.UnitX, bytes) + index => BuildByteFloat4(Vector4.UnitX) ); } @@ -99,8 +146,7 @@ public class VertexAttribute return new VertexAttribute( element, // TODO: TEMP TESTING PINNED TO BONE 0 UNTIL I SET UP BONE MAPPINGS - // (index, bytes) => WriteUInt(values[index], bytes) - (index, bytes) => WriteUInt(Vector4.Zero, bytes) + index => BuildUInt(Vector4.Zero) ); } @@ -120,7 +166,7 @@ public class VertexAttribute return new VertexAttribute( element, - (index, bytes) => WriteHalf4(new Vector4(values[index], 0), bytes) + index => BuildHalf4(new Vector4(values[index], 0)) ); } @@ -141,18 +187,18 @@ public class VertexAttribute if (!accessors.TryGetValue("TEXCOORD_1", out var accessor2)) return new VertexAttribute( element with { Type = (byte)MdlFile.VertexType.Half2 }, - (index, bytes) => WriteHalf2(values1[index], bytes) + index => BuildHalf2(values1[index]) ); var values2 = accessor2.AsVector2Array(); return new VertexAttribute( element with { Type = (byte)MdlFile.VertexType.Half4 }, - (index, bytes) => + index => { var value1 = values1[index]; var value2 = values2[index]; - WriteHalf4(new Vector4(value1.X, value1.Y, value2.X, value2.Y), bytes); + return BuildHalf4(new Vector4(value1.X, value1.Y, value2.X, value2.Y)); } ); } @@ -173,7 +219,7 @@ public class VertexAttribute return new VertexAttribute( element, - (index, bytes) => WriteByteFloat4(values[index], bytes) + index => BuildByteFloat4(values[index]) ); } @@ -193,44 +239,54 @@ public class VertexAttribute return new VertexAttribute( element, - (index, bytes) => WriteByteFloat4(values[index], bytes) + index => BuildByteFloat4(values[index]) ); } - private static void WriteSingle3(Vector3 input, List bytes) + private static byte[] BuildSingle3(Vector3 input) { - bytes.AddRange(BitConverter.GetBytes(input.X)); - bytes.AddRange(BitConverter.GetBytes(input.Y)); - bytes.AddRange(BitConverter.GetBytes(input.Z)); + return [ + ..BitConverter.GetBytes(input.X), + ..BitConverter.GetBytes(input.Y), + ..BitConverter.GetBytes(input.Z), + ]; } - private static void WriteUInt(Vector4 input, List bytes) + private static byte[] BuildUInt(Vector4 input) { - bytes.Add((byte)input.X); - bytes.Add((byte)input.Y); - bytes.Add((byte)input.Z); - bytes.Add((byte)input.W); + return [ + (byte)input.X, + (byte)input.Y, + (byte)input.Z, + (byte)input.W, + ]; } - private static void WriteByteFloat4(Vector4 input, List bytes) + private static byte[] BuildByteFloat4(Vector4 input) { - bytes.Add((byte)Math.Round(input.X * 255f)); - bytes.Add((byte)Math.Round(input.Y * 255f)); - bytes.Add((byte)Math.Round(input.Z * 255f)); - bytes.Add((byte)Math.Round(input.W * 255f)); + return [ + (byte)Math.Round(input.X * 255f), + (byte)Math.Round(input.Y * 255f), + (byte)Math.Round(input.Z * 255f), + (byte)Math.Round(input.W * 255f), + ]; } - private static void WriteHalf2(Vector2 input, List bytes) + private static byte[] BuildHalf2(Vector2 input) { - bytes.AddRange(BitConverter.GetBytes((Half)input.X)); - bytes.AddRange(BitConverter.GetBytes((Half)input.Y)); + return [ + ..BitConverter.GetBytes((Half)input.X), + ..BitConverter.GetBytes((Half)input.Y), + ]; } - private static void WriteHalf4(Vector4 input, List bytes) + private static byte[] BuildHalf4(Vector4 input) { - bytes.AddRange(BitConverter.GetBytes((Half)input.X)); - bytes.AddRange(BitConverter.GetBytes((Half)input.Y)); - bytes.AddRange(BitConverter.GetBytes((Half)input.Z)); - bytes.AddRange(BitConverter.GetBytes((Half)input.W)); + return [ + ..BitConverter.GetBytes((Half)input.X), + ..BitConverter.GetBytes((Half)input.Y), + ..BitConverter.GetBytes((Half)input.Z), + ..BitConverter.GetBytes((Half)input.W), + ]; } } diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 43630f6e..223f43ea 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -206,6 +206,9 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable 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) { @@ -214,6 +217,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var subOffset = submeshes.Count; var vertOffset = vertexBuffer.Count; var idxOffset = indices.Count; + var shapeValueOffset = shapeValues.Count; var ( vertexDeclaration, @@ -221,16 +225,18 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable xivMesh, xivSubmeshes, meshVertexBuffer, - meshIndices + meshIndices, + meshShapeData // fasdfasd ) = MeshThing(submeshnodes); vertexDeclarations.Add(vertexDeclaration); boneTables.Add(boneTable); + var meshStartIndex = (uint)(xivMesh.StartIndex + idxOffset / sizeof(ushort)); meshes.Add(xivMesh with { SubMeshIndex = (ushort)(xivMesh.SubMeshIndex + subOffset), // TODO: should probably define a type for index type hey. BoneTableIndex = (ushort)(xivMesh.BoneTableIndex + boneTableOffset), - StartIndex = (uint)(xivMesh.StartIndex + idxOffset / sizeof(ushort)), + StartIndex = meshStartIndex, VertexBufferOffset = xivMesh.VertexBufferOffset .Select(offset => (uint)(offset + vertOffset)) .ToArray(), @@ -243,6 +249,39 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable }); vertexBuffer.AddRange(meshVertexBuffer); indices.AddRange(meshIndices.SelectMany(index => BitConverter.GetBytes((ushort)index))); + foreach (var (key, (shapeMesh, meshShapeValues)) in meshShapeData) + { + List keyshapedata; + if (!shapeData.TryGetValue(key, out keyshapedata)) + { + keyshapedata = new(); + shapeData.Add(key, keyshapedata); + } + + keyshapedata.Add(shapeMesh with { + MeshIndexOffset = meshStartIndex, + ShapeValueOffset = (uint)shapeValueOffset, + }); + + shapeValues.AddRange(meshShapeValues); + } + } + + 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() @@ -292,6 +331,10 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable // 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, @@ -323,7 +366,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable MdlStructs.MeshStruct, IEnumerable, IEnumerable, - IEnumerable + IEnumerable, + IDictionary)> ) MeshThing(IEnumerable nodes) { var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() { VertexElements = Array.Empty()}; @@ -336,6 +380,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var indices = new List(); var strides = new byte[] {0, 0, 0}; var submeshes = new List(); + var morphData = new Dictionary>(); // TODO: check that attrs/elems/strides match - we should be generating per-mesh stuff for sanity's sake, but we need to make sure they match if there's >1 node mesh in a mesh. foreach (var node in nodes) @@ -343,7 +388,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var vertOff = vertexCount; var idxOff = indexCount; - var (vertDecl, newStrides, submesh, vertCount, vertStreams, idxCount, idxs) = NodeMeshThing(node); + var (vertDecl, newStrides, submesh, vertCount, vertStreams, idxCount, idxs, subMorphData) = NodeMeshThing(node); vertexDeclaration = vertDecl; // TODO: CHECK EQUAL AFTER FIRST strides = newStrides; // ALSO CHECK EQUAL vertexCount += vertCount; @@ -356,6 +401,25 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable IndexOffset = submesh.IndexOffset + idxOff // TODO: bone stuff probably }); + // TODO: HANDLE MORPHS, NEED TO ADJUST EVERY VALUE'S INDEX OFFSETS + foreach (var (key, shapeValues) in subMorphData) + { + List valueList; + if (!morphData.TryGetValue(key, out valueList)) + { + valueList = new(); + morphData.Add(key, valueList); + } + valueList.AddRange( + shapeValues + .Select(value => value with { + // but this is actually an index index + BaseIndicesIndex = (ushort)(value.BaseIndicesIndex + idxOff), + // this is a vert idx + ReplacingVertexIndex = (ushort)(value.ReplacingVertexIndex + vertOff), + }) + ); + } } // one of these per skinned mesh. @@ -390,13 +454,30 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable VertexStreamCount = (byte)(vertexDeclaration.VertexElements.Select(element => element.Stream).Max() + 1) }; + // TODO: can probably get away with flattening the values and blindly setting offsets in parent - mesh matters above, but the values are already Dealt With at this point + var shapeData = morphData.ToDictionary( + (pair) => pair.Key, + pair => ( + new MdlStructs.ShapeMeshStruct() + { + // TODO: this needs to be adjusted by the parent + MeshIndexOffset = 0, + ShapeValueCount = (uint)pair.Value.Count, + // TODO: Also update by parent + ShapeValueOffset = 0, + }, + pair.Value + ) + ); + return ( vertexDeclaration, boneTable, xivMesh, submeshes, streams[0].Concat(streams[1]).Concat(streams[2]), - indices + indices, + shapeData ); } @@ -408,7 +489,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable ushort, IEnumerable[], uint, - IEnumerable + IEnumerable, + IDictionary> ) NodeMeshThing(Node node) { // BoneTable (mesh.btidx = 255 means unskinned) @@ -423,9 +505,22 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var primitive = mesh.Primitives[0]; var accessors = primitive.VertexAccessors; + + // var foo = primitive.GetMorphTargetAccessors(0); + // var bar = foo["POSITION"]; + // var baz = bar.AsVector3Array(); + + var morphAccessors = Enumerable.Range(0, primitive.MorphTargetsCount) + // todo: map by name, probably? or do that later (probably later) + .Select(index => primitive.GetMorphTargetAccessors(index)); + + // TODO: name + var morphChangedVerts = Enumerable.Range(0, primitive.MorphTargetsCount) + .Select(_ => new List()) + .ToArray(); var rawAttributes = new[] { - VertexAttribute.Position(accessors), + VertexAttribute.Position(accessors, morphAccessors), VertexAttribute.BlendWeight(accessors), VertexAttribute.BlendIndex(accessors), VertexAttribute.Normal(accessors), @@ -440,9 +535,12 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable { if (attribute == null) continue; var element = attribute.Element; + // recreating this here really sucks - add a "withstream" or something. attributes.Add(new VertexAttribute( element with {Offset = offsets[element.Stream]}, - attribute.Write + attribute.Build, + attribute.HasMorph, + attribute.BuildMorph )); offsets[element.Stream] += attribute.Size; } @@ -460,7 +558,18 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable { foreach (var attribute in attributes) { - attribute.Write(vertexIndex, streams[attribute.Element.Stream]); + streams[attribute.Element.Stream].AddRange(attribute.Build(vertexIndex)); + } + + // this is a meme but idk maybe it's the best approach? it's not like the attr array is ever long + foreach (var (list, morphIndex) in morphChangedVerts.WithIndex()) + { + var hasMorph = attributes.Aggregate(false, (cur, attr) => cur || attr.HasMorph(morphIndex, vertexIndex)); + // Penumbra.Log.Information($"eh? {vertexIndex} {morphIndex}: {hasMorph}"); + if (hasMorph) + { + list.Add(vertexIndex); + } } } @@ -471,6 +580,52 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable // .ToArray(); var indices = primitive.GetIndices().Select(idx => (ushort)idx).ToArray(); + // BLAH + // foreach (var (list, morphIndex) in morphChangedVerts.WithIndex()) + // { + // Penumbra.Log.Information($"morph {morphIndex}: {string.Join(",", list)}"); + // } + // TODO BUILD THE MORPH VERTS + // (source, target) + var morphmappingstuff = new List[morphChangedVerts.Length]; + foreach (var (list, morphIndex) in morphChangedVerts.WithIndex()) + { + var morphmaplist = morphmappingstuff[morphIndex] = new(); + foreach (var vertIdx in list) + { + foreach (var attribute in attributes) + { + streams[attribute.Element.Stream].AddRange(attribute.BuildMorph(morphIndex, vertIdx)); + } + + var fuck = indices.WithIndex() + .Where(pair => pair.Value == vertIdx) + .Select(pair => pair.Index); + + foreach (var something in fuck) + { + morphmaplist.Add(new MdlStructs.ShapeValueStruct(){ + BaseIndicesIndex = (ushort)something, + ReplacingVertexIndex = (ushort)vertexCount, + }); + } + vertexCount++; + } + } + + // TODO: HANDLE THIS BEING MISSING - probably warn or something, it's not the end of the world + var morphData = new Dictionary>(); + if (morphmappingstuff.Length > 0) + { + var morphnames = mesh.Extras.GetNode("targetNames").Deserialize>(); + morphData = morphmappingstuff + .Zip(morphnames) + .ToDictionary( + (pair) => pair.Second, + (pair) => pair.First + ); + } + // one of these per mesh var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() { @@ -521,7 +676,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable (ushort)vertexCount, streams, (uint)indices.Length, - indices + indices, + morphData ); } From 6641f5425bf8cfe6f12f7e433a7709c57f597031 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 5 Jan 2024 20:13:39 +1100 Subject: [PATCH 06/15] Add morph handling for normal/tangent --- .../Import/Models/Import/VertexAttribute.cs | 38 +++++++++++++++++-- Penumbra/Import/Models/ModelManager.cs | 4 +- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 9fd50513..37ccb79d 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -150,7 +150,7 @@ public class VertexAttribute ); } - public static VertexAttribute? Normal(Accessors accessors) + public static VertexAttribute? Normal(Accessors accessors, IEnumerable morphAccessors) { if (!accessors.TryGetValue("NORMAL", out var accessor)) return null; @@ -164,9 +164,24 @@ public class VertexAttribute var values = accessor.AsVector3Array(); + var foo = morphAccessors + .Select(ma => ma.GetValueOrDefault("NORMAL")?.AsVector3Array()) + .ToArray(); + return new VertexAttribute( element, - index => BuildHalf4(new Vector4(values[index], 0)) + index => BuildHalf4(new Vector4(values[index], 0)), + null, + (morphIndex, vertexIndex) => + { + var value = values[vertexIndex]; + + var delta = foo[morphIndex]?[vertexIndex]; + if (delta != null) + value += delta.Value; + + return BuildHalf4(new Vector4(value, 0)); + } ); } @@ -203,7 +218,7 @@ public class VertexAttribute ); } - public static VertexAttribute? Tangent1(Accessors accessors) + public static VertexAttribute? Tangent1(Accessors accessors, IEnumerable morphAccessors) { if (!accessors.TryGetValue("TANGENT", out var accessor)) return null; @@ -217,9 +232,24 @@ public class VertexAttribute var values = accessor.AsVector4Array(); + var foo = morphAccessors + .Select(ma => ma.GetValueOrDefault("TANGENT")?.AsVector3Array()) + .ToArray(); + return new VertexAttribute( element, - index => BuildByteFloat4(values[index]) + index => BuildByteFloat4(values[index]), + null, + (morphIndex, vertexIndex) => + { + var value = values[vertexIndex]; + + var delta = foo[morphIndex]?[vertexIndex]; + if (delta != null) + value += new Vector4(delta.Value, 0); + + return BuildByteFloat4(value); + } ); } diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 223f43ea..d849f3eb 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -523,8 +523,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable VertexAttribute.Position(accessors, morphAccessors), VertexAttribute.BlendWeight(accessors), VertexAttribute.BlendIndex(accessors), - VertexAttribute.Normal(accessors), - VertexAttribute.Tangent1(accessors), + VertexAttribute.Normal(accessors, morphAccessors), + VertexAttribute.Tangent1(accessors, morphAccessors), VertexAttribute.Color(accessors), VertexAttribute.Uv(accessors), }; From 70a09264a8eba3275412f01535b55f2471865ef3 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 5 Jan 2024 22:35:36 +1100 Subject: [PATCH 07/15] Bone table imports --- .../Import/Models/Import/VertexAttribute.cs | 19 +- Penumbra/Import/Models/ModelManager.cs | 189 ++++++++++++++---- .../ModEditWindow.Models.MdlTab.cs | 2 +- 3 files changed, 170 insertions(+), 40 deletions(-) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 37ccb79d..f2ec6f1a 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -120,13 +120,12 @@ public class VertexAttribute return new VertexAttribute( element, - // TODO: TEMP TESTING PINNED TO BONE 0 UNTIL I SET UP BONE MAPPINGS - index => BuildByteFloat4(Vector4.UnitX) + index => BuildByteFloat4(values[index]) ); } // TODO: this will need to take in a skeleton mapping of some kind so i can persist the bones used and wire up the joints correctly. hopefully by the "write vertex buffer" stage of building, we already know something about the skeleton. - public static VertexAttribute? BlendIndex(Accessors accessors) + public static VertexAttribute? BlendIndex(Accessors accessors, IDictionary? boneMap) { if (!accessors.TryGetValue("JOINTS_0", out var accessor)) return null; @@ -134,6 +133,9 @@ public class VertexAttribute if (!accessors.ContainsKey("WEIGHTS_0")) throw new Exception("Mesh contained JOINTS_0 attribute but no corresponding WEIGHTS_0 attribute."); + if (boneMap == null) + throw new Exception("Mesh contained JOINTS_0 attribute but no bone mapping was created."); + var element = new MdlStructs.VertexElement() { Stream = 0, @@ -145,8 +147,15 @@ public class VertexAttribute return new VertexAttribute( element, - // TODO: TEMP TESTING PINNED TO BONE 0 UNTIL I SET UP BONE MAPPINGS - index => BuildUInt(Vector4.Zero) + index => { + var foo = values[index]; + return BuildUInt(new Vector4( + boneMap[(ushort)foo.X], + boneMap[(ushort)foo.Y], + boneMap[(ushort)foo.Z], + boneMap[(ushort)foo.W] + )); + } ); } diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index d849f3eb..875c6071 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -189,7 +189,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable 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 => { + .SelectWhere(node => + { var name = node.Name ?? node.Mesh.Name; var match = MeshNameGroupingRegex().Match(name); return match.Success @@ -201,6 +202,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable // 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(); @@ -209,7 +211,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var shapeData = new Dictionary>(); var shapeValues = new List(); - + foreach (var submeshnodes in nodes) { var boneTableOffset = boneTables.Count; @@ -221,21 +223,52 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var ( vertexDeclaration, - boneTable, + // boneTable, xivMesh, xivSubmeshes, meshVertexBuffer, meshIndices, - meshShapeData // fasdfasd + meshShapeData, + meshBoneList ) = MeshThing(submeshnodes); + var boneTableIndex = 255; + // TODO: a better check than this would be real good + if (meshBoneList.Count() > 0) + { + var boneIndices = new List(); + foreach (var mb in meshBoneList) + { + 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(vertexDeclaration); - boneTables.Add(boneTable); var meshStartIndex = (uint)(xivMesh.StartIndex + idxOffset / sizeof(ushort)); - meshes.Add(xivMesh with { + meshes.Add(xivMesh with + { SubMeshIndex = (ushort)(xivMesh.SubMeshIndex + subOffset), // TODO: should probably define a type for index type hey. - BoneTableIndex = (ushort)(xivMesh.BoneTableIndex + boneTableOffset), + BoneTableIndex = (ushort)boneTableIndex, StartIndex = meshStartIndex, VertexBufferOffset = xivMesh.VertexBufferOffset .Select(offset => (uint)(offset + vertOffset)) @@ -243,7 +276,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable }); // TODO: could probably do this with linq cleaner foreach (var xivSubmesh in xivSubmeshes) - submeshes.Add(xivSubmesh with { + 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)) }); @@ -258,7 +292,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable shapeData.Add(key, keyshapedata); } - keyshapedata.Add(shapeMesh with { + keyshapedata.Add(shapeMesh with + { MeshIndexOffset = meshStartIndex, ShapeValueOffset = (uint)shapeValueOffset, }); @@ -347,9 +382,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable IndexDataOffset = (uint)vertexBuffer.Count, }, ], - Bones = [ - "j_kosi", - ], + Bones = bones.ToArray(), Materials = [ "/mt_c0201e6180_top_a.mtrl", ], @@ -362,15 +395,16 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable // this return type is an absolute meme, class that shit up. private ( MdlStructs.VertexDeclarationStruct, - MdlStructs.BoneTableStruct, + // MdlStructs.BoneTableStruct, MdlStructs.MeshStruct, IEnumerable, IEnumerable, IEnumerable, - IDictionary)> + IDictionary)>, + IEnumerable ) MeshThing(IEnumerable nodes) { - var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() { VertexElements = Array.Empty()}; + var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() { VertexElements = Array.Empty() }; var vertexCount = (ushort)0; // there's gotta be a better way to do this with streams or enumerables or something, surely var streams = new List[3]; @@ -378,17 +412,57 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable streams[i] = new List(); var indexCount = (uint)0; var indices = new List(); - var strides = new byte[] {0, 0, 0}; + var strides = new byte[] { 0, 0, 0 }; var submeshes = new List(); var morphData = new Dictionary>(); + /* + THOUGHTS + per submesh node, before calling down to build the mesh, build a bone mapping of joint index -> bone name (not node index) - the joint indexes are what will be used in the vertices. + per submesh node, eagerly collect all blend indexes (joints) used before building anything - just as a set or something + the above means i can create a limited set and a mapping, i.e. if skeleton contains {0->a 1->b 2->c}, and mesh contains 0, 2, then i can output [a, c] + {0->0, 2->1} + (throw if >64 entries in that name array) + + then for the second prim, + again get the joint-name mapping, and again get the joint set + then can extend the values. using the samme example, if skeleton2 contains {0->c 1->d, 2->e} and mesh contains [0,2] again, then bone array can be extended to [a, c, e] and the mesh-specific mapping would be {0->1, 2->2} + + repeat, etc + */ + + var usedBones = new List(); + // TODO: check that attrs/elems/strides match - we should be generating per-mesh stuff for sanity's sake, but we need to make sure they match if there's >1 node mesh in a mesh. foreach (var node in nodes) { var vertOff = vertexCount; var idxOff = indexCount; - var (vertDecl, newStrides, submesh, vertCount, vertStreams, idxCount, idxs, subMorphData) = NodeMeshThing(node); + Dictionary? nodeBoneMap = null; + var bonething = WalkBoneThing(node); + if (bonething.HasValue) + { + var (boneNames, usedJoints) = bonething.Value; + nodeBoneMap = new(); + + // todo: probably linq this shit + foreach (var usedJoint in usedJoints) + { + // this is the 0,2 + var boneName = boneNames[usedJoint]; + var boneIdx = usedBones.IndexOf(boneName); + if (boneIdx == -1) + { + boneIdx = usedBones.Count; + usedBones.Add(boneName); + } + nodeBoneMap.Add(usedJoint, (ushort)boneIdx); + } + + Penumbra.Log.Information($"nbm {string.Join(",", nodeBoneMap.Select(kv => $"{kv.Key}:{kv.Value}"))}"); + } + + var (vertDecl, newStrides, submesh, vertCount, vertStreams, idxCount, idxs, subMorphData) = NodeMeshThing(node, nodeBoneMap); vertexDeclaration = vertDecl; // TODO: CHECK EQUAL AFTER FIRST strides = newStrides; // ALSO CHECK EQUAL vertexCount += vertCount; @@ -397,7 +471,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable indexCount += idxCount; // we need to offset the indexes to point into the new stuff indices.AddRange(idxs.Select(idx => (ushort)(idx + vertOff))); - submeshes.Add(submesh with { + submeshes.Add(submesh with + { IndexOffset = submesh.IndexOffset + idxOff // TODO: bone stuff probably }); @@ -412,7 +487,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable } valueList.AddRange( shapeValues - .Select(value => value with { + .Select(value => value with + { // but this is actually an index index BaseIndicesIndex = (ushort)(value.BaseIndicesIndex + idxOff), // this is a vert idx @@ -424,12 +500,12 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable // one of these per skinned mesh. // TODO: check if mesh has skinning at all. (err if mixed?) - var boneTable = new MdlStructs.BoneTableStruct() - { - BoneCount = 1, - // this needs to be the full 64. this should be fine _here_ with 0s because i only have one bone, but will need to be fully populated properly. in real files. - BoneIndex = new ushort[64], - }; + // var boneTable = new MdlStructs.BoneTableStruct() + // { + // BoneCount = 1, + // // this needs to be the full 64. this should be fine _here_ with 0s because i only have one bone, but will need to be fully populated properly. in real files. + // BoneIndex = new ushort[64], + // }; // mesh var xivMesh = new MdlStructs.MeshStruct() @@ -472,15 +548,59 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable return ( vertexDeclaration, - boneTable, + // boneTable, xivMesh, submeshes, streams[0].Concat(streams[1]).Concat(streams[2]), indices, - shapeData + shapeData, + usedBones ); } + private (string[], ISet)? WalkBoneThing(Node node) + { + // + if (node.Skin == null) + return null; + + var jointNames = Enumerable.Range(0, node.Skin.JointsCount) + .Select(index => node.Skin.GetJoint(index).Joint.Name ?? $"UNNAMED") + .ToArray(); + + // it might make sense to do this in the submesh handling - i do need to maintain the mesh-wide bone list, but that can be passed in/out, perhaps? + var mesh = node.Mesh; + if (mesh.Primitives.Count != 1) + throw new Exception($"Mesh has {mesh.Primitives.Count} primitives, expected 1."); + var primitive = mesh.Primitives[0]; + + var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0"); + if (jointsAccessor == null) + throw new Exception($"Skinned meshes must contain a JOINTS_0 attribute."); + + // var weightsAccssor = primitive.GetVertexAccessor("WEIGHTS_0"); + // if (weightsAccssor == null) + // throw new Exception($"Skinned meshes must contain a WEIGHTS_0 attribute."); + + var usedJoints = new HashSet(); + + // TODO: would be neat to omit any joints that are only used in 0-weight positions, but doing so would require being a _little_ smarter in vertex attrs on how to fall back when mappings aren't found - or otherwise try to ensure that mappings for unused stuff always exists + // foreach (var (joints, weights) in jointsAccessor.AsVector4Array().Zip(weightsAccssor.AsVector4Array())) + // { + // for (var index = 0; index < 4; index++) + // if (weights[index] > 0) + // usedJoints.Add((ushort)joints[index]); + // } + + foreach (var joints in jointsAccessor.AsVector4Array()) + { + for (var index = 0; index < 4; index++) + usedJoints.Add((ushort)joints[index]); + } + + return (jointNames, usedJoints); + } + private ( MdlStructs.VertexDeclarationStruct, byte[], @@ -491,7 +611,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable uint, IEnumerable, IDictionary> - ) NodeMeshThing(Node node) + ) NodeMeshThing(Node node, IDictionary? nodeBoneMap) { // BoneTable (mesh.btidx = 255 means unskinned) // vertexdecl @@ -505,7 +625,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var primitive = mesh.Primitives[0]; var accessors = primitive.VertexAccessors; - + // var foo = primitive.GetMorphTargetAccessors(0); // var bar = foo["POSITION"]; // var baz = bar.AsVector3Array(); @@ -522,7 +642,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var rawAttributes = new[] { VertexAttribute.Position(accessors, morphAccessors), VertexAttribute.BlendWeight(accessors), - VertexAttribute.BlendIndex(accessors), + VertexAttribute.BlendIndex(accessors, nodeBoneMap), VertexAttribute.Normal(accessors, morphAccessors), VertexAttribute.Tangent1(accessors, morphAccessors), VertexAttribute.Color(accessors), @@ -530,14 +650,14 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable }; var attributes = new List(); - var offsets = new byte[] {0, 0, 0}; + var offsets = new byte[] { 0, 0, 0 }; foreach (var attribute in rawAttributes) { if (attribute == null) continue; var element = attribute.Element; // recreating this here really sucks - add a "withstream" or something. attributes.Add(new VertexAttribute( - element with {Offset = offsets[element.Stream]}, + element with { Offset = offsets[element.Stream] }, attribute.Build, attribute.HasMorph, attribute.BuildMorph @@ -547,7 +667,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var strides = offsets; // TODO: when merging submeshes, i'll need to check that vert els are the same for all of them, as xiv only stores verts at the mesh level and shares them. - + var streams = new List[3]; for (var i = 0; i < 3; i++) streams[i] = new List(); @@ -604,7 +724,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable foreach (var something in fuck) { - morphmaplist.Add(new MdlStructs.ShapeValueStruct(){ + morphmaplist.Add(new MdlStructs.ShapeValueStruct() + { BaseIndicesIndex = (ushort)something, ReplacingVertexIndex = (ushort)vertexCount, }); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 5d9abda6..e030c8c0 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -81,7 +81,7 @@ public partial class ModEditWindow public void Import() { // TODO: this needs to be fleshed out a bunch. - _edit._models.ImportGltf().ContinueWith(v => Initialize(v.Result)); + _edit._models.ImportGltf().ContinueWith(v => Initialize(v.Result ?? Mdl)); } /// Export model to an interchange format. From b5b3e1b1f26184380dc49a425e7ccc74836262d0 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 6 Jan 2024 11:55:37 +1100 Subject: [PATCH 08/15] Tidy up vertex attributes --- .../Import/Models/Import/VertexAttribute.cs | 113 +++++++++--------- Penumbra/Import/Models/ModelManager.cs | 8 +- 2 files changed, 55 insertions(+), 66 deletions(-) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index f2ec6f1a..cae068db 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -13,11 +13,11 @@ public class VertexAttribute { /// XIV vertex element metadata structure. public readonly MdlStructs.VertexElement Element; - /// Write this vertex attribute's value at the specified index to the provided byte array. + /// Build a byte array containing this vertex attribute's data for the specified vertex index. public readonly BuildFn Build; - + /// Check if the specified morph target index contains a morph for the specified vertex index. public readonly HasMorphFn HasMorph; - + /// Build a byte array containing this vertex attribute's data, as modified by the specified morph target, for the specified vertex index. public readonly BuildMorphFn BuildMorph; /// Size in bytes of a single vertex's attribute value. @@ -33,7 +33,7 @@ public class VertexAttribute _ => throw new Exception($"Unhandled vertex type {(MdlFile.VertexType)Element.Type}"), }; - public VertexAttribute( + private VertexAttribute( MdlStructs.VertexElement element, BuildFn write, HasMorphFn? hasMorph = null, @@ -46,17 +46,19 @@ public class VertexAttribute BuildMorph = buildMorph ?? DefaultBuildMorph; } - // todo: this is per-shape at the moment - consider if it should do them all at once (i mean we always want to check all of them, it's mostly a semantics question on who owns the loop) - private static bool DefaultHasMorph(int morphIndex, int vertexIndex) - { - return false; - } + public VertexAttribute WithOffset(byte offset) => new VertexAttribute( + Element with { Offset = offset }, + Build, + HasMorph, + BuildMorph + ); - // xiv stores shapes as full vertex replacements, so the default value for a morph attribute is simply it's built state (rather than a delta or w/e) - private byte[] DefaultBuildMorph(int morphIndex, int vertexIndex) - { - return Build(vertexIndex); - } + // We assume that attributes don't have morph data unless explicitly configured. + private static bool DefaultHasMorph(int morphIndex, int vertexIndex) => false; + + // XIV stores shapes as full vertex replacements, so all attributes need to output something for a morph. + // As a fallback, we're just building the normal vertex data for the index. + private byte[] DefaultBuildMorph(int morphIndex, int vertexIndex) => Build(vertexIndex); public static VertexAttribute Position(Accessors accessors, IEnumerable morphAccessors) { @@ -72,27 +74,27 @@ public class VertexAttribute var values = accessor.AsVector3Array(); - var foo = morphAccessors - .Select(ma => ma.GetValueOrDefault("POSITION")?.AsVector3Array()) + var morphValues = morphAccessors + .Select(accessors => accessors.GetValueOrDefault("POSITION")?.AsVector3Array()) .ToArray(); return new VertexAttribute( element, index => BuildSingle3(values[index]), - // TODO: at the moment this is only defined for position - is it worth setting one up for normal, too? - (morphIndex, vertexIndex) => + + hasMorph: (morphIndex, vertexIndex) => { - var deltas = foo[morphIndex]; + var deltas = morphValues[morphIndex]; if (deltas == null) return false; var delta = deltas[vertexIndex]; return delta != Vector3.Zero; }, - // TODO: this will _need_ to be defined for any values that appear in morphs, i.e. geom and maybe mats - (morphIndex, vertexIndex) => + + buildMorph: (morphIndex, vertexIndex) => { var value = values[vertexIndex]; - var delta = foo[morphIndex]?[vertexIndex]; + var delta = morphValues[morphIndex]?[vertexIndex]; if (delta != null) value += delta.Value; @@ -124,7 +126,6 @@ public class VertexAttribute ); } - // TODO: this will need to take in a skeleton mapping of some kind so i can persist the bones used and wire up the joints correctly. hopefully by the "write vertex buffer" stage of building, we already know something about the skeleton. public static VertexAttribute? BlendIndex(Accessors accessors, IDictionary? boneMap) { if (!accessors.TryGetValue("JOINTS_0", out var accessor)) @@ -147,13 +148,14 @@ public class VertexAttribute return new VertexAttribute( element, - index => { - var foo = values[index]; + index => + { + var gltfIndices = values[index]; return BuildUInt(new Vector4( - boneMap[(ushort)foo.X], - boneMap[(ushort)foo.Y], - boneMap[(ushort)foo.Z], - boneMap[(ushort)foo.W] + boneMap[(ushort)gltfIndices.X], + boneMap[(ushort)gltfIndices.Y], + boneMap[(ushort)gltfIndices.Z], + boneMap[(ushort)gltfIndices.W] )); } ); @@ -173,19 +175,19 @@ public class VertexAttribute var values = accessor.AsVector3Array(); - var foo = morphAccessors - .Select(ma => ma.GetValueOrDefault("NORMAL")?.AsVector3Array()) + var morphValues = morphAccessors + .Select(accessors => accessors.GetValueOrDefault("NORMAL")?.AsVector3Array()) .ToArray(); return new VertexAttribute( element, index => BuildHalf4(new Vector4(values[index], 0)), - null, - (morphIndex, vertexIndex) => + + buildMorph: (morphIndex, vertexIndex) => { var value = values[vertexIndex]; - var delta = foo[morphIndex]?[vertexIndex]; + var delta = morphValues[morphIndex]?[vertexIndex]; if (delta != null) value += delta.Value; @@ -208,6 +210,7 @@ public class VertexAttribute var values1 = accessor1.AsVector2Array(); + // 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 }, @@ -216,6 +219,7 @@ public class VertexAttribute 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 }, index => @@ -241,19 +245,20 @@ public class VertexAttribute var values = accessor.AsVector4Array(); - var foo = morphAccessors - .Select(ma => ma.GetValueOrDefault("TANGENT")?.AsVector3Array()) + // Per glTF specification, TANGENT morph values are stored as vec3, with the W component always considered to be 0. + var morphValues = morphAccessors + .Select(accessors => accessors.GetValueOrDefault("TANGENT")?.AsVector3Array()) .ToArray(); return new VertexAttribute( element, index => BuildByteFloat4(values[index]), - null, - (morphIndex, vertexIndex) => + + buildMorph: (morphIndex, vertexIndex) => { var value = values[vertexIndex]; - var delta = foo[morphIndex]?[vertexIndex]; + var delta = morphValues[morphIndex]?[vertexIndex]; if (delta != null) value += new Vector4(delta.Value, 0); @@ -282,50 +287,40 @@ public class VertexAttribute ); } - private static byte[] BuildSingle3(Vector3 input) - { - return [ + private static byte[] BuildSingle3(Vector3 input) => + [ ..BitConverter.GetBytes(input.X), ..BitConverter.GetBytes(input.Y), ..BitConverter.GetBytes(input.Z), ]; - } - private static byte[] BuildUInt(Vector4 input) - { - return [ + private static byte[] BuildUInt(Vector4 input) => + [ (byte)input.X, (byte)input.Y, (byte)input.Z, (byte)input.W, ]; - } - private static byte[] BuildByteFloat4(Vector4 input) - { - return [ + private static byte[] BuildByteFloat4(Vector4 input) => + [ (byte)Math.Round(input.X * 255f), (byte)Math.Round(input.Y * 255f), (byte)Math.Round(input.Z * 255f), (byte)Math.Round(input.W * 255f), ]; - } - private static byte[] BuildHalf2(Vector2 input) - { - return [ + private static byte[] BuildHalf2(Vector2 input) => + [ ..BitConverter.GetBytes((Half)input.X), ..BitConverter.GetBytes((Half)input.Y), ]; - } - private static byte[] BuildHalf4(Vector4 input) - { - return [ + 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), ]; - } } diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 875c6071..b3a1d9a1 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -655,13 +655,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable { if (attribute == null) continue; var element = attribute.Element; - // recreating this here really sucks - add a "withstream" or something. - attributes.Add(new VertexAttribute( - element with { Offset = offsets[element.Stream] }, - attribute.Build, - attribute.HasMorph, - attribute.BuildMorph - )); + attributes.Add(attribute.WithOffset(offsets[element.Stream])); offsets[element.Stream] += attribute.Size; } var strides = offsets; From 6de3077afa93b1f5491ca261c4a7279de83ec5ed Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 6 Jan 2024 16:37:41 +1100 Subject: [PATCH 09/15] Clean up submeshes --- .../Import/Models/Import/SubMeshImporter.cs | 217 ++++++++++++++++++ .../Import/Models/Import/VertexAttribute.cs | 2 + Penumbra/Import/Models/ModelManager.cs | 215 +---------------- 3 files changed, 229 insertions(+), 205 deletions(-) create mode 100644 Penumbra/Import/Models/Import/SubMeshImporter.cs diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs new file mode 100644 index 00000000..941fa1d5 --- /dev/null +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -0,0 +1,217 @@ +using Lumina.Data.Parsing; +using OtterGui; +using SharpGLTF.Schema2; + +namespace Penumbra.Import.Models.Import; + +public class SubMeshImporter +{ + public struct SubMesh + { + public MdlStructs.SubmeshStruct Struct; + + public MdlStructs.VertexDeclarationStruct VertexDeclaration; + + public ushort VertexCount; + public byte[] Strides; + public List[] Streams; + + public ushort[] Indices; + + public Dictionary> ShapeValues; + } + + public static SubMesh Import(Node node, IDictionary? nodeBoneMap) + { + var importer = new SubMeshImporter(node, nodeBoneMap); + return importer.Create(); + } + + private readonly MeshPrimitive _primitive; + private readonly IDictionary? _nodeBoneMap; + + private List? _attributes; + + private ushort _vertexCount = 0; + private byte[] _strides = [0, 0, 0]; + private readonly List[] _streams; + + private ushort[]? _indices; + + private readonly List? _morphNames; + private Dictionary>? _shapeValues; + + private SubMeshImporter(Node node, IDictionary? nodeBoneMap) + { + var mesh = node.Mesh; + + var primitiveCount = mesh.Primitives.Count; + if (primitiveCount != 1) + { + var name = node.Name ?? mesh.Name ?? "(no name)"; + throw new Exception($"Mesh \"{name}\" has {primitiveCount} primitives, expected 1."); + } + + _primitive = mesh.Primitives[0]; + _nodeBoneMap = nodeBoneMap; + + try + { + _morphNames = mesh.Extras.GetNode("targetNames").Deserialize>(); + } + catch + { + _morphNames = null; + } + + // All meshes may use up to 3 byte streams. + _streams = new List[3]; + for (var i = 0; i < 3; i++) + _streams[i] = new List(); + } + + private SubMesh Create() + { + // Build all the data we'll need. + BuildIndices(); + BuildAttributes(); + BuildVertices(); + + ArgumentNullException.ThrowIfNull(_indices); + ArgumentNullException.ThrowIfNull(_attributes); + ArgumentNullException.ThrowIfNull(_shapeValues); + + return new SubMesh() + { + Struct = new MdlStructs.SubmeshStruct() + { + IndexOffset = 0, + IndexCount = (uint)_indices.Length, + AttributeIndexMask = 0, + + // TODO: Flesh these out. Game doesn't seem to rely on them existing, though. + BoneStartIndex = 0, + BoneCount = 0, + }, + + VertexDeclaration = new MdlStructs.VertexDeclarationStruct() + { + VertexElements = _attributes.Select(attribute => attribute.Element).ToArray(), + }, + + VertexCount = _vertexCount, + Strides = _strides, + Streams = _streams, + + Indices = _indices, + + ShapeValues = _shapeValues, + }; + } + + private void BuildIndices() + { + _indices = _primitive.GetIndices().Select(idx => (ushort)idx).ToArray(); + } + + private void BuildAttributes() + { + var accessors = _primitive.VertexAccessors; + + var morphAccessors = Enumerable.Range(0, _primitive.MorphTargetsCount) + .Select(index => _primitive.GetMorphTargetAccessors(index)); + + // 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), + VertexAttribute.BlendWeight(accessors), + VertexAttribute.BlendIndex(accessors, _nodeBoneMap), + VertexAttribute.Normal(accessors, morphAccessors), + VertexAttribute.Tangent1(accessors, morphAccessors), + VertexAttribute.Color(accessors), + VertexAttribute.Uv(accessors), + }; + + var attributes = new List(); + 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; + } + + _attributes = attributes; + // After building the attributes, the resulting next offsets are our stream strides. + _strides = offsets; + } + + private void BuildVertices() + { + ArgumentNullException.ThrowIfNull(_attributes); + + // Lists of vertex indices that are effected by each morph target for this primitive. + var morphModifiedVertices = Enumerable.Range(0, _primitive.MorphTargetsCount) + .Select(_ => new List()) + .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 _attributes) + _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))) + .Select(pair => pair.Value); + foreach (var modifiedVertices in changedMorphs) + modifiedVertices.Add(vertexIndex); + } + + BuildShapeValues(morphModifiedVertices); + } + + private void BuildShapeValues(List[] morphModifiedVertices) + { + ArgumentNullException.ThrowIfNull(_indices); + ArgumentNullException.ThrowIfNull(_attributes); + + var morphShapeValues = new Dictionary>(); + + foreach (var (modifiedVertices, morphIndex) in morphModifiedVertices.WithIndex()) + { + // Each for a given mesh, each shape key contains a list of shape value mappings. + var shapeValues = new List(); + + foreach (var vertexIndex in modifiedVertices) + { + // Write out the morphed vertex to the vertex streams. + foreach (var attribute in _attributes) + _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)); + foreach (var targetingIndex in targetingIndices) + shapeValues.Add(new MdlStructs.ShapeValueStruct() + { + BaseIndicesIndex = (ushort)targetingIndex, + ReplacingVertexIndex = _vertexCount, + }); + + _vertexCount++; + } + + var name = _morphNames != null ? _morphNames[morphIndex] : $"unnamed_shape_{morphIndex}"; + morphShapeValues.Add(name, shapeValues); + } + + _shapeValues = morphShapeValues; + } +} diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index cae068db..6bb9971c 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -20,6 +20,8 @@ public class VertexAttribute /// Build a byte array containing this vertex attribute's data, as modified by the specified morph target, for the specified vertex index. public readonly BuildMorphFn BuildMorph; + public byte Stream => Element.Stream; + /// Size in bytes of a single vertex's attribute value. public byte Size => (MdlFile.VertexType)Element.Type switch { diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index b3a1d9a1..3153da78 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -462,22 +462,22 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable Penumbra.Log.Information($"nbm {string.Join(",", nodeBoneMap.Select(kv => $"{kv.Key}:{kv.Value}"))}"); } - var (vertDecl, newStrides, submesh, vertCount, vertStreams, idxCount, idxs, subMorphData) = NodeMeshThing(node, nodeBoneMap); - vertexDeclaration = vertDecl; // TODO: CHECK EQUAL AFTER FIRST - strides = newStrides; // ALSO CHECK EQUAL - vertexCount += vertCount; + var subMeshThingy = SubMeshImporter.Import(node, nodeBoneMap); + vertexDeclaration = subMeshThingy.VertexDeclaration; // TODO: CHECK EQUAL AFTER FIRST + strides = subMeshThingy.Strides; // ALSO CHECK EQUAL + vertexCount += subMeshThingy.VertexCount; for (var i = 0; i < 3; i++) - streams[i].AddRange(vertStreams[i]); - indexCount += idxCount; + streams[i].AddRange(subMeshThingy.Streams[i]); + indexCount += (uint)subMeshThingy.Indices.Length; // we need to offset the indexes to point into the new stuff - indices.AddRange(idxs.Select(idx => (ushort)(idx + vertOff))); - submeshes.Add(submesh with + indices.AddRange(subMeshThingy.Indices.Select(idx => (ushort)(idx + vertOff))); + submeshes.Add(subMeshThingy.Struct with { - IndexOffset = submesh.IndexOffset + idxOff + IndexOffset = subMeshThingy.Struct.IndexOffset + idxOff // TODO: bone stuff probably }); // TODO: HANDLE MORPHS, NEED TO ADJUST EVERY VALUE'S INDEX OFFSETS - foreach (var (key, shapeValues) in subMorphData) + foreach (var (key, shapeValues) in subMeshThingy.ShapeValues) { List valueList; if (!morphData.TryGetValue(key, out valueList)) @@ -601,201 +601,6 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable return (jointNames, usedJoints); } - private ( - MdlStructs.VertexDeclarationStruct, - byte[], - // MdlStructs.MeshStruct, - MdlStructs.SubmeshStruct, - ushort, - IEnumerable[], - uint, - IEnumerable, - IDictionary> - ) NodeMeshThing(Node node, IDictionary? nodeBoneMap) - { - // BoneTable (mesh.btidx = 255 means unskinned) - // vertexdecl - - var mesh = node.Mesh; - - // TODO: should probably say _what_ mesh - // TODO: would be cool to support >1 primitive (esp. given they're effectively what submeshes are modeled as), but blender doesn't really use them, so not going to prio that at all. - if (mesh.Primitives.Count != 1) - throw new Exception($"Mesh has {mesh.Primitives.Count} primitives, expected 1."); - var primitive = mesh.Primitives[0]; - - var accessors = primitive.VertexAccessors; - - // var foo = primitive.GetMorphTargetAccessors(0); - // var bar = foo["POSITION"]; - // var baz = bar.AsVector3Array(); - - var morphAccessors = Enumerable.Range(0, primitive.MorphTargetsCount) - // todo: map by name, probably? or do that later (probably later) - .Select(index => primitive.GetMorphTargetAccessors(index)); - - // TODO: name - var morphChangedVerts = Enumerable.Range(0, primitive.MorphTargetsCount) - .Select(_ => new List()) - .ToArray(); - - var rawAttributes = new[] { - VertexAttribute.Position(accessors, morphAccessors), - VertexAttribute.BlendWeight(accessors), - VertexAttribute.BlendIndex(accessors, nodeBoneMap), - VertexAttribute.Normal(accessors, morphAccessors), - VertexAttribute.Tangent1(accessors, morphAccessors), - VertexAttribute.Color(accessors), - VertexAttribute.Uv(accessors), - }; - - var attributes = new List(); - var offsets = new byte[] { 0, 0, 0 }; - foreach (var attribute in rawAttributes) - { - if (attribute == null) continue; - var element = attribute.Element; - attributes.Add(attribute.WithOffset(offsets[element.Stream])); - offsets[element.Stream] += attribute.Size; - } - var strides = offsets; - - // TODO: when merging submeshes, i'll need to check that vert els are the same for all of them, as xiv only stores verts at the mesh level and shares them. - - var streams = new List[3]; - for (var i = 0; i < 3; i++) - streams[i] = new List(); - - // todo: this is a bit lmao but also... probably the most sane option? getting the count that is - var vertexCount = primitive.VertexAccessors["POSITION"].Count; - for (var vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) - { - foreach (var attribute in attributes) - { - streams[attribute.Element.Stream].AddRange(attribute.Build(vertexIndex)); - } - - // this is a meme but idk maybe it's the best approach? it's not like the attr array is ever long - foreach (var (list, morphIndex) in morphChangedVerts.WithIndex()) - { - var hasMorph = attributes.Aggregate(false, (cur, attr) => cur || attr.HasMorph(morphIndex, vertexIndex)); - // Penumbra.Log.Information($"eh? {vertexIndex} {morphIndex}: {hasMorph}"); - if (hasMorph) - { - list.Add(vertexIndex); - } - } - } - - // indices - // var indexCount = primitive.GetIndexAccessor().Count; - // var indices = primitive.GetIndices() - // .SelectMany(index => BitConverter.GetBytes((ushort)index)) - // .ToArray(); - var indices = primitive.GetIndices().Select(idx => (ushort)idx).ToArray(); - - // BLAH - // foreach (var (list, morphIndex) in morphChangedVerts.WithIndex()) - // { - // Penumbra.Log.Information($"morph {morphIndex}: {string.Join(",", list)}"); - // } - // TODO BUILD THE MORPH VERTS - // (source, target) - var morphmappingstuff = new List[morphChangedVerts.Length]; - foreach (var (list, morphIndex) in morphChangedVerts.WithIndex()) - { - var morphmaplist = morphmappingstuff[morphIndex] = new(); - foreach (var vertIdx in list) - { - foreach (var attribute in attributes) - { - streams[attribute.Element.Stream].AddRange(attribute.BuildMorph(morphIndex, vertIdx)); - } - - var fuck = indices.WithIndex() - .Where(pair => pair.Value == vertIdx) - .Select(pair => pair.Index); - - foreach (var something in fuck) - { - morphmaplist.Add(new MdlStructs.ShapeValueStruct() - { - BaseIndicesIndex = (ushort)something, - ReplacingVertexIndex = (ushort)vertexCount, - }); - } - vertexCount++; - } - } - - // TODO: HANDLE THIS BEING MISSING - probably warn or something, it's not the end of the world - var morphData = new Dictionary>(); - if (morphmappingstuff.Length > 0) - { - var morphnames = mesh.Extras.GetNode("targetNames").Deserialize>(); - morphData = morphmappingstuff - .Zip(morphnames) - .ToDictionary( - (pair) => pair.Second, - (pair) => pair.First - ); - } - - // one of these per mesh - var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() - { - VertexElements = attributes.Select(attribute => attribute.Element).ToArray(), - }; - - // mesh - // var xivMesh = new MdlStructs.MeshStruct() - // { - // // TODO: sum across submeshes. - // // TODO: would be cool to share verts on submesh boundaries but that's way out of scope for now. - // VertexCount = (ushort)vertexCount, - // IndexCount = (uint)indexCount, - // // TODO: will have to think about how to represent this - materials can be named, so maybe adjust in parent? - // MaterialIndex = 0, - // // TODO: this will need adjusting by parent - // SubMeshIndex = 0, - // SubMeshCount = 1, - // // TODO: update in parent - // BoneTableIndex = 0, - // // TODO: this is relative to the lod's index buffer, and is an index, not byte offset - // StartIndex = 0, - // // TODO: these are relative to the lod vertex buffer. these values are accurate for a 0 offset, but lod will need to adjust - // VertexBufferOffset = [0, (uint)streams[0].Count, (uint)(streams[0].Count + streams[1].Count)], - // VertexBufferStride = strides, - // VertexStreamCount = /* 2 */ (byte)(attributes.Select(attribute => attribute.Element.Stream).Max() + 1), - // }; - - // submesh - // TODO: once we have multiple submeshes, the _first_ should probably set an index offset of 0, and then further ones delta from there - and then they can be blindly adjusted by the parent that's laying out the meshes. - var xivSubmesh = new MdlStructs.SubmeshStruct() - { - IndexOffset = 0, - IndexCount = (uint)indices.Length, - AttributeIndexMask = 0, - // TODO: not sure how i want to handle these ones - BoneStartIndex = 0, - BoneCount = 1, - }; - - // var vertexBuffer = streams[0].Concat(streams[1]).Concat(streams[2]); - - return ( - vertexDeclaration, - strides, - // xivMesh, - xivSubmesh, - (ushort)vertexCount, - streams, - (uint)indices.Length, - indices, - morphData - ); - } - public bool Equals(IAction? other) { if (other is not ImportGltfAction rhs) From 1a1c662364fb47fbc07e330bd0ccb4b1c42fa3cd Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 6 Jan 2024 20:40:30 +1100 Subject: [PATCH 10/15] Clean up meshes --- Penumbra/Import/Models/Import/MeshImporter.cs | 240 +++++++++++++++++ .../Import/Models/Import/SubMeshImporter.cs | 8 +- Penumbra/Import/Models/ModelManager.cs | 251 ++---------------- 3 files changed, 260 insertions(+), 239 deletions(-) create mode 100644 Penumbra/Import/Models/Import/MeshImporter.cs diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs new file mode 100644 index 00000000..e67b7c4e --- /dev/null +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -0,0 +1,240 @@ +using Lumina.Data.Parsing; +using SharpGLTF.Schema2; + +namespace Penumbra.Import.Models.Import; + +public class MeshImporter +{ + public struct Mesh + { + public MdlStructs.MeshStruct MeshStruct; + public List SubMeshStructs; + + public MdlStructs.VertexDeclarationStruct VertexDeclaration; + public IEnumerable VertexBuffer; + + public List Indicies; + + public List? Bones; + + public List ShapeKeys; + } + + public struct MeshShapeKey + { + public string Name; + public MdlStructs.ShapeMeshStruct ShapeMesh; + public List ShapeValues; + } + + public static Mesh Import(IEnumerable nodes) + { + var importer = new MeshImporter(nodes); + return importer.Create(); + } + + private IEnumerable _nodes; + + private List _subMeshes = new(); + + private MdlStructs.VertexDeclarationStruct? _vertexDeclaration; + private byte[]? _strides; + private ushort _vertexCount = 0; + private List[] _streams; + + private List _indices = new(); + + private List? _bones; + + private readonly Dictionary> _shapeValues = new(); + + private MeshImporter(IEnumerable nodes) + { + _nodes = nodes; + + // All meshes may use up to 3 byte streams. + _streams = new List[3]; + for (var streamIndex = 0; streamIndex < 3; streamIndex++) + _streams[streamIndex] = new List(); + } + + private Mesh Create() + { + foreach (var node in _nodes) + BuildSubMeshForNode(node); + + ArgumentNullException.ThrowIfNull(_strides); + ArgumentNullException.ThrowIfNull(_vertexDeclaration); + + return new Mesh() + { + MeshStruct = new MdlStructs.MeshStruct() + { + VertexBufferOffset = [0, (uint)_streams[0].Count, (uint)(_streams[0].Count + _streams[1].Count)], + VertexBufferStride = _strides, + VertexCount = _vertexCount, + VertexStreamCount = (byte)_vertexDeclaration.Value.VertexElements + .Select(element => element.Stream + 1) + .Max(), + + StartIndex = 0, + IndexCount = (uint)_indices.Count, + + // TODO: import material names + MaterialIndex = 0, + + SubMeshIndex = 0, + SubMeshCount = (ushort)_subMeshes.Count, + + BoneTableIndex = 0, + }, + SubMeshStructs = _subMeshes, + + VertexDeclaration = _vertexDeclaration.Value, + VertexBuffer = _streams[0].Concat(_streams[1]).Concat(_streams[2]), + + Indicies = _indices, + + Bones = _bones, + + ShapeKeys = _shapeValues + .Select(pair => new MeshShapeKey() + { + Name = pair.Key, + ShapeMesh = new MdlStructs.ShapeMeshStruct() + { + MeshIndexOffset = 0, + ShapeValueOffset = 0, + ShapeValueCount = (uint)pair.Value.Count, + }, + ShapeValues = pair.Value, + }) + .ToList(), + }; + } + + private void BuildSubMeshForNode(Node node) + { + // Record some offsets we'll be using later, before they get mutated with sub-mesh values. + var vertexOffset = _vertexCount; + var indexOffset = _indices.Count; + + var nodeBoneMap = CreateNodeBoneMap(node); + var subMesh = SubMeshImporter.Import(node, nodeBoneMap); + + var subMeshName = node.Name ?? node.Mesh.Name; + + // 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 new Exception($"Sub-mesh \"{subMeshName}\" vertex declaration mismatch. All sub-meshes of a mesh must have equivalent vertex declarations."); + + // 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 submesh return structure and computing when needed. + if (_strides == null) + _strides = subMesh.Strides; + + // Merge the sub-mesh streams into the main mesh stream bodies. + _vertexCount += subMesh.VertexCount; + + for (var streamIndex = 0; streamIndex < 3; streamIndex++) + _streams[streamIndex].AddRange(subMesh.Streams[streamIndex]); + + // As we're appending vertex data to the buffers, we need to update indices to point into that later block. + _indices.AddRange(subMesh.Indices.Select(index => (ushort)(index + vertexOffset))); + + // Merge the sub-mesh's shape values into the mesh's. + foreach (var (name, subMeshShapeValues) in subMesh.ShapeValues) + { + if (!_shapeValues.TryGetValue(name, out var meshShapeValues)) + { + meshShapeValues = new(); + _shapeValues.Add(name, meshShapeValues); + } + + meshShapeValues.AddRange(subMeshShapeValues.Select(value => value with + { + BaseIndicesIndex = (ushort)(value.BaseIndicesIndex + indexOffset), + ReplacingVertexIndex = (ushort)(value.ReplacingVertexIndex + vertexOffset), + })); + } + + // And finally, merge in the sub-mesh struct itself. + _subMeshes.Add(subMesh.SubMeshStruct with + { + IndexOffset = (ushort)(subMesh.SubMeshStruct.IndexOffset + indexOffset), + }); + } + + private 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? CreateNodeBoneMap(Node node) + { + // Unskinned assets can skip this all of this. + if (node.Skin == null) + return null; + + // Build an array of joint names, preserving the joint index from the skin. + // Any unnamed joints we'll be coalescing on a fallback bone name - though this is realistically unlikely to occur. + var jointNames = Enumerable.Range(0, node.Skin.JointsCount) + .Select(index => node.Skin.GetJoint(index).Joint.Name ?? "unnamed_joint") + .ToArray(); + + // TODO: This is duplicated with the submesh 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 new 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"); + if (jointsAccessor == null) + throw new Exception($"Skinned 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(); + 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. + if (_bones == null) + _bones = new(); + + // Build a dictionary of node-specific joint indices mesh-wide bone indices. + var nodeBoneMap = new Dictionary(); + foreach (var usedJoint in usedJoints) + { + var jointName = jointNames[usedJoint]; + var boneIndex = _bones.IndexOf(jointName); + if (boneIndex == -1) + { + boneIndex = _bones.Count; + _bones.Add(jointName); + } + nodeBoneMap.Add(usedJoint, (ushort)boneIndex); + } + + return nodeBoneMap; + } +} diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs index 941fa1d5..1d604105 100644 --- a/Penumbra/Import/Models/Import/SubMeshImporter.cs +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -8,7 +8,7 @@ public class SubMeshImporter { public struct SubMesh { - public MdlStructs.SubmeshStruct Struct; + public MdlStructs.SubmeshStruct SubMeshStruct; public MdlStructs.VertexDeclarationStruct VertexDeclaration; @@ -66,8 +66,8 @@ public class SubMeshImporter // All meshes may use up to 3 byte streams. _streams = new List[3]; - for (var i = 0; i < 3; i++) - _streams[i] = new List(); + for (var streamIndex = 0; streamIndex < 3; streamIndex++) + _streams[streamIndex] = new List(); } private SubMesh Create() @@ -83,7 +83,7 @@ public class SubMeshImporter return new SubMesh() { - Struct = new MdlStructs.SubmeshStruct() + SubMeshStruct = new MdlStructs.SubmeshStruct() { IndexOffset = 0, IndexCount = (uint)_indices.Length, diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 3153da78..65067242 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -221,23 +221,13 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var idxOffset = indices.Count; var shapeValueOffset = shapeValues.Count; - var ( - vertexDeclaration, - // boneTable, - xivMesh, - xivSubmeshes, - meshVertexBuffer, - meshIndices, - meshShapeData, - meshBoneList - ) = MeshThing(submeshnodes); + var meshthing = MeshImporter.Import(submeshnodes); var boneTableIndex = 255; - // TODO: a better check than this would be real good - if (meshBoneList.Count() > 0) + if (meshthing.Bones != null) { var boneIndices = new List(); - foreach (var mb in meshBoneList) + foreach (var mb in meshthing.Bones) { var boneIndex = bones.IndexOf(mb); if (boneIndex == -1) @@ -262,43 +252,43 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable }); } - vertexDeclarations.Add(vertexDeclaration); - var meshStartIndex = (uint)(xivMesh.StartIndex + idxOffset / sizeof(ushort)); - meshes.Add(xivMesh with + vertexDeclarations.Add(meshthing.VertexDeclaration); + var meshStartIndex = (uint)(meshthing.MeshStruct.StartIndex + idxOffset / sizeof(ushort)); + meshes.Add(meshthing.MeshStruct with { - SubMeshIndex = (ushort)(xivMesh.SubMeshIndex + subOffset), + SubMeshIndex = (ushort)(meshthing.MeshStruct.SubMeshIndex + subOffset), // TODO: should probably define a type for index type hey. BoneTableIndex = (ushort)boneTableIndex, StartIndex = meshStartIndex, - VertexBufferOffset = xivMesh.VertexBufferOffset + VertexBufferOffset = meshthing.MeshStruct.VertexBufferOffset .Select(offset => (uint)(offset + vertOffset)) .ToArray(), }); // TODO: could probably do this with linq cleaner - foreach (var xivSubmesh in xivSubmeshes) + 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(meshVertexBuffer); - indices.AddRange(meshIndices.SelectMany(index => BitConverter.GetBytes((ushort)index))); - foreach (var (key, (shapeMesh, meshShapeValues)) in meshShapeData) + 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(key, out keyshapedata)) + if (!shapeData.TryGetValue(shapeKey.Name, out keyshapedata)) { keyshapedata = new(); - shapeData.Add(key, keyshapedata); + shapeData.Add(shapeKey.Name, keyshapedata); } - keyshapedata.Add(shapeMesh with + keyshapedata.Add(shapeKey.ShapeMesh with { MeshIndexOffset = meshStartIndex, ShapeValueOffset = (uint)shapeValueOffset, }); - shapeValues.AddRange(meshShapeValues); + shapeValues.AddRange(shapeKey.ShapeValues); } } @@ -392,215 +382,6 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable Out = mdl; } - // this return type is an absolute meme, class that shit up. - private ( - MdlStructs.VertexDeclarationStruct, - // MdlStructs.BoneTableStruct, - MdlStructs.MeshStruct, - IEnumerable, - IEnumerable, - IEnumerable, - IDictionary)>, - IEnumerable - ) MeshThing(IEnumerable nodes) - { - var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() { VertexElements = Array.Empty() }; - var vertexCount = (ushort)0; - // there's gotta be a better way to do this with streams or enumerables or something, surely - var streams = new List[3]; - for (var i = 0; i < 3; i++) - streams[i] = new List(); - var indexCount = (uint)0; - var indices = new List(); - var strides = new byte[] { 0, 0, 0 }; - var submeshes = new List(); - var morphData = new Dictionary>(); - - /* - THOUGHTS - per submesh node, before calling down to build the mesh, build a bone mapping of joint index -> bone name (not node index) - the joint indexes are what will be used in the vertices. - per submesh node, eagerly collect all blend indexes (joints) used before building anything - just as a set or something - the above means i can create a limited set and a mapping, i.e. if skeleton contains {0->a 1->b 2->c}, and mesh contains 0, 2, then i can output [a, c] + {0->0, 2->1} - (throw if >64 entries in that name array) - - then for the second prim, - again get the joint-name mapping, and again get the joint set - then can extend the values. using the samme example, if skeleton2 contains {0->c 1->d, 2->e} and mesh contains [0,2] again, then bone array can be extended to [a, c, e] and the mesh-specific mapping would be {0->1, 2->2} - - repeat, etc - */ - - var usedBones = new List(); - - // TODO: check that attrs/elems/strides match - we should be generating per-mesh stuff for sanity's sake, but we need to make sure they match if there's >1 node mesh in a mesh. - foreach (var node in nodes) - { - var vertOff = vertexCount; - var idxOff = indexCount; - - Dictionary? nodeBoneMap = null; - var bonething = WalkBoneThing(node); - if (bonething.HasValue) - { - var (boneNames, usedJoints) = bonething.Value; - nodeBoneMap = new(); - - // todo: probably linq this shit - foreach (var usedJoint in usedJoints) - { - // this is the 0,2 - var boneName = boneNames[usedJoint]; - var boneIdx = usedBones.IndexOf(boneName); - if (boneIdx == -1) - { - boneIdx = usedBones.Count; - usedBones.Add(boneName); - } - nodeBoneMap.Add(usedJoint, (ushort)boneIdx); - } - - Penumbra.Log.Information($"nbm {string.Join(",", nodeBoneMap.Select(kv => $"{kv.Key}:{kv.Value}"))}"); - } - - var subMeshThingy = SubMeshImporter.Import(node, nodeBoneMap); - vertexDeclaration = subMeshThingy.VertexDeclaration; // TODO: CHECK EQUAL AFTER FIRST - strides = subMeshThingy.Strides; // ALSO CHECK EQUAL - vertexCount += subMeshThingy.VertexCount; - for (var i = 0; i < 3; i++) - streams[i].AddRange(subMeshThingy.Streams[i]); - indexCount += (uint)subMeshThingy.Indices.Length; - // we need to offset the indexes to point into the new stuff - indices.AddRange(subMeshThingy.Indices.Select(idx => (ushort)(idx + vertOff))); - submeshes.Add(subMeshThingy.Struct with - { - IndexOffset = subMeshThingy.Struct.IndexOffset + idxOff - // TODO: bone stuff probably - }); - // TODO: HANDLE MORPHS, NEED TO ADJUST EVERY VALUE'S INDEX OFFSETS - foreach (var (key, shapeValues) in subMeshThingy.ShapeValues) - { - List valueList; - if (!morphData.TryGetValue(key, out valueList)) - { - valueList = new(); - morphData.Add(key, valueList); - } - valueList.AddRange( - shapeValues - .Select(value => value with - { - // but this is actually an index index - BaseIndicesIndex = (ushort)(value.BaseIndicesIndex + idxOff), - // this is a vert idx - ReplacingVertexIndex = (ushort)(value.ReplacingVertexIndex + vertOff), - }) - ); - } - } - - // one of these per skinned mesh. - // TODO: check if mesh has skinning at all. (err if mixed?) - // var boneTable = new MdlStructs.BoneTableStruct() - // { - // BoneCount = 1, - // // this needs to be the full 64. this should be fine _here_ with 0s because i only have one bone, but will need to be fully populated properly. in real files. - // BoneIndex = new ushort[64], - // }; - - // mesh - var xivMesh = new MdlStructs.MeshStruct() - { - // TODO: sum across submeshes. - // TODO: would be cool to share verts on submesh boundaries but that's way out of scope for now. - VertexCount = vertexCount, - IndexCount = indexCount, - // TODO: will have to think about how to represent this - materials can be named, so maybe adjust in parent? - MaterialIndex = 0, - // TODO: this will need adjusting by parent - SubMeshIndex = 0, - SubMeshCount = (ushort)submeshes.Count, - // TODO: update in parent - BoneTableIndex = 0, - // TODO: this is relative to the lod's index buffer, and is an index, not byte offset - StartIndex = 0, - // TODO: these are relative to the lod vertex buffer. these values are accurate for a 0 offset, but lod will need to adjust - VertexBufferOffset = [0, (uint)streams[0].Count, (uint)(streams[0].Count + streams[1].Count)], - VertexBufferStride = strides, - // VertexStreamCount = /* 2 */ (byte)(attributes.Select(attribute => attribute.Element.Stream).Max() + 1), - VertexStreamCount = (byte)(vertexDeclaration.VertexElements.Select(element => element.Stream).Max() + 1) - }; - - // TODO: can probably get away with flattening the values and blindly setting offsets in parent - mesh matters above, but the values are already Dealt With at this point - var shapeData = morphData.ToDictionary( - (pair) => pair.Key, - pair => ( - new MdlStructs.ShapeMeshStruct() - { - // TODO: this needs to be adjusted by the parent - MeshIndexOffset = 0, - ShapeValueCount = (uint)pair.Value.Count, - // TODO: Also update by parent - ShapeValueOffset = 0, - }, - pair.Value - ) - ); - - return ( - vertexDeclaration, - // boneTable, - xivMesh, - submeshes, - streams[0].Concat(streams[1]).Concat(streams[2]), - indices, - shapeData, - usedBones - ); - } - - private (string[], ISet)? WalkBoneThing(Node node) - { - // - if (node.Skin == null) - return null; - - var jointNames = Enumerable.Range(0, node.Skin.JointsCount) - .Select(index => node.Skin.GetJoint(index).Joint.Name ?? $"UNNAMED") - .ToArray(); - - // it might make sense to do this in the submesh handling - i do need to maintain the mesh-wide bone list, but that can be passed in/out, perhaps? - var mesh = node.Mesh; - if (mesh.Primitives.Count != 1) - throw new Exception($"Mesh has {mesh.Primitives.Count} primitives, expected 1."); - var primitive = mesh.Primitives[0]; - - var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0"); - if (jointsAccessor == null) - throw new Exception($"Skinned meshes must contain a JOINTS_0 attribute."); - - // var weightsAccssor = primitive.GetVertexAccessor("WEIGHTS_0"); - // if (weightsAccssor == null) - // throw new Exception($"Skinned meshes must contain a WEIGHTS_0 attribute."); - - var usedJoints = new HashSet(); - - // TODO: would be neat to omit any joints that are only used in 0-weight positions, but doing so would require being a _little_ smarter in vertex attrs on how to fall back when mappings aren't found - or otherwise try to ensure that mappings for unused stuff always exists - // foreach (var (joints, weights) in jointsAccessor.AsVector4Array().Zip(weightsAccssor.AsVector4Array())) - // { - // for (var index = 0; index < 4; index++) - // if (weights[index] > 0) - // usedJoints.Add((ushort)joints[index]); - // } - - foreach (var joints in jointsAccessor.AsVector4Array()) - { - for (var index = 0; index < 4; index++) - usedJoints.Add((ushort)joints[index]); - } - - return (jointNames, usedJoints); - } - public bool Equals(IAction? other) { if (other is not ImportGltfAction rhs) From 13d594ca878c6a3cd7e12beb4ea7e8601aa5ad93 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 6 Jan 2024 23:13:34 +1100 Subject: [PATCH 11/15] 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) From b62bc44564c143f4aec0b14111c46dd18ddfa2df Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 7 Jan 2024 11:29:31 +1100 Subject: [PATCH 12/15] Clean up model import UI/wiring --- Penumbra/Import/Models/ModelManager.cs | 19 +++---- .../ModEditWindow.Models.MdlTab.cs | 35 +++++++++++-- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 49 +++++++++++++------ 3 files changed, 72 insertions(+), 31 deletions(-) diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index e77a94e3..afb92fc0 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -10,7 +10,7 @@ using SharpGLTF.Schema2; namespace Penumbra.Import.Models; -public sealed class ModelManager(IFramework framework, GamePathParser _parser) : SingleTaskQueue, IDisposable +public sealed class ModelManager(IFramework framework, GamePathParser parser) : SingleTaskQueue, IDisposable { private readonly IFramework _framework = framework; @@ -29,17 +29,17 @@ public sealed class ModelManager(IFramework framework, GamePathParser _parser) : public Task ExportToGltf(MdlFile mdl, SklbFile? sklb, string outputPath) => Enqueue(new ExportToGltfAction(this, mdl, sklb, outputPath)); - public Task ImportGltf() + public Task ImportGltf(string inputPath) { - var action = new ImportGltfAction(); - return Enqueue(action).ContinueWith(_ => action.Out!); + var action = new ImportGltfAction(inputPath); + return Enqueue(action).ContinueWith(_ => action.Out); } /// Try to find the .sklb path for a .mdl file. /// .mdl file to look up the skeleton for. public string? ResolveSklbForMdl(string mdlPath) { - var info = _parser.GetFileInfo(mdlPath); + var info = parser.GetFileInfo(mdlPath); if (info.FileType is not FileType.Model) return null; @@ -126,18 +126,13 @@ public sealed class ModelManager(IFramework framework, GamePathParser _parser) : } } - private partial class ImportGltfAction : IAction + private partial class ImportGltfAction(string inputPath) : IAction { public MdlFile? Out; - public ImportGltfAction() - { - // - } - public void Execute(CancellationToken cancel) { - var model = ModelRoot.Load("C:\\Users\\ackwell\\blender\\gltf-tests\\c0201e6180_top.gltf"); + var model = ModelRoot.Load(inputPath); Out = ModelImporter.Import(model); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 90a6645a..06196610 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -18,8 +18,9 @@ public partial class ModEditWindow public List? GamePaths { get; private set; } public int GamePathIndex; - public bool PendingIo { get; private set; } - public string? IoException { get; private set; } + private bool _dirty; + public bool PendingIo { get; private set; } + public string? IoException { get; private set; } public MdlTab(ModEditWindow edit, byte[] bytes, string path, IMod? mod) { @@ -46,6 +47,16 @@ public partial class ModEditWindow public byte[] Write() => Mdl.Write(); + public bool Dirty + { + get + { + var dirty = _dirty; + _dirty = false; + return dirty; + } + } + /// Find the list of game paths that may correspond to this model. /// Resolved path to a .mdl. /// Mod within which the .mdl is resolved. @@ -77,14 +88,28 @@ public partial class ModEditWindow }); } - public void Import() + /// Import a model from an interchange format. + /// Disk path to load model data from. + public void Import(string inputPath) { - // TODO: this needs to be fleshed out a bunch. - _edit._models.ImportGltf().ContinueWith(v => Initialize(v.Result ?? Mdl)); + PendingIo = true; + _edit._models.ImportGltf(inputPath) + .ContinueWith(task => + { + IoException = task.Exception?.ToString(); + PendingIo = false; + + if (task.IsCompletedSuccessfully && task.Result != null) + { + Initialize(task.Result); + _dirty = true; + } + }); } /// Export model to an interchange format. /// Disk path to save the resulting file to. + /// Game path to consider as the canonical .mdl path during export, used for resolution of other files. public void Export(string outputPath, Utf8GamePath mdlPath) { SklbFile? sklb = null; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 5703c882..b3598b9d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -35,15 +35,9 @@ public partial class ModEditWindow ); } - DrawExport(tab, disabled); + DrawImportExport(tab, disabled); - var ret = false; - - if (ImGui.Button("import test")) - { - tab.Import(); - ret |= true; - } + var ret = tab.Dirty; ret |= DrawModelMaterialDetails(tab, disabled); @@ -56,11 +50,41 @@ public partial class ModEditWindow return !disabled && ret; } - private void DrawExport(MdlTab tab, bool disabled) + private void DrawImportExport(MdlTab tab, bool disabled) { - if (!ImGui.CollapsingHeader("Export")) + if (!ImGui.CollapsingHeader("Import / Export")) return; + var windowWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; + var childWidth = (windowWidth - ImGui.GetStyle().ItemSpacing.X * 3) / 2; + var childSize = new Vector2(childWidth, 0); + + DrawImport(tab, childSize, disabled); + ImGui.SameLine(); + DrawExport(tab, childSize, disabled); + + if (tab.IoException != null) + ImGuiUtil.TextWrapped(tab.IoException); + } + + private void DrawImport(MdlTab tab, Vector2 size, bool disabled) + { + using var frame = ImRaii.FramedGroup("Import", size); + + 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) => + { + if (success && paths.Count > 0) + tab.Import(paths[0]); + }, 1, _mod!.ModPath.FullName, false); + } + } + + private void DrawExport(MdlTab tab, Vector2 size, bool disabled) + { + using var frame = ImRaii.FramedGroup("Export", size); + if (tab.GamePaths == null) { if (tab.IoException == null) @@ -89,9 +113,6 @@ public partial class ModEditWindow _mod!.ModPath.FullName, false ); - - if (tab.IoException != null) - ImGuiUtil.TextWrapped(tab.IoException); } private void DrawGamePathCombo(MdlTab tab) @@ -116,7 +137,7 @@ public partial class ModEditWindow const string label = "Game Path"; var preview = tab.GamePaths![tab.GamePathIndex].ToString(); var labelWidth = ImGui.CalcTextSize(label).X + ImGui.GetStyle().ItemInnerSpacing.X; - var buttonWidth = ImGui.GetContentRegionAvail().X - labelWidth; + var buttonWidth = ImGui.GetContentRegionAvail().X - labelWidth - ImGui.GetStyle().ItemSpacing.X; if (tab.GamePaths!.Count == 1) { using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); From a2b92f129656a8212312e694cdbbc5e819bc3eb5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 7 Jan 2024 23:03:52 +0100 Subject: [PATCH 13/15] Some rework, add drag & drop. --- .../ModEditWindow.Models.MdlTab.cs | 5 +- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 51 ++++++++++++++----- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index c3fc4963..f9e19599 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -142,13 +142,12 @@ public partial class ModEditWindow .ContinueWith(task => { IoException = task.Exception?.ToString(); - PendingIo = false; - - if (task.IsCompletedSuccessfully && task.Result != null) + if (task is { IsCompletedSuccessfully: true, Result: not null }) { Initialize(task.Result); _dirty = true; } + PendingIo = false; }); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index b3598b9d..41c5591a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -15,7 +15,7 @@ public partial class ModEditWindow private const int MdlMaterialMaximum = 4; private readonly FileEditor _modelTab; - private readonly ModelManager _models; + private readonly ModelManager _models; private string _modelNewMaterial = string.Empty; private readonly List _subMeshAttributeTagWidgets = []; @@ -55,9 +55,7 @@ public partial class ModEditWindow if (!ImGui.CollapsingHeader("Import / Export")) return; - var windowWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; - var childWidth = (windowWidth - ImGui.GetStyle().ItemSpacing.X * 3) / 2; - var childSize = new Vector2(childWidth, 0); + var childSize = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); DrawImport(tab, childSize, disabled); ImGui.SameLine(); @@ -67,21 +65,35 @@ public partial class ModEditWindow ImGuiUtil.TextWrapped(tab.IoException); } - private void DrawImport(MdlTab tab, Vector2 size, bool disabled) + private void DrawImport(MdlTab tab, Vector2 size, bool _1) { - using var frame = ImRaii.FramedGroup("Import", size); - - 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) => + _dragDropManager.CreateImGuiSource("ModelDragDrop", + m => m.Extensions.Any(e => ValidModelExtensions.Contains(e.ToLowerInvariant())), m => { - if (success && paths.Count > 0) - tab.Import(paths[0]); - }, 1, _mod!.ModPath.FullName, false); + if (!GetFirstModel(m.Files, out var file)) + return false; + + ImGui.TextUnformatted($"Dragging model for editing: {Path.GetFileName(file)}"); + return true; + }); + + using (var frame = ImRaii.FramedGroup("Import", size)) + { + 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) => + { + 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 file)) + tab.Import(file); } - private void DrawExport(MdlTab tab, Vector2 size, bool disabled) + private void DrawExport(MdlTab tab, Vector2 size, bool _) { using var frame = ImRaii.FramedGroup("Export", size); @@ -431,4 +443,15 @@ public partial class ModEditWindow return false; } + + private static bool GetFirstModel(IEnumerable files, [NotNullWhen(true)] out string? file) + { + file = files.FirstOrDefault(f => ValidModelExtensions.Contains(Path.GetExtension(f).ToLowerInvariant())); + return file != null; + } + + private static readonly string[] ValidModelExtensions = + [ + ".gltf", + ]; } From b0f61e6929dddd0d184da55d10d363fd23af46e7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 7 Jan 2024 23:22:48 +0100 Subject: [PATCH 14/15] Auto formatting, some cleanup, some initialization changes. --- Penumbra/Import/Models/Import/MeshImporter.cs | 122 +++++++-------- .../Import/Models/Import/ModelImporter.cs | 140 ++++++++---------- .../Import/Models/Import/SubMeshImporter.cs | 63 ++++---- .../Import/Models/Import/VertexAttribute.cs | 119 ++++++++------- Penumbra/UI/ModsTab/MultiModPanel.cs | 2 +- 5 files changed, 213 insertions(+), 233 deletions(-) diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index e67b7c4e..95eede2b 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -3,17 +3,17 @@ using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Import; -public class MeshImporter +public class MeshImporter(IEnumerable nodes) { public struct Mesh { - public MdlStructs.MeshStruct MeshStruct; + public MdlStructs.MeshStruct MeshStruct; public List SubMeshStructs; public MdlStructs.VertexDeclarationStruct VertexDeclaration; - public IEnumerable VertexBuffer; + public IEnumerable VertexBuffer; - public List Indicies; + public List Indices; public List? Bones; @@ -22,8 +22,8 @@ public class MeshImporter public struct MeshShapeKey { - public string Name; - public MdlStructs.ShapeMeshStruct ShapeMesh; + public string Name; + public MdlStructs.ShapeMeshStruct ShapeMesh; public List ShapeValues; } @@ -33,79 +33,60 @@ public class MeshImporter return importer.Create(); } - private IEnumerable _nodes; + private readonly List _subMeshes = []; - private List _subMeshes = new(); + private MdlStructs.VertexDeclarationStruct? _vertexDeclaration; + private byte[]? _strides; + private ushort _vertexCount; + private readonly List[] _streams = [[], [], []]; - private MdlStructs.VertexDeclarationStruct? _vertexDeclaration; - private byte[]? _strides; - private ushort _vertexCount = 0; - private List[] _streams; - - private List _indices = new(); + private readonly List _indices = []; private List? _bones; - private readonly Dictionary> _shapeValues = new(); - - private MeshImporter(IEnumerable nodes) - { - _nodes = nodes; - - // All meshes may use up to 3 byte streams. - _streams = new List[3]; - for (var streamIndex = 0; streamIndex < 3; streamIndex++) - _streams[streamIndex] = new List(); - } + private readonly Dictionary> _shapeValues = []; private Mesh Create() { - foreach (var node in _nodes) + foreach (var node in nodes) BuildSubMeshForNode(node); ArgumentNullException.ThrowIfNull(_strides); ArgumentNullException.ThrowIfNull(_vertexDeclaration); - return new Mesh() + return new Mesh { - MeshStruct = new MdlStructs.MeshStruct() + MeshStruct = new MdlStructs.MeshStruct { VertexBufferOffset = [0, (uint)_streams[0].Count, (uint)(_streams[0].Count + _streams[1].Count)], VertexBufferStride = _strides, - VertexCount = _vertexCount, + VertexCount = _vertexCount, VertexStreamCount = (byte)_vertexDeclaration.Value.VertexElements .Select(element => element.Stream + 1) .Max(), - StartIndex = 0, IndexCount = (uint)_indices.Count, // TODO: import material names - MaterialIndex = 0, - - SubMeshIndex = 0, - SubMeshCount = (ushort)_subMeshes.Count, - + MaterialIndex = 0, + SubMeshIndex = 0, + SubMeshCount = (ushort)_subMeshes.Count, BoneTableIndex = 0, }, - SubMeshStructs = _subMeshes, - + SubMeshStructs = _subMeshes, VertexDeclaration = _vertexDeclaration.Value, - VertexBuffer = _streams[0].Concat(_streams[1]).Concat(_streams[2]), - - Indicies = _indices, - - Bones = _bones, - + VertexBuffer = _streams[0].Concat(_streams[1]).Concat(_streams[2]), + Indices = _indices, + Bones = _bones, ShapeKeys = _shapeValues .Select(pair => new MeshShapeKey() { Name = pair.Key, ShapeMesh = new MdlStructs.ShapeMeshStruct() { - MeshIndexOffset = 0, + MeshIndexOffset = 0, ShapeValueOffset = 0, - ShapeValueCount = (uint)pair.Value.Count, + ShapeValueCount = (uint)pair.Value.Count, }, ShapeValues = pair.Value, }) @@ -117,10 +98,10 @@ public class MeshImporter { // Record some offsets we'll be using later, before they get mutated with sub-mesh values. var vertexOffset = _vertexCount; - var indexOffset = _indices.Count; + var indexOffset = _indices.Count; var nodeBoneMap = CreateNodeBoneMap(node); - var subMesh = SubMeshImporter.Import(node, nodeBoneMap); + var subMesh = SubMeshImporter.Import(node, nodeBoneMap); var subMeshName = node.Name ?? node.Mesh.Name; @@ -128,18 +109,18 @@ public class MeshImporter if (_vertexDeclaration == null) _vertexDeclaration = subMesh.VertexDeclaration; else if (VertexDeclarationMismatch(subMesh.VertexDeclaration, _vertexDeclaration.Value)) - throw new Exception($"Sub-mesh \"{subMeshName}\" vertex declaration mismatch. All sub-meshes of a mesh must have equivalent vertex declarations."); + throw new Exception( + $"Sub-mesh \"{subMeshName}\" vertex declaration mismatch. All sub-meshes of a mesh must have equivalent vertex declarations."); // 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 submesh return structure and computing when needed. - if (_strides == null) - _strides = subMesh.Strides; + // TODO: I mean, given that strides are derivable, might be worth dropping strides from the sub mesh return structure and computing when needed. + _strides ??= subMesh.Strides; // Merge the sub-mesh streams into the main mesh stream bodies. _vertexCount += subMesh.VertexCount; - for (var streamIndex = 0; streamIndex < 3; streamIndex++) - _streams[streamIndex].AddRange(subMesh.Streams[streamIndex]); + foreach (var (stream, subStream) in _streams.Zip(subMesh.Streams)) + stream.AddRange(subStream); // As we're appending vertex data to the buffers, we need to update indices to point into that later block. _indices.AddRange(subMesh.Indices.Select(index => (ushort)(index + vertexOffset))); @@ -149,7 +130,7 @@ public class MeshImporter { if (!_shapeValues.TryGetValue(name, out var meshShapeValues)) { - meshShapeValues = new(); + meshShapeValues = []; _shapeValues.Add(name, meshShapeValues); } @@ -167,19 +148,20 @@ public class MeshImporter }); } - private bool VertexDeclarationMismatch(MdlStructs.VertexDeclarationStruct a, MdlStructs.VertexDeclarationStruct b) + 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; + 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 + || pair.First.Type != pair.Second.Type + || pair.First.Offset != pair.Second.Offset + || pair.First.Stream != pair.Second.Stream ); } @@ -195,31 +177,30 @@ public class MeshImporter .Select(index => node.Skin.GetJoint(index).Joint.Name ?? "unnamed_joint") .ToArray(); - // TODO: This is duplicated with the submesh 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)"; + // 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 new 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"); - if (jointsAccessor == null) - throw new Exception($"Skinned mesh \"{meshName}\" is skinned but does not contain skinning vertex attributes."); + var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0") + ?? throw new Exception($"Skinned 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(); 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. - if (_bones == null) - _bones = new(); + _bones ??= []; // Build a dictionary of node-specific joint indices mesh-wide bone indices. var nodeBoneMap = new Dictionary(); @@ -232,6 +213,7 @@ public class MeshImporter boneIndex = _bones.Count; _bones.Add(jointName); } + nodeBoneMap.Add(usedJoint, (ushort)boneIndex); } diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs index f53d2b64..abe87934 100644 --- a/Penumbra/Import/Models/Import/ModelImporter.cs +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -4,7 +4,7 @@ using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Import; -public partial class ModelImporter +public partial class ModelImporter(ModelRoot _model) { public static MdlFile Import(ModelRoot model) { @@ -13,29 +13,22 @@ public partial class ModelImporter } // NOTE: This is intended to match TexTool's grouping regex, ".*[_ ^]([0-9]+)[\\.\\-]?([0-9]+)?$" - [GeneratedRegex(@"[_ ^](?'Mesh'[0-9]+)[.-]?(?'SubMesh'[0-9]+)?$", RegexOptions.Compiled)] + [GeneratedRegex(@"[_ ^](?'Mesh'[0-9]+)[.-]?(?'SubMesh'[0-9]+)?$", RegexOptions.Compiled | RegexOptions.NonBacktracking | RegexOptions.ExplicitCapture)] private static partial Regex MeshNameGroupingRegex(); - private readonly ModelRoot _model; + private readonly List _meshes = []; + private readonly List _subMeshes = []; - private List _meshes = new(); - private List _subMeshes = new(); + private readonly List _vertexDeclarations = []; + private readonly List _vertexBuffer = []; - private List _vertexDeclarations = new(); - private List _vertexBuffer = new(); + private readonly List _indices = []; - private List _indices = new(); + private readonly List _bones = []; + private readonly List _boneTables = []; - private List _bones = new(); - private List _boneTables = new(); - - private Dictionary> _shapeMeshes = new(); - private List _shapeValues = new(); - - private ModelImporter(ModelRoot model) - { - _model = model; - } + private readonly Dictionary> _shapeMeshes = []; + private readonly List _shapeValues = []; private MdlFile Create() { @@ -43,8 +36,8 @@ public partial class ModelImporter 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(); + // Now that all 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) { @@ -53,75 +46,64 @@ public partial class ModelImporter ShapeName = keyName, // NOTE: these values are per-LoD. ShapeMeshStartIndex = [(ushort)shapeMeshes.Count, 0, 0], - ShapeMeshCount = [(ushort)keyMeshes.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() + 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(), + VertexOffset = [0, 0, 0], + VertexBufferSize = [(uint)_vertexBuffer.Count, 0, 0], + IndexOffset = [(uint)_vertexBuffer.Count, 0, 0], + IndexBufferSize = [(uint)indexBuffer.Length, 0, 0], + VertexDeclarations = [.. _vertexDeclarations], + Meshes = [.. _meshes], + SubMeshes = [.. _subMeshes], + BoneTables = [.. _boneTables], + Bones = [.. _bones], // 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, - }], + Shapes = [.. shapes], + ShapeMeshes = [.. shapeMeshes], + ShapeValues = [.. _shapeValues], + 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], + Radius = 1, + BoundingBoxes = MdlFile.EmptyBoundingBox, + BoneBoundingBoxes = Enumerable.Repeat(MdlFile.EmptyBoundingBox, _bones.Count).ToArray(), + RemainingData = [.._vertexBuffer, ..indexBuffer], }; } /// Returns an iterator over sorted, grouped mesh nodes. - private IEnumerable> GroupedMeshNodes() => - _model.LogicalNodes + private IEnumerable> GroupedMeshNodes() + => _model.LogicalNodes .Where(node => node.Mesh != null) - .Select(node => + .Select(node => { - var name = node.Name ?? node.Mesh.Name ?? "NOMATCH"; + var name = node.Name ?? node.Mesh.Name ?? "NOMATCH"; var match = MeshNameGroupingRegex().Match(name); return (node, match); }) @@ -140,12 +122,12 @@ public partial class ModelImporter 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 subMeshOffset = _subMeshes.Count; + var vertexOffset = _vertexBuffer.Count; + var indexOffset = _indices.Count; var shapeValueOffset = _shapeValues.Count; - var mesh = MeshImporter.Import(subMeshNodes); + 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. @@ -163,22 +145,21 @@ public partial class ModelImporter .ToArray(), }); - foreach (var subMesh in mesh.SubMeshStructs) - _subMeshes.Add(subMesh with - { - IndexOffset = (uint)(subMesh.IndexOffset + indexOffset), - }); + _subMeshes.AddRange(mesh.SubMeshStructs.Select(m => m with + { + IndexOffset = (uint)(m.IndexOffset + indexOffset), + })); _vertexDeclarations.Add(mesh.VertexDeclaration); _vertexBuffer.AddRange(mesh.VertexBuffer); - _indices.AddRange(mesh.Indicies); + _indices.AddRange(mesh.Indices); foreach (var meshShapeKey in mesh.ShapeKeys) { if (!_shapeMeshes.TryGetValue(meshShapeKey.Name, out var shapeMeshes)) { - shapeMeshes = new(); + shapeMeshes = []; _shapeMeshes.Add(meshShapeKey.Name, shapeMeshes); } @@ -203,6 +184,7 @@ public partial class ModelImporter boneIndex = _bones.Count; _bones.Add(boneName); } + boneIndices.Add((ushort)boneIndex); } diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs index 1d604105..5dec4384 100644 --- a/Penumbra/Import/Models/Import/SubMeshImporter.cs +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -12,8 +12,8 @@ public class SubMeshImporter public MdlStructs.VertexDeclarationStruct VertexDeclaration; - public ushort VertexCount; - public byte[] Strides; + public ushort VertexCount; + public byte[] Strides; public List[] Streams; public ushort[] Indices; @@ -27,19 +27,19 @@ public class SubMeshImporter return importer.Create(); } - private readonly MeshPrimitive _primitive; + private readonly MeshPrimitive _primitive; private readonly IDictionary? _nodeBoneMap; private List? _attributes; - private ushort _vertexCount = 0; - private byte[] _strides = [0, 0, 0]; + private ushort _vertexCount; + private byte[] _strides = [0, 0, 0]; private readonly List[] _streams; private ushort[]? _indices; - private readonly List? _morphNames; - private Dictionary>? _shapeValues; + private readonly List? _morphNames; + private Dictionary>? _shapeValues; private SubMeshImporter(Node node, IDictionary? nodeBoneMap) { @@ -52,7 +52,7 @@ public class SubMeshImporter throw new Exception($"Mesh \"{name}\" has {primitiveCount} primitives, expected 1."); } - _primitive = mesh.Primitives[0]; + _primitive = mesh.Primitives[0]; _nodeBoneMap = nodeBoneMap; try @@ -67,7 +67,7 @@ public class SubMeshImporter // All meshes may use up to 3 byte streams. _streams = new List[3]; for (var streamIndex = 0; streamIndex < 3; streamIndex++) - _streams[streamIndex] = new List(); + _streams[streamIndex] = []; } private SubMesh Create() @@ -85,26 +85,22 @@ public class SubMeshImporter { SubMeshStruct = new MdlStructs.SubmeshStruct() { - IndexOffset = 0, - IndexCount = (uint)_indices.Length, + IndexOffset = 0, + IndexCount = (uint)_indices.Length, AttributeIndexMask = 0, // TODO: Flesh these out. Game doesn't seem to rely on them existing, though. BoneStartIndex = 0, - BoneCount = 0, + BoneCount = 0, }, - VertexDeclaration = new MdlStructs.VertexDeclarationStruct() { VertexElements = _attributes.Select(attribute => attribute.Element).ToArray(), }, - VertexCount = _vertexCount, - Strides = _strides, - Streams = _streams, - - Indices = _indices, - + Strides = _strides, + Streams = _streams, + Indices = _indices, ShapeValues = _shapeValues, }; } @@ -119,11 +115,12 @@ public class SubMeshImporter var accessors = _primitive.VertexAccessors; var morphAccessors = Enumerable.Range(0, _primitive.MorphTargetsCount) - .Select(index => _primitive.GetMorphTargetAccessors(index)); + .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[] { + var rawAttributes = new[] + { VertexAttribute.Position(accessors, morphAccessors), VertexAttribute.BlendWeight(accessors), VertexAttribute.BlendIndex(accessors, _nodeBoneMap), @@ -134,10 +131,17 @@ public class SubMeshImporter }; var attributes = new List(); - var offsets = new byte[] { 0, 0, 0 }; + var offsets = new byte[] + { + 0, + 0, + 0, + }; foreach (var attribute in rawAttributes) { - if (attribute == null) continue; + if (attribute == null) + continue; + attributes.Add(attribute.WithOffset(offsets[attribute.Stream])); offsets[attribute.Stream] += attribute.Size; } @@ -177,7 +181,7 @@ public class SubMeshImporter BuildShapeValues(morphModifiedVertices); } - private void BuildShapeValues(List[] morphModifiedVertices) + private void BuildShapeValues(IEnumerable> morphModifiedVertices) { ArgumentNullException.ThrowIfNull(_indices); ArgumentNullException.ThrowIfNull(_attributes); @@ -198,12 +202,11 @@ public class SubMeshImporter // Find any indices that target this vertex index and create a mapping. var targetingIndices = _indices.WithIndex() .SelectWhere(pair => (pair.Value == vertexIndex, pair.Index)); - foreach (var targetingIndex in targetingIndices) - shapeValues.Add(new MdlStructs.ShapeValueStruct() - { - BaseIndicesIndex = (ushort)targetingIndex, - ReplacingVertexIndex = _vertexCount, - }); + shapeValues.AddRange(targetingIndices.Select(targetingIndex => new MdlStructs.ShapeValueStruct + { + BaseIndicesIndex = (ushort)targetingIndex, + ReplacingVertexIndex = _vertexCount, + })); _vertexCount++; } diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 6bb9971c..0b0e90ba 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -13,27 +13,32 @@ public class VertexAttribute { /// XIV vertex element metadata structure. public readonly MdlStructs.VertexElement Element; + /// Build a byte array containing this vertex attribute's data for the specified vertex index. public readonly BuildFn Build; + /// Check if the specified morph target index contains a morph for the specified vertex index. public readonly HasMorphFn HasMorph; + /// Build a byte array containing this vertex attribute's data, as modified by the specified morph target, for the specified vertex index. public readonly BuildMorphFn BuildMorph; - public byte Stream => Element.Stream; + public byte Stream + => Element.Stream; /// Size in bytes of a single vertex's attribute value. - 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, + 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, - _ => throw new Exception($"Unhandled vertex type {(MdlFile.VertexType)Element.Type}"), - }; + _ => throw new Exception($"Unhandled vertex type {(MdlFile.VertexType)Element.Type}"), + }; private VertexAttribute( MdlStructs.VertexElement element, @@ -42,25 +47,30 @@ public class VertexAttribute BuildMorphFn? buildMorph = null ) { - Element = element; - Build = write; - HasMorph = hasMorph ?? DefaultHasMorph; + Element = element; + Build = write; + HasMorph = hasMorph ?? DefaultHasMorph; BuildMorph = buildMorph ?? DefaultBuildMorph; } - public VertexAttribute WithOffset(byte offset) => new VertexAttribute( - Element with { Offset = offset }, - Build, - HasMorph, - BuildMorph - ); + public VertexAttribute WithOffset(byte offset) + => new( + Element with { Offset = offset }, + Build, + HasMorph, + BuildMorph + ); - // We assume that attributes don't have morph data unless explicitly configured. - private static bool DefaultHasMorph(int morphIndex, int vertexIndex) => false; + /// We assume that attributes don't have morph data unless explicitly configured. + private static bool DefaultHasMorph(int morphIndex, int vertexIndex) + => false; - // XIV stores shapes as full vertex replacements, so all attributes need to output something for a morph. - // As a fallback, we're just building the normal vertex data for the index. - private byte[] DefaultBuildMorph(int morphIndex, int vertexIndex) => Build(vertexIndex); + /// + /// XIV stores shapes as full vertex replacements, so all attributes need to output something for a morph. + /// As a fallback, we're just building the normal vertex data for the index. + /// > + private byte[] DefaultBuildMorph(int morphIndex, int vertexIndex) + => Build(vertexIndex); public static VertexAttribute Position(Accessors accessors, IEnumerable morphAccessors) { @@ -70,29 +80,29 @@ public class VertexAttribute var element = new MdlStructs.VertexElement() { Stream = 0, - Type = (byte)MdlFile.VertexType.Single3, - Usage = (byte)MdlFile.VertexUsage.Position, + Type = (byte)MdlFile.VertexType.Single3, + Usage = (byte)MdlFile.VertexUsage.Position, }; var values = accessor.AsVector3Array(); var morphValues = morphAccessors - .Select(accessors => accessors.GetValueOrDefault("POSITION")?.AsVector3Array()) + .Select(a => a.GetValueOrDefault("POSITION")?.AsVector3Array()) .ToArray(); return new VertexAttribute( element, index => BuildSingle3(values[index]), - - hasMorph: (morphIndex, vertexIndex) => + (morphIndex, vertexIndex) => { var deltas = morphValues[morphIndex]; - if (deltas == null) return false; + if (deltas == null) + return false; + var delta = deltas[vertexIndex]; return delta != Vector3.Zero; }, - - buildMorph: (morphIndex, vertexIndex) => + (morphIndex, vertexIndex) => { var value = values[vertexIndex]; @@ -116,8 +126,8 @@ public class VertexAttribute var element = new MdlStructs.VertexElement() { Stream = 0, - Type = (byte)MdlFile.VertexType.ByteFloat4, - Usage = (byte)MdlFile.VertexUsage.BlendWeights, + Type = (byte)MdlFile.VertexType.ByteFloat4, + Usage = (byte)MdlFile.VertexUsage.BlendWeights, }; var values = accessor.AsVector4Array(); @@ -142,8 +152,8 @@ public class VertexAttribute var element = new MdlStructs.VertexElement() { Stream = 0, - Type = (byte)MdlFile.VertexType.UInt, - Usage = (byte)MdlFile.VertexUsage.BlendIndices, + Type = (byte)MdlFile.VertexType.UInt, + Usage = (byte)MdlFile.VertexUsage.BlendIndices, }; var values = accessor.AsVector4Array(); @@ -171,20 +181,19 @@ public class VertexAttribute var element = new MdlStructs.VertexElement() { Stream = 1, - Type = (byte)MdlFile.VertexType.Half4, - Usage = (byte)MdlFile.VertexUsage.Normal, + Type = (byte)MdlFile.VertexType.Half4, + Usage = (byte)MdlFile.VertexUsage.Normal, }; var values = accessor.AsVector3Array(); var morphValues = morphAccessors - .Select(accessors => accessors.GetValueOrDefault("NORMAL")?.AsVector3Array()) + .Select(a => a.GetValueOrDefault("NORMAL")?.AsVector3Array()) .ToArray(); return new VertexAttribute( element, index => BuildHalf4(new Vector4(values[index], 0)), - buildMorph: (morphIndex, vertexIndex) => { var value = values[vertexIndex]; @@ -207,7 +216,7 @@ public class VertexAttribute var element = new MdlStructs.VertexElement() { Stream = 1, - Usage = (byte)MdlFile.VertexUsage.UV, + Usage = (byte)MdlFile.VertexUsage.UV, }; var values1 = accessor1.AsVector2Array(); @@ -241,21 +250,20 @@ public class VertexAttribute var element = new MdlStructs.VertexElement() { Stream = 1, - Type = (byte)MdlFile.VertexType.ByteFloat4, - Usage = (byte)MdlFile.VertexUsage.Tangent1, + Type = (byte)MdlFile.VertexType.ByteFloat4, + Usage = (byte)MdlFile.VertexUsage.Tangent1, }; var values = accessor.AsVector4Array(); // Per glTF specification, TANGENT morph values are stored as vec3, with the W component always considered to be 0. var morphValues = morphAccessors - .Select(accessors => accessors.GetValueOrDefault("TANGENT")?.AsVector3Array()) + .Select(a => a.GetValueOrDefault("TANGENT")?.AsVector3Array()) .ToArray(); return new VertexAttribute( element, index => BuildByteFloat4(values[index]), - buildMorph: (morphIndex, vertexIndex) => { var value = values[vertexIndex]; @@ -277,8 +285,8 @@ public class VertexAttribute var element = new MdlStructs.VertexElement() { Stream = 1, - Type = (byte)MdlFile.VertexType.ByteFloat4, - Usage = (byte)MdlFile.VertexUsage.Color, + Type = (byte)MdlFile.VertexType.ByteFloat4, + Usage = (byte)MdlFile.VertexUsage.Color, }; var values = accessor.AsVector4Array(); @@ -289,14 +297,16 @@ public class VertexAttribute ); } - private static byte[] BuildSingle3(Vector3 input) => + private static byte[] BuildSingle3(Vector3 input) + => [ ..BitConverter.GetBytes(input.X), ..BitConverter.GetBytes(input.Y), ..BitConverter.GetBytes(input.Z), ]; - private static byte[] BuildUInt(Vector4 input) => + private static byte[] BuildUInt(Vector4 input) + => [ (byte)input.X, (byte)input.Y, @@ -304,7 +314,8 @@ public class VertexAttribute (byte)input.W, ]; - private static byte[] BuildByteFloat4(Vector4 input) => + private static byte[] BuildByteFloat4(Vector4 input) + => [ (byte)Math.Round(input.X * 255f), (byte)Math.Round(input.Y * 255f), @@ -312,13 +323,15 @@ public class VertexAttribute (byte)Math.Round(input.W * 255f), ]; - private static byte[] BuildHalf2(Vector2 input) => + private static byte[] BuildHalf2(Vector2 input) + => [ ..BitConverter.GetBytes((Half)input.X), ..BitConverter.GetBytes((Half)input.Y), ]; - private static byte[] BuildHalf4(Vector4 input) => + private static byte[] BuildHalf4(Vector4 input) + => [ ..BitConverter.GetBytes((Half)input.X), ..BitConverter.GetBytes((Half)input.Y), diff --git a/Penumbra/UI/ModsTab/MultiModPanel.cs b/Penumbra/UI/ModsTab/MultiModPanel.cs index 1e4117ec..595240f4 100644 --- a/Penumbra/UI/ModsTab/MultiModPanel.cs +++ b/Penumbra/UI/ModsTab/MultiModPanel.cs @@ -49,7 +49,7 @@ public class MultiModPanel(ModFileSystemSelector _selector, ModDataEditor _edito ImGui.TableNextColumn(); var icon = (path is ModFileSystem.Leaf ? FontAwesomeIcon.FileCircleMinus : FontAwesomeIcon.FolderMinus).ToIconString(); if (ImGuiUtil.DrawDisabledButton(icon, new Vector2(sizeType), "Remove from selection.", false, true)) - _selector.RemovePathFromMultiselection(path); + _selector.RemovePathFromMultiSelection(path); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); From 8c7c7e20a0626ebda47889d661f12019a57acee1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 7 Jan 2024 23:23:25 +0100 Subject: [PATCH 15/15] Update OtterGui --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 2c603cea..df754445 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 2c603cea9b1d4dd500e30972b64bd2f25012dc4c +Subproject commit df754445aa6f67fbeb84a292fe808ee560bc3cf7