Penumbra/Penumbra/Import/Models/SkeletonConverter.cs
2024-04-23 15:10:16 +02:00

137 lines
5.5 KiB
C#

using System.Xml;
using OtterGui;
using Penumbra.Import.Models.Export;
namespace Penumbra.Import.Models;
public static class SkeletonConverter
{
/// <summary> Parse XIV skeleton data from a havok XML tagfile. </summary>
/// <param name="xml"> Havok XML tagfile containing skeleton data. </param>
public static XivSkeleton FromXml(string xml)
{
var document = new XmlDocument();
document.LoadXml(xml);
var mainSkeletonId = GetMainSkeletonId(document);
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);
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})");
var bones = referencePose
.Zip(parentIndices, boneNames)
.Select(values =>
{
var (transform, parentIndex, name) = values;
return new XivSkeleton.Bone()
{
Transform = transform,
ParentIndex = parentIndex,
Name = name,
};
})
.ToArray();
return new XivSkeleton(bones);
}
/// <summary> Get the main skeleton ID for a given skeleton document. </summary>
/// <param name="node"> XML skeleton document. </param>
private static string GetMainSkeletonId(XmlNode node)
{
var animationSkeletons = node
.SelectSingleNode("/hktagfile/object[@type='hkaAnimationContainer']/array[@name='skeletons']")?
.ChildNodes;
if (animationSkeletons?.Count != 1)
throw new Exception($"Assumption broken: Expected 1 hkaAnimationContainer skeleton, got {animationSkeletons?.Count ?? 0}.");
return animationSkeletons[0]!.InnerText;
}
/// <summary> Read the reference pose transforms for a skeleton. </summary>
/// <param name="node"> XML node for the skeleton. </param>
private static XivSkeleton.Transform[] ReadReferencePose(XmlNode node)
{
return ReadArray(
CheckExists(node.SelectSingleNode("array[@name='referencePose']")),
n =>
{
var raw = ReadVec12(n);
return new XivSkeleton.Transform()
{
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]),
};
}
);
}
/// <summary> Read a 12-item vector from a tagfile. </summary>
/// <param name="node"> Havok Vec12 XML node. </param>
private static float[] ReadVec12(XmlNode node)
{
var array = node.ChildNodes
.Cast<XmlNode>()
.Where(n => n.NodeType != XmlNodeType.Comment)
.Select(n =>
{
var text = n.InnerText.AsSpan().Trim()[1..];
return BitConverter.Int32BitsToSingle(int.Parse(text, NumberStyles.HexNumber));
})
.ToArray();
if (array.Length != 12)
throw new InvalidDataException($"Unexpected Vector12 length ({array.Length}).");
return array;
}
/// <summary> Read the bone parent relations for a skeleton. </summary>
/// <param name="node"> XML node for the skeleton. </param>
private static int[] ReadParentIndices(XmlNode node)
// todo: would be neat to genericise array between bare and children
=> CheckExists(node.SelectSingleNode("array[@name='parentIndices']"))
.InnerText
.Split((char[]) [' ', '\n'], StringSplitOptions.RemoveEmptyEntries)
.Select(int.Parse)
.ToArray();
/// <summary> Read the names of bones in a skeleton. </summary>
/// <param name="node"> XML node for the skeleton. </param>
private static string[] ReadBoneNames(XmlNode node)
=> ReadArray(
CheckExists(node.SelectSingleNode("array[@name='bones']")),
n => CheckExists(n.SelectSingleNode("string[@name='name']")).InnerText
);
/// <summary> Read an XML tagfile array, converting it via the provided conversion function. </summary>
/// <param name="node"> Tagfile XML array node. </param>
/// <param name="convert"> Function to convert array item nodes to required data types. </param>
private static T[] ReadArray<T>(XmlNode node, Func<XmlNode, T> convert)
{
var element = (XmlElement)node;
var size = int.Parse(element.GetAttribute("size"));
var array = new T[size];
foreach (var (childNode, index) in element.ChildNodes.Cast<XmlElement>().WithIndex())
array[index] = convert(childNode);
return array;
}
/// <summary> Check if the argument is null, returning a non-nullable value if it exists, and throwing if not. </summary>
private static T CheckExists<T>(T? value)
{
ArgumentNullException.ThrowIfNull(value);
return value;
}
}