diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 1b53df8a..84628c2c 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -13,24 +13,17 @@ namespace Penumbra.Import.Models.Export; public class MeshExporter { - public class Mesh + public class Mesh(IEnumerable> meshes, NodeBuilder[]? joints) { - private IMeshBuilder[] _meshes; - private NodeBuilder[]? _joints; - - public Mesh(IMeshBuilder[] meshes, NodeBuilder[]? joints) - { - _meshes = meshes; - _joints = joints; - } - public void AddToScene(SceneBuilder scene) { - foreach (var mesh in _meshes) - if (_joints == null) + foreach (var mesh in meshes) + { + if (joints == null) scene.AddRigidMesh(mesh, Matrix4x4.Identity); else - scene.AddSkinnedMesh(mesh, Matrix4x4.Identity, _joints); + scene.AddSkinnedMesh(mesh, Matrix4x4.Identity, joints); + } } } @@ -43,9 +36,11 @@ public class MeshExporter private const byte MaximumMeshBufferStreams = 3; private readonly MdlFile _mdl; - private readonly byte _lod; - private readonly ushort _meshIndex; - private MdlStructs.MeshStruct XivMesh => _mdl.Meshes[_meshIndex]; + private readonly byte _lod; + private readonly ushort _meshIndex; + + private MdlStructs.MeshStruct XivMesh + => _mdl.Meshes[_meshIndex]; private readonly Dictionary? _boneIndexMap; @@ -53,10 +48,10 @@ public class MeshExporter private readonly Type _materialType; private readonly Type _skinningType; - private MeshExporter(MdlFile mdl, byte lod, ushort meshIndex, Dictionary? boneNameMap) + private MeshExporter(MdlFile mdl, byte lod, ushort meshIndex, IReadOnlyDictionary? boneNameMap) { - _mdl = mdl; - _lod = lod; + _mdl = mdl; + _lod = lod; _meshIndex = meshIndex; if (boneNameMap != null) @@ -76,11 +71,12 @@ public class MeshExporter if (_skinningType != typeof(VertexEmpty) && _boneIndexMap == null) Penumbra.Log.Warning($"Mesh {meshIndex} has skinned vertex usages but no bone information was provided."); - Penumbra.Log.Debug($"Mesh {meshIndex} using vertex types geometry: {_geometryType.Name}, material: {_materialType.Name}, skinning: {_skinningType.Name}"); + Penumbra.Log.Debug( + $"Mesh {meshIndex} using vertex types geometry: {_geometryType.Name}, material: {_materialType.Name}, skinning: {_skinningType.Name}"); } - /// Build a mapping between indices in this mesh's bone table (if any), and the glTF joint indices provdied. - private Dictionary? BuildBoneIndexMap(Dictionary boneNameMap) + /// Build a mapping between indices in this mesh's bone table (if any), and the glTF joint indices provided. + private Dictionary? BuildBoneIndexMap(IReadOnlyDictionary boneNameMap) { // A BoneTableIndex of 255 means that this mesh is not skinned. if (XivMesh.BoneTableIndex == 255) @@ -105,11 +101,10 @@ public class MeshExporter /// Build glTF meshes for this XIV mesh. private IMeshBuilder[] BuildMeshes() { - var indices = BuildIndices(); + var indices = BuildIndices(); var vertices = BuildVertices(); // NOTE: Index indices are specified relative to the LOD's 0, but we're reading chunks for each mesh, so we're specifying the index base relative to the mesh's base. - if (XivMesh.SubMeshCount == 0) return [BuildMesh($"mesh {_meshIndex}", indices, vertices, 0, (int)XivMesh.IndexCount)]; @@ -117,7 +112,8 @@ public class MeshExporter .Skip(XivMesh.SubMeshIndex) .Take(XivMesh.SubMeshCount) .WithIndex() - .Select(submesh => BuildMesh($"mesh {_meshIndex}.{submesh.Index}", indices, vertices, (int)(submesh.Value.IndexOffset - XivMesh.StartIndex), (int)submesh.Value.IndexCount)) + .Select(subMesh => BuildMesh($"mesh {_meshIndex}.{subMesh.Index}", indices, vertices, + (int)(subMesh.Value.IndexOffset - XivMesh.StartIndex), (int)subMesh.Value.IndexCount)) .ToArray(); } @@ -161,7 +157,7 @@ public class MeshExporter } var primitiveVertices = meshBuilder.Primitives.First().Vertices; - var shapeNames = new List(); + var shapeNames = new List(); foreach (var shape in _mdl.Shapes) { @@ -177,24 +173,28 @@ public class MeshExporter ) .Where(shapeValue => shapeValue.BaseIndicesIndex >= indexBase - && shapeValue.BaseIndicesIndex < indexBase + indexCount + && shapeValue.BaseIndicesIndex < indexBase + indexCount ) .ToList(); - if (shapeValues.Count == 0) continue; + if (shapeValues.Count == 0) + continue; var morphBuilder = meshBuilder.UseMorphTarget(shapeNames.Count); shapeNames.Add(shape.ShapeName); foreach (var shapeValue in shapeValues) + { morphBuilder.SetVertex( primitiveVertices[gltfIndices[shapeValue.BaseIndicesIndex - indexBase]].GetGeometry(), vertices[shapeValue.ReplacingVertexIndex].GetGeometry() ); + } } - meshBuilder.Extras = JsonContent.CreateFrom(new Dictionary() { - {"targetNames", shapeNames} + meshBuilder.Extras = JsonContent.CreateFrom(new Dictionary() + { + { "targetNames", shapeNames }, }); return meshBuilder; @@ -249,23 +249,25 @@ public class MeshExporter } /// Read a vertex attribute of the specified type from a vertex buffer stream. - private object ReadVertexAttribute(MdlFile.VertexType type, BinaryReader reader) + private static object ReadVertexAttribute(MdlFile.VertexType type, BinaryReader reader) { return type switch { MdlFile.VertexType.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), - MdlFile.VertexType.UInt => reader.ReadBytes(4), - MdlFile.VertexType.ByteFloat4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f), + MdlFile.VertexType.UInt => reader.ReadBytes(4), + MdlFile.VertexType.ByteFloat4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, + reader.ReadByte() / 255f), MdlFile.VertexType.Half2 => new Vector2((float)reader.ReadHalf(), (float)reader.ReadHalf()), - MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf()), + MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), + (float)reader.ReadHalf()), - _ => throw new ArgumentOutOfRangeException() + _ => throw new ArgumentOutOfRangeException(), }; } /// Get the vertex geometry type for this mesh's vertex usages. - private Type GetGeometryType(IReadOnlyDictionary usages) + private static Type GetGeometryType(IReadOnlyDictionary usages) { if (!usages.ContainsKey(MdlFile.VertexUsage.Position)) throw new Exception("Mesh does not contain position vertex elements."); @@ -304,16 +306,16 @@ public class MeshExporter } /// Get the vertex material type for this mesh's vertex usages. - private Type GetMaterialType(IReadOnlyDictionary usages) + private static Type GetMaterialType(IReadOnlyDictionary usages) { var uvCount = 0; if (usages.TryGetValue(MdlFile.VertexUsage.UV, out var type)) uvCount = type switch { - MdlFile.VertexType.Half2 => 1, - MdlFile.VertexType.Half4 => 2, + MdlFile.VertexType.Half2 => 1, + MdlFile.VertexType.Half4 => 2, MdlFile.VertexType.Single4 => 2, - _ => throw new Exception($"Unexpected UV vertex type {type}.") + _ => throw new Exception($"Unexpected UV vertex type {type}."), }; var materialUsages = ( @@ -323,11 +325,11 @@ public class MeshExporter return materialUsages switch { - (2, true) => typeof(VertexColor1Texture2), + (2, true) => typeof(VertexColor1Texture2), (2, false) => typeof(VertexTexture2), - (1, true) => typeof(VertexColor1Texture1), + (1, true) => typeof(VertexColor1Texture1), (1, false) => typeof(VertexTexture1), - (0, true) => typeof(VertexColor1), + (0, true) => typeof(VertexColor1), (0, false) => typeof(VertexEmpty), _ => throw new Exception("Unreachable."), @@ -377,7 +379,7 @@ public class MeshExporter } /// Get the vertex skinning type for this mesh's vertex usages. - private Type GetSkinningType(IReadOnlyDictionary usages) + private static Type GetSkinningType(IReadOnlyDictionary usages) { if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices)) return typeof(VertexJoints4); @@ -400,7 +402,8 @@ public class MeshExporter var weights = ToVector4(attributes[MdlFile.VertexUsage.BlendWeights]); var bindings = Enumerable.Range(0, 4) - .Select(bindingIndex => { + .Select(bindingIndex => + { // NOTE: I've not seen any files that throw this error that aren't completely broken. var xivBoneIndex = indices[bindingIndex]; if (!_boneIndexMap.TryGetValue(xivBoneIndex, out var jointIndex)) @@ -417,44 +420,44 @@ public class MeshExporter /// Clamps any tangent W value other than 1 to -1. /// Some XIV models seemingly store -1 as 0, this patches over that. - private Vector4 FixTangentVector(Vector4 tangent) + private static Vector4 FixTangentVector(Vector4 tangent) => tangent with { W = tangent.W == 1 ? 1 : -1 }; /// Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. - private Vector2 ToVector2(object data) + private static Vector2 ToVector2(object data) => data switch { Vector2 v2 => v2, Vector3 v3 => new Vector2(v3.X, v3.Y), Vector4 v4 => new Vector2(v4.X, v4.Y), - _ => throw new ArgumentOutOfRangeException($"Invalid Vector2 input {data}") + _ => throw new ArgumentOutOfRangeException($"Invalid Vector2 input {data}"), }; /// Convert a vertex attribute value to a Vector3. Supported inputs are Vector2, Vector3, and Vector4. - private Vector3 ToVector3(object data) + private static Vector3 ToVector3(object data) => data switch { Vector2 v2 => new Vector3(v2.X, v2.Y, 0), Vector3 v3 => v3, Vector4 v4 => new Vector3(v4.X, v4.Y, v4.Z), - _ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}") + _ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}"), }; /// Convert a vertex attribute value to a Vector4. Supported inputs are Vector2, Vector3, and Vector4. - private Vector4 ToVector4(object data) + private static Vector4 ToVector4(object data) => data switch { - Vector2 v2 => new Vector4(v2.X, v2.Y, 0, 0), + Vector2 v2 => new Vector4(v2.X, v2.Y, 0, 0), Vector3 v3 => new Vector4(v3.X, v3.Y, v3.Z, 1), Vector4 v4 => v4, - _ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}") + _ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}"), }; /// Convert a vertex attribute value to a byte array. - private byte[] ToByteArray(object data) + private static byte[] ToByteArray(object data) => data switch { byte[] value => value, - _ => throw new ArgumentOutOfRangeException($"Invalid byte[] input {data}") + _ => throw new ArgumentOutOfRangeException($"Invalid byte[] input {data}"), }; } diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs index 35819e7a..2060c323 100644 --- a/Penumbra/Import/Models/Export/ModelExporter.cs +++ b/Penumbra/Import/Models/Export/ModelExporter.cs @@ -6,26 +6,17 @@ namespace Penumbra.Import.Models.Export; public class ModelExporter { - public class Model + public class Model(List meshes, GltfSkeleton? skeleton) { - 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; + var skeletonRoot = skeleton?.Root; if (skeletonRoot != null) scene.AddNode(skeletonRoot); // Add all the meshes to the scene. - foreach (var mesh in _meshes) + foreach (var mesh in meshes) mesh.AddToScene(scene); } } @@ -64,10 +55,8 @@ public class ModelExporter NodeBuilder? root = null; var names = new Dictionary(); var joints = new List(); - for (var boneIndex = 0; boneIndex < skeleton.Bones.Length; boneIndex++) + foreach (var bone in skeleton.Bones) { - var bone = skeleton.Bones[boneIndex]; - if (names.ContainsKey(bone.Name)) continue; var node = new NodeBuilder(bone.Name); @@ -93,10 +82,10 @@ public class ModelExporter if (root == null) return null; - return new() + return new GltfSkeleton { Root = root, - Joints = joints.ToArray(), + Joints = [.. joints], Names = names, }; } diff --git a/Penumbra/Import/Models/Export/Skeleton.cs b/Penumbra/Import/Models/Export/Skeleton.cs index 09cdcc32..fee107a0 100644 --- a/Penumbra/Import/Models/Export/Skeleton.cs +++ b/Penumbra/Import/Models/Export/Skeleton.cs @@ -3,14 +3,9 @@ using SharpGLTF.Scenes; namespace Penumbra.Import.Models.Export; /// Representation of a skeleton within XIV. -public class XivSkeleton +public class XivSkeleton(XivSkeleton.Bone[] bones) { - public Bone[] Bones; - - public XivSkeleton(Bone[] bones) - { - Bones = bones; - } + public Bone[] Bones = bones; public struct Bone { diff --git a/Penumbra/Import/Models/HavokConverter.cs b/Penumbra/Import/Models/HavokConverter.cs index 7f87d50a..01c27b61 100644 --- a/Penumbra/Import/Models/HavokConverter.cs +++ b/Penumbra/Import/Models/HavokConverter.cs @@ -16,17 +16,18 @@ public static unsafe class HavokConverter /// A byte array representing the .hkx file. public static string HkxToXml(byte[] hkx) { + const hkSerializeUtil.SaveOptionBits options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers + | hkSerializeUtil.SaveOptionBits.TextFormat + | hkSerializeUtil.SaveOptionBits.WriteAttributes; + var tempHkx = CreateTempFile(); File.WriteAllBytes(tempHkx, hkx); var resource = Read(tempHkx); File.Delete(tempHkx); - if (resource == null) throw new Exception("Failed to read havok file."); - - var options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers - | hkSerializeUtil.SaveOptionBits.TextFormat - | hkSerializeUtil.SaveOptionBits.WriteAttributes; + if (resource == null) + throw new Exception("Failed to read havok file."); var file = Write(resource, options); file.Close(); @@ -41,17 +42,19 @@ public static unsafe class HavokConverter /// A string representing the .xml file. public static byte[] XmlToHkx(string xml) { + const hkSerializeUtil.SaveOptionBits options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers + | hkSerializeUtil.SaveOptionBits.WriteAttributes; + var tempXml = CreateTempFile(); File.WriteAllText(tempXml, xml); var resource = Read(tempXml); File.Delete(tempXml); - if (resource == null) throw new Exception("Failed to read havok file."); - - var options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers - | hkSerializeUtil.SaveOptionBits.WriteAttributes; + if (resource == null) + throw new Exception("Failed to read havok file."); + g var file = Write(resource, options); file.Close(); @@ -74,7 +77,7 @@ public static unsafe class HavokConverter var builtinTypeRegistry = hkBuiltinTypeRegistry.Instance(); var loadOptions = stackalloc hkSerializeUtil.LoadOptions[1]; - loadOptions->Flags = new() { Storage = (int)hkSerializeUtil.LoadOptionBits.Default }; + loadOptions->Flags = new hkFlags { Storage = (int)hkSerializeUtil.LoadOptionBits.Default }; loadOptions->ClassNameRegistry = builtinTypeRegistry->GetClassNameRegistry(); loadOptions->TypeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry(); @@ -92,37 +95,42 @@ public static unsafe class HavokConverter ) { var tempFile = CreateTempFile(); - var path = Marshal.StringToHGlobalAnsi(tempFile); - var oStream = new hkOstream(); + var path = Marshal.StringToHGlobalAnsi(tempFile); + var oStream = new hkOstream(); oStream.Ctor((byte*)path); var result = stackalloc hkResult[1]; var saveOptions = new hkSerializeUtil.SaveOptions() { - Flags = new() { Storage = (int)optionBits } + Flags = new hkFlags { Storage = (int)optionBits }, }; - var builtinTypeRegistry = hkBuiltinTypeRegistry.Instance(); - var classNameRegistry = builtinTypeRegistry->GetClassNameRegistry(); - var typeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry(); + var classNameRegistry = builtinTypeRegistry->GetClassNameRegistry(); + var typeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry(); try { - var name = "hkRootLevelContainer"; + const string name = "hkRootLevelContainer"; var resourcePtr = (hkRootLevelContainer*)resource->GetContentsPointer(name, typeInfoRegistry); - if (resourcePtr == null) throw new Exception("Failed to retrieve havok root level container resource."); + if (resourcePtr == null) + throw new Exception("Failed to retrieve havok root level container resource."); var hkRootLevelContainerClass = classNameRegistry->GetClassByName(name); - if (hkRootLevelContainerClass == null) throw new Exception("Failed to retrieve havok root level container type."); + if (hkRootLevelContainerClass == null) + throw new Exception("Failed to retrieve havok root level container type."); hkSerializeUtil.Save(result, resourcePtr, hkRootLevelContainerClass, oStream.StreamWriter.ptr, saveOptions); } - finally { oStream.Dtor(); } + finally + { + oStream.Dtor(); + } - if (result->Result == hkResult.hkResultEnum.Failure) throw new Exception("Failed to serialize havok file."); + if (result->Result == hkResult.hkResultEnum.Failure) + throw new Exception("Failed to serialize havok file."); return new FileStream(tempFile, FileMode.Open); } diff --git a/Penumbra/Import/Models/SkeletonConverter.cs b/Penumbra/Import/Models/SkeletonConverter.cs index 24bcf3e0..7058a159 100644 --- a/Penumbra/Import/Models/SkeletonConverter.cs +++ b/Penumbra/Import/Models/SkeletonConverter.cs @@ -15,16 +15,15 @@ public static class SkeletonConverter var mainSkeletonId = GetMainSkeletonId(document); - var skeletonNode = document.SelectSingleNode($"/hktagfile/object[@type='hkaSkeleton'][@id='{mainSkeletonId}']"); - if (skeletonNode == null) - throw new InvalidDataException($"Failed to find skeleton with id {mainSkeletonId}."); - + var skeletonNode = document.SelectSingleNode($"/hktagfile/object[@type='hkaSkeleton'][@id='{mainSkeletonId}']") + ?? throw new InvalidDataException($"Failed to find skeleton with id {mainSkeletonId}."); var referencePose = ReadReferencePose(skeletonNode); var parentIndices = ReadParentIndices(skeletonNode); - var boneNames = ReadBoneNames(skeletonNode); + var boneNames = ReadBoneNames(skeletonNode); if (boneNames.Length != parentIndices.Length || boneNames.Length != referencePose.Length) - throw new InvalidDataException($"Mismatch in bone value array lengths: names({boneNames.Length}) parents({parentIndices.Length}) pose({referencePose.Length})"); + throw new InvalidDataException( + $"Mismatch in bone value array lengths: names({boneNames.Length}) parents({parentIndices.Length}) pose({referencePose.Length})"); var bones = referencePose .Zip(parentIndices, boneNames) @@ -33,9 +32,9 @@ public static class SkeletonConverter var (transform, parentIndex, name) = values; return new XivSkeleton.Bone() { - Transform = transform, + Transform = transform, ParentIndex = parentIndex, - Name = name, + Name = name, }; }) .ToArray(); @@ -63,14 +62,14 @@ public static class SkeletonConverter { return ReadArray( CheckExists(node.SelectSingleNode("array[@name='referencePose']")), - node => + n => { - var raw = ReadVec12(node); + var raw = ReadVec12(n); return new XivSkeleton.Transform() { - Translation = new(raw[0], raw[1], raw[2]), - Rotation = new(raw[4], raw[5], raw[6], raw[7]), - Scale = new(raw[8], raw[9], raw[10]), + Translation = new Vector3(raw[0], raw[1], raw[2]), + Rotation = new Quaternion(raw[4], raw[5], raw[6], raw[7]), + Scale = new Vector3(raw[8], raw[9], raw[10]), }; } ); @@ -82,11 +81,11 @@ public static class SkeletonConverter { var array = node.ChildNodes .Cast() - .Where(node => node.NodeType != XmlNodeType.Comment) - .Select(node => + .Where(n => n.NodeType != XmlNodeType.Comment) + .Select(n => { - var text = node.InnerText.Trim()[1..]; - // TODO: surely there's a less shit way to do this i mean seriously + var text = n.InnerText.Trim()[1..]; + // TODO: surely there's a less shit way to do this I mean seriously return BitConverter.ToSingle(BitConverter.GetBytes(int.Parse(text, NumberStyles.HexNumber))); }) .ToArray(); @@ -100,24 +99,20 @@ public static class SkeletonConverter /// Read the bone parent relations for a skeleton. /// XML node for the skeleton. private static int[] ReadParentIndices(XmlNode node) - { // todo: would be neat to genericise array between bare and children - return CheckExists(node.SelectSingleNode("array[@name='parentIndices']")) + => CheckExists(node.SelectSingleNode("array[@name='parentIndices']")) .InnerText - .Split(new char[] { ' ', '\n' }, StringSplitOptions.RemoveEmptyEntries) + .Split((char[]) [' ', '\n'], StringSplitOptions.RemoveEmptyEntries) .Select(int.Parse) .ToArray(); - } /// Read the names of bones in a skeleton. /// XML node for the skeleton. private static string[] ReadBoneNames(XmlNode node) - { - return ReadArray( + => ReadArray( CheckExists(node.SelectSingleNode("array[@name='bones']")), - node => CheckExists(node.SelectSingleNode("string[@name='name']")).InnerText + n => CheckExists(n.SelectSingleNode("string[@name='name']")).InnerText ); - } /// Read an XML tagfile array, converting it via the provided conversion function. /// Tagfile XML array node. @@ -125,10 +120,9 @@ public static class SkeletonConverter private static T[] ReadArray(XmlNode node, Func convert) { var element = (XmlElement)node; + var size = int.Parse(element.GetAttribute("size")); + var array = new T[size]; - var size = int.Parse(element.GetAttribute("size")); - - var array = new T[size]; foreach (var (childNode, index) in element.ChildNodes.Cast().WithIndex()) array[index] = convert(childNode);