mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-13 20:24:17 +01:00
Clean up and refactor skeleton logic
This commit is contained in:
parent
d646c5e4b5
commit
d7cac3e09a
4 changed files with 223 additions and 149 deletions
|
|
@ -61,6 +61,8 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
|
||||||
{
|
{
|
||||||
var sklbPath = "chara/human/c0201/skeleton/base/b0001/skl_c0201b0001.sklb";
|
var sklbPath = "chara/human/c0201/skeleton/base/b0001/skl_c0201b0001.sklb";
|
||||||
|
|
||||||
|
// NOTE: to resolve game path from _mod_, will need to wire the mod class via the modeditwindow to the model editor, through to here.
|
||||||
|
// NOTE: to get the game path for a model we'll probably need to use a reverse resolve - there's no guarantee for a modded model that they're named per game path, nor that there's only one name.
|
||||||
var succeeded = Utf8GamePath.FromString(sklbPath, out var utf8Path, true);
|
var succeeded = Utf8GamePath.FromString(sklbPath, out var utf8Path, true);
|
||||||
var testResolve = _activeCollectionData.Current.ResolvePath(utf8Path);
|
var testResolve = _activeCollectionData.Current.ResolvePath(utf8Path);
|
||||||
Penumbra.Log.Information($"resolved: {(testResolve == null ? "NULL" : testResolve.ToString())}");
|
Penumbra.Log.Information($"resolved: {(testResolve == null ? "NULL" : testResolve.ToString())}");
|
||||||
|
|
@ -72,56 +74,40 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
|
||||||
FullPath path => File.ReadAllBytes(path.ToPath())
|
FullPath path => File.ReadAllBytes(path.ToPath())
|
||||||
};
|
};
|
||||||
|
|
||||||
var something = new Garbage(bytes);
|
var sklb = new SklbFile(bytes);
|
||||||
|
|
||||||
var fuck = new HavokConverter();
|
// TODO: Consider making these static methods.
|
||||||
var killme = fuck.HkxToXml(something.Skeleton);
|
var havokConverter = new HavokConverter();
|
||||||
|
var xml = havokConverter.HkxToXml(sklb.Skeleton);
|
||||||
|
|
||||||
var doc = new XmlDocument();
|
var skeletonConverter = new SkeletonConverter();
|
||||||
doc.LoadXml(killme);
|
var skeleton = skeletonConverter.FromXml(xml);
|
||||||
|
|
||||||
var skels = doc.SelectNodes("/hktagfile/object[@type='hkaSkeleton']")
|
// this is (less) atrocious
|
||||||
.Cast<XmlElement>()
|
|
||||||
.Select(element => new Skel(element))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
// todo: look into how this is selecting the skel - only first?
|
|
||||||
var animSkel = doc.SelectSingleNode("/hktagfile/object[@type='hkaAnimationContainer']")
|
|
||||||
.SelectNodes("array[@name='skeletons']")
|
|
||||||
.Cast<XmlElement>()
|
|
||||||
.First();
|
|
||||||
var mainSkelId = animSkel.ChildNodes[0].InnerText;
|
|
||||||
|
|
||||||
var mainSkel = skels.First(skel => skel.Id == mainSkelId);
|
|
||||||
|
|
||||||
// this is atrocious
|
|
||||||
NodeBuilder? root = null;
|
NodeBuilder? root = null;
|
||||||
var boneMap = new Dictionary<string, NodeBuilder>();
|
var boneMap = new Dictionary<string, NodeBuilder>();
|
||||||
for (var boneIndex = 0; boneIndex < mainSkel.BoneNames.Length; boneIndex++)
|
for (var boneIndex = 0; boneIndex < skeleton.Bones.Length; boneIndex++)
|
||||||
{
|
{
|
||||||
var name = mainSkel.BoneNames[boneIndex];
|
var bone = skeleton.Bones[boneIndex];
|
||||||
if (boneMap.ContainsKey(name)) continue;
|
|
||||||
|
|
||||||
var node = new NodeBuilder(name);
|
if (boneMap.ContainsKey(bone.Name)) continue;
|
||||||
|
|
||||||
var rp = mainSkel.ReferencePose[boneIndex];
|
var node = new NodeBuilder(bone.Name);
|
||||||
var transform = new AffineTransform(
|
boneMap[bone.Name] = node;
|
||||||
new Vector3(rp[8], rp[9], rp[10]),
|
|
||||||
new Quaternion(rp[4], rp[5], rp[6], rp[7]),
|
|
||||||
new Vector3([rp[0], rp[1], rp[2]])
|
|
||||||
);
|
|
||||||
node.SetLocalTransform(transform, false);
|
|
||||||
|
|
||||||
boneMap[name] = node;
|
node.SetLocalTransform(new AffineTransform(
|
||||||
|
bone.Transform.Scale,
|
||||||
|
bone.Transform.Rotation,
|
||||||
|
bone.Transform.Translation
|
||||||
|
), false);
|
||||||
|
|
||||||
var parentId = mainSkel.ParentIndices[boneIndex];
|
if (bone.ParentIndex == -1)
|
||||||
if (parentId == -1)
|
|
||||||
{
|
{
|
||||||
root = node;
|
root = node;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var parent = boneMap[mainSkel.BoneNames[parentId]];
|
var parent = boneMap[skeleton.Bones[bone.ParentIndex].Name];
|
||||||
parent.AddNode(node);
|
parent.AddNode(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,120 +116,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
|
||||||
var model = scene.ToGltf2();
|
var model = scene.ToGltf2();
|
||||||
model.SaveGLTF(@"C:\Users\ackwell\blender\gltf-tests\zoingo.gltf");
|
model.SaveGLTF(@"C:\Users\ackwell\blender\gltf-tests\zoingo.gltf");
|
||||||
|
|
||||||
Penumbra.Log.Information($"zoingo {string.Join(',', mainSkel.ParentIndices)}");
|
Penumbra.Log.Information($"zoingo!");
|
||||||
}
|
|
||||||
|
|
||||||
// this is garbage that should be in gamedata
|
|
||||||
|
|
||||||
private sealed class Garbage
|
|
||||||
{
|
|
||||||
public byte[] Skeleton;
|
|
||||||
|
|
||||||
public Garbage(byte[] data)
|
|
||||||
{
|
|
||||||
using var stream = new MemoryStream(data);
|
|
||||||
using var reader = new BinaryReader(stream);
|
|
||||||
|
|
||||||
var magic = reader.ReadUInt32();
|
|
||||||
if (magic != 0x736B6C62)
|
|
||||||
throw new InvalidDataException("Invalid sklb magic");
|
|
||||||
|
|
||||||
// todo do this all properly jfc
|
|
||||||
var version = reader.ReadUInt32();
|
|
||||||
|
|
||||||
var oldHeader = version switch {
|
|
||||||
0x31313030 or 0x31313130 or 0x31323030 => true,
|
|
||||||
0x31333030 => false,
|
|
||||||
_ => throw new InvalidDataException($"Unknown version {version}")
|
|
||||||
};
|
|
||||||
|
|
||||||
// Skeleton offset directly follows the layer offset.
|
|
||||||
uint skeletonOffset;
|
|
||||||
if (oldHeader)
|
|
||||||
{
|
|
||||||
reader.ReadInt16();
|
|
||||||
skeletonOffset = reader.ReadUInt16();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
reader.ReadUInt32();
|
|
||||||
skeletonOffset = reader.ReadUInt32();
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.Seek(skeletonOffset);
|
|
||||||
Skeleton = reader.ReadBytes((int)(reader.BaseStream.Length - skeletonOffset));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Skel
|
|
||||||
{
|
|
||||||
public readonly string Id;
|
|
||||||
|
|
||||||
public readonly float[][] ReferencePose;
|
|
||||||
public readonly int[] ParentIndices;
|
|
||||||
public readonly string[] BoneNames;
|
|
||||||
|
|
||||||
// TODO: this shouldn't have any reference to the skel xml - i should just make it a bare class that can be repr'd in gamedata or whatever
|
|
||||||
public Skel(XmlElement el)
|
|
||||||
{
|
|
||||||
Id = el.GetAttribute("id");
|
|
||||||
|
|
||||||
ReferencePose = ReadReferencePose(el);
|
|
||||||
ParentIndices = ReadParentIndices(el);
|
|
||||||
BoneNames = ReadBoneNames(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
private float[][] ReadReferencePose(XmlElement el)
|
|
||||||
{
|
|
||||||
return ReadArray(
|
|
||||||
(XmlElement)el.SelectSingleNode("array[@name='referencePose']"),
|
|
||||||
ReadVec12
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private float[] ReadVec12(XmlElement el)
|
|
||||||
{
|
|
||||||
return el.ChildNodes
|
|
||||||
.Cast<XmlNode>()
|
|
||||||
.Where(node => node.NodeType != XmlNodeType.Comment)
|
|
||||||
.Select(node => {
|
|
||||||
var t = node.InnerText.Trim()[1..];
|
|
||||||
// todo: surely there's a less shit way to do this i mean seriously
|
|
||||||
return BitConverter.ToSingle(BitConverter.GetBytes(int.Parse(t, NumberStyles.HexNumber)));
|
|
||||||
})
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
private int[] ReadParentIndices(XmlElement el)
|
|
||||||
{
|
|
||||||
// todo: would be neat to genericise array between bare and children
|
|
||||||
return el.SelectSingleNode("array[@name='parentIndices']")
|
|
||||||
.InnerText
|
|
||||||
.Split(new char[] {' ', '\n'}, StringSplitOptions.RemoveEmptyEntries)
|
|
||||||
.Select(int.Parse)
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
private string[] ReadBoneNames(XmlElement el)
|
|
||||||
{
|
|
||||||
return ReadArray(
|
|
||||||
(XmlElement)el.SelectSingleNode("array[@name='bones']"),
|
|
||||||
el => el.SelectSingleNode("string[@name='name']").InnerText
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private T[] ReadArray<T>(XmlElement el, Func<XmlElement, T> convert)
|
|
||||||
{
|
|
||||||
var size = int.Parse(el.GetAttribute("size"));
|
|
||||||
|
|
||||||
var array = new T[size];
|
|
||||||
foreach (var (node, index) in el.ChildNodes.Cast<XmlElement>().WithIndex())
|
|
||||||
{
|
|
||||||
array[index] = convert(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ExportToGltfAction : IAction
|
private class ExportToGltfAction : IAction
|
||||||
|
|
|
||||||
25
Penumbra/Import/Models/Skeleton.cs
Normal file
25
Penumbra/Import/Models/Skeleton.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
namespace Penumbra.Import.Models;
|
||||||
|
|
||||||
|
// TODO: this should almost certainly live in gamedata. if not, it should at _least_ be adjacent to the model handling.
|
||||||
|
public class Skeleton
|
||||||
|
{
|
||||||
|
public Bone[] Bones;
|
||||||
|
|
||||||
|
public Skeleton(Bone[] bones)
|
||||||
|
{
|
||||||
|
Bones = bones;
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Bone
|
||||||
|
{
|
||||||
|
public string Name;
|
||||||
|
public int ParentIndex;
|
||||||
|
public Transform Transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Transform {
|
||||||
|
public Vector3 Scale;
|
||||||
|
public Quaternion Rotation;
|
||||||
|
public Vector3 Translation;
|
||||||
|
}
|
||||||
|
}
|
||||||
132
Penumbra/Import/Models/SkeletonConverter.cs
Normal file
132
Penumbra/Import/Models/SkeletonConverter.cs
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
using System.Xml;
|
||||||
|
using OtterGui;
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var document = new XmlDocument();
|
||||||
|
document.LoadXml(xml);
|
||||||
|
|
||||||
|
var mainSkeletonId = GetMainSkeletonId(document);
|
||||||
|
|
||||||
|
var skeletonNode = document.SelectSingleNode($"/hktagfile/object[@type='hkaSkeleton'][@id='{mainSkeletonId}']");
|
||||||
|
if (skeletonNode == null)
|
||||||
|
throw new InvalidDataException();
|
||||||
|
|
||||||
|
var referencePose = ReadReferencePose(skeletonNode);
|
||||||
|
var parentIndices = ReadParentIndices(skeletonNode);
|
||||||
|
var boneNames = ReadBoneNames(skeletonNode);
|
||||||
|
|
||||||
|
if (boneNames.Length != parentIndices.Length || boneNames.Length != referencePose.Length)
|
||||||
|
throw new InvalidDataException();
|
||||||
|
|
||||||
|
var bones = referencePose
|
||||||
|
.Zip(parentIndices, boneNames)
|
||||||
|
.Select(values =>
|
||||||
|
{
|
||||||
|
var (transform, parentIndex, name) = values;
|
||||||
|
return new Skeleton.Bone()
|
||||||
|
{
|
||||||
|
Transform = transform,
|
||||||
|
ParentIndex = parentIndex,
|
||||||
|
Name = name,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return new Skeleton(bones);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get the main skeleton ID for a given skeleton document.</summary>
|
||||||
|
/// <param name="node">XML skeleton document.</param>
|
||||||
|
private 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 Skeleton.Transform[] ReadReferencePose(XmlNode node)
|
||||||
|
{
|
||||||
|
return ReadArray(
|
||||||
|
CheckExists(node.SelectSingleNode("array[@name='referencePose']")),
|
||||||
|
node =>
|
||||||
|
{
|
||||||
|
var raw = ReadVec12(node);
|
||||||
|
return new Skeleton.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]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float[] ReadVec12(XmlNode node)
|
||||||
|
{
|
||||||
|
var array = node.ChildNodes
|
||||||
|
.Cast<XmlNode>()
|
||||||
|
.Where(node => node.NodeType != XmlNodeType.Comment)
|
||||||
|
.Select(node =>
|
||||||
|
{
|
||||||
|
var text = node.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();
|
||||||
|
|
||||||
|
if (array.Length != 12)
|
||||||
|
throw new InvalidDataException();
|
||||||
|
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int[] ReadParentIndices(XmlNode node)
|
||||||
|
{
|
||||||
|
// todo: would be neat to genericise array between bare and children
|
||||||
|
return CheckExists(node.SelectSingleNode("array[@name='parentIndices']"))
|
||||||
|
.InnerText
|
||||||
|
.Split(new char[] { ' ', '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(int.Parse)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string[] ReadBoneNames(XmlNode node)
|
||||||
|
{
|
||||||
|
return ReadArray(
|
||||||
|
CheckExists(node.SelectSingleNode("array[@name='bones']")),
|
||||||
|
node => CheckExists(node.SelectSingleNode("string[@name='name']")).InnerText
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T CheckExists<T>(T? value)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
44
Penumbra/Import/Models/SklbFile.cs
Normal file
44
Penumbra/Import/Models/SklbFile.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
using Lumina.Extensions;
|
||||||
|
|
||||||
|
namespace Penumbra.Import.Models;
|
||||||
|
|
||||||
|
// TODO: yeah this goes in gamedata.
|
||||||
|
public class SklbFile
|
||||||
|
{
|
||||||
|
public byte[] Skeleton;
|
||||||
|
|
||||||
|
public SklbFile(byte[] data)
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(data);
|
||||||
|
using var reader = new BinaryReader(stream);
|
||||||
|
|
||||||
|
var magic = reader.ReadUInt32();
|
||||||
|
if (magic != 0x736B6C62)
|
||||||
|
throw new InvalidDataException("Invalid sklb magic");
|
||||||
|
|
||||||
|
// todo do this all properly jfc
|
||||||
|
var version = reader.ReadUInt32();
|
||||||
|
|
||||||
|
var oldHeader = version switch {
|
||||||
|
0x31313030 or 0x31313130 or 0x31323030 => true,
|
||||||
|
0x31333030 => false,
|
||||||
|
_ => throw new InvalidDataException($"Unknown version {version}")
|
||||||
|
};
|
||||||
|
|
||||||
|
// Skeleton offset directly follows the layer offset.
|
||||||
|
uint skeletonOffset;
|
||||||
|
if (oldHeader)
|
||||||
|
{
|
||||||
|
reader.ReadInt16();
|
||||||
|
skeletonOffset = reader.ReadUInt16();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
reader.ReadUInt32();
|
||||||
|
skeletonOffset = reader.ReadUInt32();
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.Seek(skeletonOffset);
|
||||||
|
Skeleton = reader.ReadBytes((int)(reader.BaseStream.Length - skeletonOffset));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue