mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-15 21:24:18 +01:00
Hook up rudimentary skeleton resolution for equipment models
This commit is contained in:
parent
18fd36d2d7
commit
695c18439d
5 changed files with 121 additions and 123 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit 0dc4c892308aea30314d118362b3ebab7706f4e5
|
||||
Subproject commit b6a68ab60be6a46f8ede63425cd0716dedf693a3
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue