Hook up rudimentary skeleton resolution for equipment models

This commit is contained in:
ackwell 2023-12-30 02:41:19 +11:00
parent 18fd36d2d7
commit 695c18439d
5 changed files with 121 additions and 123 deletions

@ -1 +1 @@
Subproject commit 0dc4c892308aea30314d118362b3ebab7706f4e5
Subproject commit b6a68ab60be6a46f8ede63425cd0716dedf693a3

View file

@ -1,12 +1,8 @@
using System.Xml;
using Dalamud.Plugin.Services;
using Lumina.Extensions;
using OtterGui;
using OtterGui.Tasks;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Files;
using Penumbra.Import.Modules;
using Penumbra.String.Classes;
using SharpGLTF.Scenes;
using SharpGLTF.Transforms;
@ -14,14 +10,16 @@ namespace Penumbra.Import.Models;
public sealed class ModelManager : SingleTaskQueue, IDisposable
{
private readonly IFramework _framework;
private readonly IDataManager _gameData;
private readonly ActiveCollectionData _activeCollectionData;
private readonly ConcurrentDictionary<IAction, (Task, CancellationTokenSource)> _tasks = new();
private bool _disposed = false;
public ModelManager(IDataManager gameData, ActiveCollectionData activeCollectionData)
public ModelManager(IFramework framework, IDataManager gameData, ActiveCollectionData activeCollectionData)
{
_framework = framework;
_gameData = gameData;
_activeCollectionData = activeCollectionData;
}
@ -54,86 +52,33 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
return task;
}
public Task ExportToGltf(MdlFile mdl, string outputPath)
=> Enqueue(new ExportToGltfAction(mdl, outputPath));
public void SkeletonTest()
{
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 testResolve = _activeCollectionData.Current.ResolvePath(utf8Path);
Penumbra.Log.Information($"resolved: {(testResolve == null ? "NULL" : testResolve.ToString())}");
// TODO: is it worth trying to use streams for these instead? i'll need to do this for mtrl/tex too, so might be a good idea. that said, the mtrl reader doesn't accept streams, so...
var bytes = testResolve switch
{
null => _gameData.GetFile(sklbPath).Data,
FullPath path => File.ReadAllBytes(path.ToPath())
};
var sklb = new SklbFile(bytes);
// TODO: Consider making these static methods.
var havokConverter = new HavokConverter();
var xml = havokConverter.HkxToXml(sklb.Skeleton);
var skeletonConverter = new SkeletonConverter();
var skeleton = skeletonConverter.FromXml(xml);
// this is (less) atrocious
NodeBuilder? root = null;
var boneMap = new Dictionary<string, NodeBuilder>();
for (var boneIndex = 0; boneIndex < skeleton.Bones.Length; boneIndex++)
{
var bone = skeleton.Bones[boneIndex];
if (boneMap.ContainsKey(bone.Name)) continue;
var node = new NodeBuilder(bone.Name);
boneMap[bone.Name] = node;
node.SetLocalTransform(new AffineTransform(
bone.Transform.Scale,
bone.Transform.Rotation,
bone.Transform.Translation
), false);
if (bone.ParentIndex == -1)
{
root = node;
continue;
}
var parent = boneMap[skeleton.Bones[bone.ParentIndex].Name];
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!");
}
public Task ExportToGltf(MdlFile mdl, SklbFile? sklb, string outputPath)
=> Enqueue(new ExportToGltfAction(this, mdl, sklb, outputPath));
private class ExportToGltfAction : IAction
{
private readonly ModelManager _manager;
private readonly MdlFile _mdl;
private readonly SklbFile? _sklb;
private readonly string _outputPath;
public ExportToGltfAction(MdlFile mdl, string outputPath)
public ExportToGltfAction(ModelManager manager, MdlFile mdl, SklbFile? sklb, string outputPath)
{
_manager = manager;
_mdl = mdl;
_sklb = sklb;
_outputPath = outputPath;
}
public void Execute(CancellationToken token)
public void Execute(CancellationToken cancel)
{
var scene = new SceneBuilder();
var skeletonRoot = BuildSkeleton(cancel);
if (skeletonRoot != null)
scene.AddNode(skeletonRoot);
// TODO: group by LoD in output tree
for (byte lodIndex = 0; lodIndex < _mdl.LodCount; lodIndex++)
{
@ -151,6 +96,53 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
model.SaveGLTF(_outputPath);
}
// TODO: this should be moved to a seperate model converter or something
private NodeBuilder? BuildSkeleton(CancellationToken cancel)
{
if (_sklb == null)
return null;
// TODO: Consider making these static methods.
// TODO: work out how i handle this havok deal. running it outside the framework causes an immediate ctd.
var havokConverter = new HavokConverter();
var xmlTask = _manager._framework.RunOnFrameworkThread(() => havokConverter.HkxToXml(_sklb.Skeleton));
xmlTask.Wait(cancel);
var xml = xmlTask.Result;
var skeletonConverter = new SkeletonConverter();
var skeleton = skeletonConverter.FromXml(xml);
// this is (less) atrocious
NodeBuilder? root = null;
var boneMap = new Dictionary<string, NodeBuilder>();
for (var boneIndex = 0; boneIndex < skeleton.Bones.Length; boneIndex++)
{
var bone = skeleton.Bones[boneIndex];
if (boneMap.ContainsKey(bone.Name)) continue;
var node = new NodeBuilder(bone.Name);
boneMap[bone.Name] = node;
node.SetLocalTransform(new AffineTransform(
bone.Transform.Scale,
bone.Transform.Rotation,
bone.Transform.Translation
), false);
if (bone.ParentIndex == -1)
{
root = node;
continue;
}
var parent = boneMap[skeleton.Bones[bone.ParentIndex].Name];
parent.AddNode(node);
}
return root;
}
public bool Equals(IAction? other)
{
if (other is not ExportToGltfAction rhs)

View file

@ -1,44 +0,0 @@
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));
}
}

