From b7edf521b62d205e43d82eef66c746a4b4e0f4f3 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 4 Jan 2024 19:27:04 +1100 Subject: [PATCH] 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);