diff --git a/Penumbra/Import/Models/MeshConverter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs similarity index 84% rename from Penumbra/Import/Models/MeshConverter.cs rename to Penumbra/Import/Models/Export/MeshExporter.cs index 97121798..fdfa59e4 100644 --- a/Penumbra/Import/Models/MeshConverter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -6,15 +6,39 @@ using SharpGLTF.Geometry; using SharpGLTF.Geometry.VertexTypes; using SharpGLTF.IO; using SharpGLTF.Materials; +using SharpGLTF.Scenes; -namespace Penumbra.Import.Modules; +namespace Penumbra.Import.Models.Export; -public sealed class MeshConverter +public class MeshExporter { - public static IMeshBuilder[] ToGltf(MdlFile mdl, byte lod, ushort meshIndex, Dictionary? boneNameMap) + public class Mesh { - var self = new MeshConverter(mdl, lod, meshIndex, boneNameMap); - return self.BuildMesh(); + private IMeshBuilder[] _meshes; + private NodeBuilder[]? _joints; + + public Mesh(IMeshBuilder[] meshes, NodeBuilder[]? joints) + { + _meshes = meshes; + _joints = joints; + } + + public void AddToScene(SceneBuilder scene) + { + // TODO: throw if mesh has skinned vertices but no joints are available? + foreach (var mesh in _meshes) + if (_joints == null) + scene.AddRigidMesh(mesh, Matrix4x4.Identity); + else + scene.AddSkinnedMesh(mesh, Matrix4x4.Identity, _joints); + } + } + + // TODO: replace bonenamemap with a gltfskeleton + public static Mesh Export(MdlFile mdl, byte lod, ushort meshIndex, GltfSkeleton? skeleton) + { + var self = new MeshExporter(mdl, lod, meshIndex, skeleton?.Names); + return new Mesh(self.BuildMeshes(), skeleton?.Joints); } private const byte MaximumMeshBufferStreams = 3; @@ -22,14 +46,14 @@ public sealed class MeshConverter private readonly MdlFile _mdl; private readonly byte _lod; private readonly ushort _meshIndex; - private MdlStructs.MeshStruct Mesh => _mdl.Meshes[_meshIndex]; + private MdlStructs.MeshStruct XivMesh => _mdl.Meshes[_meshIndex]; private readonly Dictionary? _boneIndexMap; private readonly Type _geometryType; private readonly Type _skinningType; - private MeshConverter(MdlFile mdl, byte lod, ushort meshIndex, Dictionary? boneNameMap) + private MeshExporter(MdlFile mdl, byte lod, ushort meshIndex, Dictionary? boneNameMap) { _mdl = mdl; _lod = lod; @@ -49,7 +73,7 @@ public sealed class MeshConverter private Dictionary BuildBoneIndexMap(Dictionary boneNameMap) { // todo: BoneTableIndex of 255 means null? if so, it should probably feed into the attributes we assign... - var xivBoneTable = _mdl.BoneTables[Mesh.BoneTableIndex]; + var xivBoneTable = _mdl.BoneTables[XivMesh.BoneTableIndex]; var indexMap = new Dictionary(); @@ -66,16 +90,16 @@ public sealed class MeshConverter return indexMap; } - private IMeshBuilder[] BuildMesh() + private IMeshBuilder[] BuildMeshes() { var indices = BuildIndices(); var vertices = BuildVertices(); - // TODO: handle submeshCount = 0 + // TODO: handle SubMeshCount = 0 return _mdl.SubMeshes - .Skip(Mesh.SubMeshIndex) - .Take(Mesh.SubMeshCount) + .Skip(XivMesh.SubMeshIndex) + .Take(XivMesh.SubMeshCount) .Select(submesh => BuildSubMesh(submesh, indices, vertices)) .ToArray(); } @@ -83,7 +107,7 @@ public sealed class MeshConverter private IMeshBuilder BuildSubMesh(MdlStructs.SubmeshStruct submesh, IReadOnlyList indices, IReadOnlyList vertices) { // Index indices are specified relative to the LOD's 0, but we're reading chunks for each mesh. - var startIndex = (int)(submesh.IndexOffset - Mesh.StartIndex); + var startIndex = (int)(submesh.IndexOffset - XivMesh.StartIndex); var meshBuilderType = typeof(MeshBuilder<,,,>).MakeGenericType( typeof(MaterialBuilder), @@ -124,7 +148,7 @@ public sealed class MeshConverter var shapeValues = _mdl.ShapeMeshes .Skip(shape.ShapeMeshStartIndex[_lod]) .Take(shape.ShapeMeshCount[_lod]) - .Where(shapeMesh => shapeMesh.MeshIndexOffset == Mesh.StartIndex) + .Where(shapeMesh => shapeMesh.MeshIndexOffset == XivMesh.StartIndex) .SelectMany(shapeMesh => _mdl.ShapeValues .Skip((int)shapeMesh.ShapeValueOffset) @@ -158,8 +182,8 @@ public sealed class MeshConverter private IReadOnlyList BuildIndices() { var reader = new BinaryReader(new MemoryStream(_mdl.RemainingData)); - reader.Seek(_mdl.IndexOffset[_lod] + Mesh.StartIndex * sizeof(ushort)); - return reader.ReadStructuresAsArray((int)Mesh.IndexCount); + reader.Seek(_mdl.IndexOffset[_lod] + XivMesh.StartIndex * sizeof(ushort)); + return reader.ReadStructuresAsArray((int)XivMesh.IndexCount); } private IReadOnlyList BuildVertices() @@ -172,7 +196,7 @@ public sealed class MeshConverter for (var streamIndex = 0; streamIndex < MaximumMeshBufferStreams; streamIndex++) { streams[streamIndex] = new BinaryReader(new MemoryStream(_mdl.RemainingData)); - streams[streamIndex].Seek(_mdl.VertexOffset[_lod] + Mesh.VertexBufferOffset[streamIndex]); + streams[streamIndex].Seek(_mdl.VertexOffset[_lod] + XivMesh.VertexBufferOffset[streamIndex]); } var sortedElements = _mdl.VertexDeclarations[_meshIndex].VertexElements @@ -183,7 +207,7 @@ public sealed class MeshConverter var vertices = new List(); var attributes = new Dictionary(); - for (var vertexIndex = 0; vertexIndex < Mesh.VertexCount; vertexIndex++) + for (var vertexIndex = 0; vertexIndex < XivMesh.VertexCount; vertexIndex++) { attributes.Clear(); diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs new file mode 100644 index 00000000..c8716cf3 --- /dev/null +++ b/Penumbra/Import/Models/Export/ModelExporter.cs @@ -0,0 +1,100 @@ +using Penumbra.GameData.Files; +using SharpGLTF.Scenes; +using SharpGLTF.Transforms; + +namespace Penumbra.Import.Models.Export; + +public class ModelExporter +{ + public class Model + { + private List _meshes; + private GltfSkeleton? _skeleton; + + public Model(List meshes, GltfSkeleton? skeleton) + { + _meshes = meshes; + _skeleton = skeleton; + } + + public void AddToScene(SceneBuilder scene) + { + // If there's a skeleton, the root node should be added before we add any potentially skinned meshes. + var skeletonRoot = _skeleton?.Root; + if (skeletonRoot != null) + scene.AddNode(skeletonRoot); + + // Add all the meshes to the scene. + foreach (var mesh in _meshes) + mesh.AddToScene(scene); + } + } + + public static Model Export(MdlFile mdl, XivSkeleton? xivSkeleton) + { + var gltfSkeleton = xivSkeleton != null ? ConvertSkeleton(xivSkeleton) : null; + var meshes = ConvertMeshes(mdl, gltfSkeleton); + return new Model(meshes, gltfSkeleton); + } + + private static List ConvertMeshes(MdlFile mdl, GltfSkeleton? skeleton) + { + var meshes = new List(); + + for (byte lodIndex = 0; lodIndex < mdl.LodCount; lodIndex++) + { + var lod = mdl.Lods[lodIndex]; + + // TODO: consider other types of mesh? + for (ushort meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++) + { + var mesh = MeshExporter.Export(mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset), skeleton); + meshes.Add(mesh); + } + } + + return meshes; + } + + private static GltfSkeleton? ConvertSkeleton(XivSkeleton skeleton) + { + NodeBuilder? root = null; + var names = new Dictionary(); + var joints = new List(); + for (var boneIndex = 0; boneIndex < skeleton.Bones.Length; boneIndex++) + { + var bone = skeleton.Bones[boneIndex]; + + if (names.ContainsKey(bone.Name)) continue; + + var node = new NodeBuilder(bone.Name); + names[bone.Name] = joints.Count; + joints.Add(node); + + node.SetLocalTransform(new AffineTransform( + bone.Transform.Scale, + bone.Transform.Rotation, + bone.Transform.Translation + ), false); + + if (bone.ParentIndex == -1) + { + root = node; + continue; + } + + var parent = joints[names[skeleton.Bones[bone.ParentIndex].Name]]; + parent.AddNode(node); + } + + if (root == null) + return null; + + return new() + { + Root = root, + Joints = joints.ToArray(), + Names = names, + }; + } +} diff --git a/Penumbra/Import/Models/Skeleton.cs b/Penumbra/Import/Models/Export/Skeleton.cs similarity index 53% rename from Penumbra/Import/Models/Skeleton.cs rename to Penumbra/Import/Models/Export/Skeleton.cs index fb5c8284..13379dc4 100644 --- a/Penumbra/Import/Models/Skeleton.cs +++ b/Penumbra/Import/Models/Export/Skeleton.cs @@ -1,11 +1,12 @@ -namespace Penumbra.Import.Models; +using SharpGLTF.Scenes; -// TODO: this should almost certainly live in gamedata. if not, it should at _least_ be adjacent to the model handling. -public class Skeleton +namespace Penumbra.Import.Models.Export; + +public class XivSkeleton { public Bone[] Bones; - public Skeleton(Bone[] bones) + public XivSkeleton(Bone[] bones) { Bones = bones; } @@ -23,3 +24,10 @@ public class Skeleton public Vector3 Translation; } } + +public struct GltfSkeleton +{ + public NodeBuilder Root; + public NodeBuilder[] Joints; + public Dictionary Names; +} diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 2dd64235..9f72619f 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -2,7 +2,7 @@ using Dalamud.Plugin.Services; using OtterGui.Tasks; using Penumbra.Collections.Manager; using Penumbra.GameData.Files; -using Penumbra.Import.Modules; +using Penumbra.Import.Models.Export; using SharpGLTF.Scenes; using SharpGLTF.Transforms; @@ -73,36 +73,18 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable public void Execute(CancellationToken cancel) { + var xivSkeleton = BuildSkeleton(cancel); + var model = ModelExporter.Export(_mdl, xivSkeleton); + var scene = new SceneBuilder(); + model.AddToScene(scene); - var skeleton = BuildSkeleton(cancel); - if (skeleton != null) - scene.AddNode(skeleton.Value.Root); - - // TODO: group by LoD in output tree - for (byte lodIndex = 0; lodIndex < _mdl.LodCount; lodIndex++) - { - var lod = _mdl.Lods[lodIndex]; - - // TODO: consider other types of mesh? - for (ushort meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++) - { - var meshBuilders = MeshConverter.ToGltf(_mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset), skeleton?.Names); - // TODO: use a value from the mesh converter for this check, rather than assuming that it has joints - foreach (var meshBuilder in meshBuilders) - if (skeleton == null) - scene.AddRigidMesh(meshBuilder, Matrix4x4.Identity); - else - scene.AddSkinnedMesh(meshBuilder, Matrix4x4.Identity, skeleton?.Joints); - } - } - - var model = scene.ToGltf2(); - model.SaveGLTF(_outputPath); + var gltfModel = scene.ToGltf2(); + gltfModel.SaveGLTF(_outputPath); } // TODO: this should be moved to a seperate model converter or something - private (NodeBuilder Root, NodeBuilder[] Joints, Dictionary Names)? BuildSkeleton(CancellationToken cancel) + private XivSkeleton? BuildSkeleton(CancellationToken cancel) { if (_sklb == null) return null; @@ -117,40 +99,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable var skeletonConverter = new SkeletonConverter(); var skeleton = skeletonConverter.FromXml(xml); - // this is (less) atrocious - NodeBuilder? root = null; - var names = new Dictionary(); - var joints = new List(); - for (var boneIndex = 0; boneIndex < skeleton.Bones.Length; boneIndex++) - { - var bone = skeleton.Bones[boneIndex]; - - if (names.ContainsKey(bone.Name)) continue; - - var node = new NodeBuilder(bone.Name); - names[bone.Name] = joints.Count; - joints.Add(node); - - node.SetLocalTransform(new AffineTransform( - bone.Transform.Scale, - bone.Transform.Rotation, - bone.Transform.Translation - ), false); - - if (bone.ParentIndex == -1) - { - root = node; - continue; - } - - var parent = joints[names[skeleton.Bones[bone.ParentIndex].Name]]; - parent.AddNode(node); - } - - if (root == null) - return null; - - return (root, joints.ToArray(), names); + return skeleton; } public bool Equals(IAction? other) diff --git a/Penumbra/Import/Models/SkeletonConverter.cs b/Penumbra/Import/Models/SkeletonConverter.cs index d54b0294..e265e5c3 100644 --- a/Penumbra/Import/Models/SkeletonConverter.cs +++ b/Penumbra/Import/Models/SkeletonConverter.cs @@ -1,12 +1,13 @@ using System.Xml; using OtterGui; +using Penumbra.Import.Models.Export; namespace Penumbra.Import.Models; // TODO: tempted to say that this living here is more okay? that or next to havok converter, wherever that ends up. public class SkeletonConverter { - public Skeleton FromXml(string xml) + public XivSkeleton FromXml(string xml) { var document = new XmlDocument(); document.LoadXml(xml); @@ -29,7 +30,7 @@ public class SkeletonConverter .Select(values => { var (transform, parentIndex, name) = values; - return new Skeleton.Bone() + return new XivSkeleton.Bone() { Transform = transform, ParentIndex = parentIndex, @@ -38,7 +39,7 @@ public class SkeletonConverter }) .ToArray(); - return new Skeleton(bones); + return new XivSkeleton(bones); } /// Get the main skeleton ID for a given skeleton document. @@ -57,14 +58,14 @@ public class SkeletonConverter /// Read the reference pose transforms for a skeleton. /// XML node for the skeleton. - private Skeleton.Transform[] ReadReferencePose(XmlNode node) + private XivSkeleton.Transform[] ReadReferencePose(XmlNode node) { return ReadArray( CheckExists(node.SelectSingleNode("array[@name='referencePose']")), node => { var raw = ReadVec12(node); - return new Skeleton.Transform() + return new XivSkeleton.Transform() { Translation = new(raw[0], raw[1], raw[2]), Rotation = new(raw[4], raw[5], raw[6], raw[7]),