View file

@ -8,16 +8,20 @@ namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
private class MdlTab : IWritable
private partial class MdlTab : IWritable
{
private ModEditWindow _edit;
public readonly MdlFile Mdl;
public readonly List<Utf8GamePath> GamePaths;
private readonly List<string>[] _attributes;
public bool PendingIo { get; private set; } = false;
// TODO: this can probably be genericised across all of chara
[GeneratedRegex(@"chara/equipment/e(?'Set'\d{4})/model/c(?'Race'\d{4})e\k'Set'_.+\.mdl", RegexOptions.Compiled)]
private static partial Regex CharaEquipmentRegex();
public MdlTab(ModEditWindow edit, byte[] bytes, string path, Mod? mod)
{
_edit = edit;
@ -54,11 +58,61 @@ public partial class ModEditWindow
/// <param name="outputPath"> Disk path to save the resulting file to. </param>
public void Export(string outputPath)
{
// NOTES ON EST (i don't think it's worth supporting yet...)
// for collection wide lookup;
// Collections.Cache.EstCache::GetEstEntry
// Collections.Cache.MetaCache::GetEstEntry
// Collections.ModCollection.MetaCache?
// for default lookup, probably;
// EstFile.GetDefault(...)
// TODO: allow user to pick the gamepath in the ui
// TODO: what if there's no gamepaths?
var mdlPath = GamePaths.First();
var sklbPath = GetSklbPath(mdlPath.ToString());
var sklb = sklbPath != null ? ReadSklb(sklbPath) : null;
PendingIo = true;
_edit._models.ExportToGltf(Mdl, outputPath)
_edit._models.ExportToGltf(Mdl, sklb, outputPath)
.ContinueWith(_ => PendingIo = false);
}
/// <summary> Try to find the .sklb path for a .mdl file. </summary>
/// <param name="mdlPath"> .mdl file to look up the skeleton for. </param>
private string? GetSklbPath(string mdlPath)
{
// TODO: This needs to be drastically expanded, it's dodgy af rn
var match = CharaEquipmentRegex().Match(mdlPath);
if (!match.Success)
return null;
var race = match.Groups["Race"].Value;
return $"chara/human/c{race}/skeleton/base/b0001/skl_c{race}b0001.sklb";
}
/// <summary> Read a .sklb from the active collection or game. </summary>
/// <param name="sklbPath"> Game path to the .sklb to load. </param>
private SklbFile ReadSklb(string sklbPath)
{
// TODO: if cross-collection lookups are turned off, this conversion can be skipped
if (!Utf8GamePath.FromString(sklbPath, out var utf8SklbPath, true))
throw new Exception("TODO: handle - should it throw, or try to fail gracefully?");
var resolvedPath = _edit._activeCollections.Current.ResolvePath(utf8SklbPath);
// TODO: is it worth trying to use streams for these instead? i'll need to do this for mtrl/tex too, so might be a good idea. that said, the mtrl reader doesn't accept streams, so...
var bytes = resolvedPath switch
{
null => _edit._dalamud.GameData.GetFile(sklbPath)?.Data,
FullPath path => File.ReadAllBytes(path.ToPath()),
};
if (bytes == null)
throw new Exception("TODO: handle - this effectively means that the resolved path doesn't exist. graceful?");
return new SklbFile(bytes);
}
/// <summary> Remove the material given by the index. </summary>
/// <remarks> Meshes using the removed material are redirected to material 0, and those after the index are corrected. </remarks>
public void RemoveMaterial(int materialIndex)

View file

@ -38,10 +38,6 @@ public partial class ModEditWindow
{
tab.Export("C:\\Users\\ackwell\\blender\\gltf-tests\\bingo.gltf");
}
if (ImGui.Button("zoingo boingo"))
{
_models.SkeletonTest();
}
ImGui.TextUnformatted("blippity blap");
foreach (var gamePath in tab.GamePaths)
ImGui.TextUnformatted(gamePath.ToString());