mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-13 20:24:17 +01:00
Initial skeleton tests
This commit is contained in:
parent
bc24110c9f
commit
635d606112
3 changed files with 330 additions and 2 deletions
141
Penumbra/Import/Models/HavokConverter.cs
Normal file
141
Penumbra/Import/Models/HavokConverter.cs
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
using FFXIVClientStructs.Havok;
|
||||
|
||||
namespace Penumbra.Import.Models;
|
||||
|
||||
// TODO: where should this live? interop i guess, in penum? or game data?
|
||||
public unsafe class HavokConverter
|
||||
{
|
||||
/// <summary>Creates a temporary file and returns its path.</summary>
|
||||
/// <returns>Path to a temporary file.</returns>
|
||||
private string CreateTempFile()
|
||||
{
|
||||
var s = File.Create(Path.GetTempFileName());
|
||||
s.Close();
|
||||
return s.Name;
|
||||
}
|
||||
|
||||
/// <summary>Converts a .hkx file to a .xml file.</summary>
|
||||
/// <param name="hkx">A byte array representing the .hkx file.</param>
|
||||
/// <returns>A string representing the .xml file.</returns>
|
||||
/// <exception cref="Exceptions.HavokReadException">Thrown if parsing the .hkx file fails.</exception>
|
||||
/// <exception cref="Exceptions.HavokWriteException">Thrown if writing the .xml file fails.</exception>
|
||||
public string HkxToXml(byte[] hkx)
|
||||
{
|
||||
var tempHkx = CreateTempFile();
|
||||
File.WriteAllBytes(tempHkx, hkx);
|
||||
|
||||
var resource = Read(tempHkx);
|
||||
File.Delete(tempHkx);
|
||||
|
||||
if (resource == null) throw new Exception("HavokReadException");
|
||||
|
||||
var options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers
|
||||
| hkSerializeUtil.SaveOptionBits.TextFormat
|
||||
| hkSerializeUtil.SaveOptionBits.WriteAttributes;
|
||||
|
||||
var file = Write(resource, options);
|
||||
file.Close();
|
||||
|
||||
var bytes = File.ReadAllText(file.Name);
|
||||
File.Delete(file.Name);
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// <summary>Converts a .xml file to a .hkx file.</summary>
|
||||
/// <param name="xml">A string representing the .xml file.</param>
|
||||
/// <returns>A byte array representing the .hkx file.</returns>
|
||||
/// <exception cref="Exceptions.HavokReadException">Thrown if parsing the .xml file fails.</exception>
|
||||
/// <exception cref="Exceptions.HavokWriteException">Thrown if writing the .hkx file fails.</exception>
|
||||
public byte[] XmlToHkx(string xml)
|
||||
{
|
||||
var tempXml = CreateTempFile();
|
||||
File.WriteAllText(tempXml, xml);
|
||||
|
||||
var resource = Read(tempXml);
|
||||
File.Delete(tempXml);
|
||||
|
||||
if (resource == null) throw new Exception("HavokReadException");
|
||||
|
||||
var options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers
|
||||
| hkSerializeUtil.SaveOptionBits.WriteAttributes;
|
||||
|
||||
var file = Write(resource, options);
|
||||
file.Close();
|
||||
|
||||
var bytes = File.ReadAllBytes(file.Name);
|
||||
File.Delete(file.Name);
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a serialized file into an hkResource*.
|
||||
/// The type is guessed automatically by Havok.
|
||||
/// This pointer might be null - you should check for that.
|
||||
/// </summary>
|
||||
/// <param name="filePath">Path to a file on the filesystem.</param>
|
||||
/// <returns>A (potentially null) pointer to an hkResource.</returns>
|
||||
private hkResource* Read(string filePath)
|
||||
{
|
||||
var path = Marshal.StringToHGlobalAnsi(filePath);
|
||||
|
||||
var builtinTypeRegistry = hkBuiltinTypeRegistry.Instance();
|
||||
|
||||
var loadOptions = stackalloc hkSerializeUtil.LoadOptions[1];
|
||||
loadOptions->Flags = new() { Storage = (int)hkSerializeUtil.LoadOptionBits.Default };
|
||||
loadOptions->ClassNameRegistry = builtinTypeRegistry->GetClassNameRegistry();
|
||||
loadOptions->TypeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry();
|
||||
|
||||
// TODO: probably can loadfrombuffer this
|
||||
var resource = hkSerializeUtil.LoadFromFile((byte*)path, null, loadOptions);
|
||||
return resource;
|
||||
}
|
||||
|
||||
/// <summary>Serializes an hkResource* to a temporary file.</summary>
|
||||
/// <param name="resource">A pointer to the hkResource, opened through Read().</param>
|
||||
/// <param name="optionBits">Flags representing how to serialize the file.</param>
|
||||
/// <returns>An opened FileStream of a temporary file. You are expected to read the file and delete it.</returns>
|
||||
/// <exception cref="Exceptions.HavokWriteException">Thrown if accessing the root level container fails.</exception>
|
||||
/// <exception cref="Exceptions.HavokFailureException">Thrown if an unknown failure in writing occurs.</exception>
|
||||
private FileStream Write(
|
||||
hkResource* resource,
|
||||
hkSerializeUtil.SaveOptionBits optionBits
|
||||
)
|
||||
{
|
||||
var tempFile = CreateTempFile();
|
||||
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 }
|
||||
};
|
||||
|
||||
|
||||
var builtinTypeRegistry = hkBuiltinTypeRegistry.Instance();
|
||||
var classNameRegistry = builtinTypeRegistry->GetClassNameRegistry();
|
||||
var typeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry();
|
||||
|
||||
try
|
||||
{
|
||||
var name = "hkRootLevelContainer";
|
||||
|
||||
var resourcePtr = (hkRootLevelContainer*)resource->GetContentsPointer(name, typeInfoRegistry);
|
||||
if (resourcePtr == null) throw new Exception("HavokWriteException");
|
||||
|
||||
var hkRootLevelContainerClass = classNameRegistry->GetClassByName(name);
|
||||
if (hkRootLevelContainerClass == null) throw new Exception("HavokWriteException");
|
||||
|
||||
hkSerializeUtil.Save(result, resourcePtr, hkRootLevelContainerClass, oStream.StreamWriter.ptr, saveOptions);
|
||||
}
|
||||
finally { oStream.Dtor(); }
|
||||
|
||||
if (result->Result == hkResult.hkResultEnum.Failure) throw new Exception("HavokFailureException");
|
||||
|
||||
return new FileStream(tempFile, FileMode.Open);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +1,26 @@
|
|||
using System.Xml;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Lumina.Data;
|
||||
using Lumina.Extensions;
|
||||
using OtterGui;
|
||||
using OtterGui.Tasks;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.Import.Modules;
|
||||
using SharpGLTF.Scenes;
|
||||
using SharpGLTF.Transforms;
|
||||
|
||||
namespace Penumbra.Import.Models;
|
||||
|
||||
public sealed class ModelManager : SingleTaskQueue, IDisposable
|
||||
{
|
||||
private readonly IDataManager _gameData;
|
||||
|
||||
private readonly ConcurrentDictionary<IAction, (Task, CancellationTokenSource)> _tasks = new();
|
||||
private bool _disposed = false;
|
||||
|
||||
public ModelManager()
|
||||
public ModelManager(IDataManager gameData)
|
||||
{
|
||||
//
|
||||
_gameData = gameData;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
|
@ -46,6 +54,181 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
|
|||
public Task ExportToGltf(MdlFile mdl, string path)
|
||||
=> Enqueue(new ExportToGltfAction(mdl, path));
|
||||
|
||||
public void SkeletonTest()
|
||||
{
|
||||
var sklbPath = "chara/human/c0201/skeleton/base/b0001/skl_c0201b0001.sklb";
|
||||
|
||||
var something = _gameData.GetFile<Garbage>(sklbPath);
|
||||
|
||||
var fuck = new HavokConverter();
|
||||
var killme = fuck.HkxToXml(something.Skeleton);
|
||||
|
||||
var doc = new XmlDocument();
|
||||
doc.LoadXml(killme);
|
||||
|
||||
var skels = doc.SelectNodes("/hktagfile/object[@type='hkaSkeleton']")
|
||||
.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;
|
||||
var boneMap = new Dictionary<string, NodeBuilder>();
|
||||
for (var boneIndex = 0; boneIndex < mainSkel.BoneNames.Length; boneIndex++)
|
||||
{
|
||||
var name = mainSkel.BoneNames[boneIndex];
|
||||
if (boneMap.ContainsKey(name)) continue;
|
||||
|
||||
var node = new NodeBuilder(name);
|
||||
|
||||
var rp = mainSkel.ReferencePose[boneIndex];
|
||||
var transform = new AffineTransform(
|
||||
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;
|
||||
|
||||
var parentId = mainSkel.ParentIndices[boneIndex];
|
||||
if (parentId == -1)
|
||||
{
|
||||
root = node;
|
||||
continue;
|
||||
}
|
||||
|
||||
var parent = boneMap[mainSkel.BoneNames[parentId]];
|
||||
parent.AddNode(node);
|
||||
}
|
||||
|
||||
var scene = new SceneBuilder();
|
||||
scene.AddNode(root);
|
||||
var model = scene.ToGltf2();
|
||||
model.SaveGLTF(@"C:\Users\ackwell\blender\gltf-tests\zoingo.gltf");
|
||||
|
||||
Penumbra.Log.Information($"zoingo {string.Join(',', mainSkel.ParentIndices)}");
|
||||
}
|
||||
|
||||
// this is garbage that should be in gamedata
|
||||
|
||||
private sealed class Garbage : FileResource
|
||||
{
|
||||
public byte[] Skeleton;
|
||||
|
||||
public override void LoadFile()
|
||||
{
|
||||
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 readonly MdlFile _mdl;
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ public partial class ModEditWindow
|
|||
var task = _models.ExportToGltf(file, "C:\\Users\\ackwell\\blender\\gltf-tests\\bingo.gltf");
|
||||
task.ContinueWith(_ => _pendingIo = false);
|
||||
}
|
||||
if (ImGui.Button("zoingo boingo"))
|
||||
{
|
||||
_models.SkeletonTest();
|
||||
}
|
||||
|
||||
var ret = false;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue