mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-22 08:29:19 +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 Dalamud.Plugin.Services;
|
||||||
using Lumina.Extensions;
|
|
||||||
using OtterGui;
|
|
||||||
using OtterGui.Tasks;
|
using OtterGui.Tasks;
|
||||||
using Penumbra.Collections.Manager;
|
using Penumbra.Collections.Manager;
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
using Penumbra.Import.Modules;
|
using Penumbra.Import.Modules;
|
||||||
using Penumbra.String.Classes;
|
|
||||||
using SharpGLTF.Scenes;
|
using SharpGLTF.Scenes;
|
||||||
using SharpGLTF.Transforms;
|
using SharpGLTF.Transforms;
|
||||||
|
|
||||||
|
|
@ -14,14 +10,16 @@ namespace Penumbra.Import.Models;
|
||||||
|
|
||||||
public sealed class ModelManager : SingleTaskQueue, IDisposable
|
public sealed class ModelManager : SingleTaskQueue, IDisposable
|
||||||
{
|
{
|
||||||
|
private readonly IFramework _framework;
|
||||||
private readonly IDataManager _gameData;
|
private readonly IDataManager _gameData;
|
||||||
private readonly ActiveCollectionData _activeCollectionData;
|
private readonly ActiveCollectionData _activeCollectionData;
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<IAction, (Task, CancellationTokenSource)> _tasks = new();
|
private readonly ConcurrentDictionary<IAction, (Task, CancellationTokenSource)> _tasks = new();
|
||||||
private bool _disposed = false;
|
private bool _disposed = false;
|
||||||
|
|
||||||
public ModelManager(IDataManager gameData, ActiveCollectionData activeCollectionData)
|
public ModelManager(IFramework framework, IDataManager gameData, ActiveCollectionData activeCollectionData)
|
||||||
{
|
{
|
||||||
|
_framework = framework;
|
||||||
_gameData = gameData;
|
_gameData = gameData;
|
||||||
_activeCollectionData = activeCollectionData;
|
_activeCollectionData = activeCollectionData;
|
||||||
}
|
}
|
||||||
|
|
@ -54,31 +52,62 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task ExportToGltf(MdlFile mdl, string outputPath)
|
public Task ExportToGltf(MdlFile mdl, SklbFile? sklb, string outputPath)
|
||||||
=> Enqueue(new ExportToGltfAction(mdl, outputPath));
|
=> Enqueue(new ExportToGltfAction(this, mdl, sklb, outputPath));
|
||||||
|
|
||||||
public void SkeletonTest()
|
private class ExportToGltfAction : IAction
|
||||||
{
|
{
|
||||||
var sklbPath = "chara/human/c0201/skeleton/base/b0001/skl_c0201b0001.sklb";
|
private readonly ModelManager _manager;
|
||||||
|
|
||||||
// NOTE: to resolve game path from _mod_, will need to wire the mod class via the modeditwindow to the model editor, through to here.
|
private readonly MdlFile _mdl;
|
||||||
// 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.
|
private readonly SklbFile? _sklb;
|
||||||
var succeeded = Utf8GamePath.FromString(sklbPath, out var utf8Path, true);
|
private readonly string _outputPath;
|
||||||
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...
|
public ExportToGltfAction(ModelManager manager, MdlFile mdl, SklbFile? sklb, string outputPath)
|
||||||
var bytes = testResolve switch
|
|
||||||
{
|
{
|
||||||
null => _gameData.GetFile(sklbPath).Data,
|
_manager = manager;
|
||||||
FullPath path => File.ReadAllBytes(path.ToPath())
|
_mdl = mdl;
|
||||||
};
|
_sklb = sklb;
|
||||||
|
_outputPath = outputPath;
|
||||||
|
}
|
||||||
|
|
||||||
var sklb = new SklbFile(bytes);
|
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++)
|
||||||
|
{
|
||||||
|
var lod = _mdl.Lods[lodIndex];
|
||||||
|
|
||||||
|
// TODO: consider other types?
|
||||||
|
for (ushort meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++)
|
||||||
|
{
|
||||||
|
var meshBuilder = MeshConverter.ToGltf(_mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset));
|
||||||
|
scene.AddRigidMesh(meshBuilder, Matrix4x4.Identity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var model = scene.ToGltf2();
|
||||||
|
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: 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 havokConverter = new HavokConverter();
|
||||||
var xml = havokConverter.HkxToXml(sklb.Skeleton);
|
var xmlTask = _manager._framework.RunOnFrameworkThread(() => havokConverter.HkxToXml(_sklb.Skeleton));
|
||||||
|
xmlTask.Wait(cancel);
|
||||||
|
var xml = xmlTask.Result;
|
||||||
|
|
||||||
var skeletonConverter = new SkeletonConverter();
|
var skeletonConverter = new SkeletonConverter();
|
||||||
var skeleton = skeletonConverter.FromXml(xml);
|
var skeleton = skeletonConverter.FromXml(xml);
|
||||||
|
|
@ -111,44 +140,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
|
||||||
parent.AddNode(node);
|
parent.AddNode(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
var scene = new SceneBuilder();
|
return root;
|
||||||
scene.AddNode(root);
|
|
||||||
var model = scene.ToGltf2();
|
|
||||||
model.SaveGLTF(@"C:\Users\ackwell\blender\gltf-tests\zoingo.gltf");
|
|
||||||
|
|
||||||
Penumbra.Log.Information($"zoingo!");
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ExportToGltfAction : IAction
|
|
||||||
{
|
|
||||||
private readonly MdlFile _mdl;
|
|
||||||
private readonly string _outputPath;
|
|
||||||
|
|
||||||
public ExportToGltfAction(MdlFile mdl, string outputPath)
|
|
||||||
{
|
|
||||||
_mdl = mdl;
|
|
||||||
_outputPath = outputPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Execute(CancellationToken token)
|
|
||||||
{
|
|
||||||
var scene = new SceneBuilder();
|
|
||||||
|
|
||||||
// TODO: group by LoD in output tree
|
|
||||||
for (byte lodIndex = 0; lodIndex < _mdl.LodCount; lodIndex++)
|
|
||||||
{
|
|
||||||
var lod = _mdl.Lods[lodIndex];
|
|
||||||
|
|
||||||
// TODO: consider other types?
|
|
||||||
for (ushort meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++)
|
|
||||||
{
|
|
||||||
var meshBuilder = MeshConverter.ToGltf(_mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset));
|
|
||||||
scene.AddRigidMesh(meshBuilder, Matrix4x4.Identity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var model = scene.ToGltf2();
|
|
||||||
model.SaveGLTF(_outputPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Equals(IAction? other)
|
public bool Equals(IAction? other)
|
||||||
|
|
|
||||||
|
|
@ -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,7 +8,7 @@ namespace Penumbra.UI.AdvancedWindow;
|
||||||
|
|
||||||
public partial class ModEditWindow
|
public partial class ModEditWindow
|
||||||
{
|
{
|
||||||
private class MdlTab : IWritable
|
private partial class MdlTab : IWritable
|
||||||
{
|
{
|
||||||
private ModEditWindow _edit;
|
private ModEditWindow _edit;
|
||||||
|
|
||||||
|
|
@ -18,6 +18,10 @@ public partial class ModEditWindow
|
||||||
|
|
||||||
public bool PendingIo { get; private set; } = false;
|
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)
|
public MdlTab(ModEditWindow edit, byte[] bytes, string path, Mod? mod)
|
||||||
{
|
{
|
||||||
_edit = edit;
|
_edit = edit;
|
||||||
|
|
@ -54,11 +58,61 @@ public partial class ModEditWindow
|
||||||
/// <param name="outputPath"> Disk path to save the resulting file to. </param>
|
/// <param name="outputPath"> Disk path to save the resulting file to. </param>
|
||||||
public void Export(string outputPath)
|
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;
|
PendingIo = true;
|
||||||
_edit._models.ExportToGltf(Mdl, outputPath)
|
_edit._models.ExportToGltf(Mdl, sklb, outputPath)
|
||||||
.ContinueWith(_ => PendingIo = false);
|
.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>
|
/// <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>
|
/// <remarks> Meshes using the removed material are redirected to material 0, and those after the index are corrected. </remarks>
|
||||||
public void RemoveMaterial(int materialIndex)
|
public void RemoveMaterial(int materialIndex)
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,6 @@ public partial class ModEditWindow
|
||||||
{
|
{
|
||||||
tab.Export("C:\\Users\\ackwell\\blender\\gltf-tests\\bingo.gltf");
|
tab.Export("C:\\Users\\ackwell\\blender\\gltf-tests\\bingo.gltf");
|
||||||
}
|
}
|
||||||
if (ImGui.Button("zoingo boingo"))
|
|
||||||
{
|
|
||||||
_models.SkeletonTest();
|
|
||||||
}
|
|
||||||
ImGui.TextUnformatted("blippity blap");
|
ImGui.TextUnformatted("blippity blap");
|
||||||
foreach (var gamePath in tab.GamePaths)
|
foreach (var gamePath in tab.GamePaths)
|
||||||
ImGui.TextUnformatted(gamePath.ToString());
|
ImGui.TextUnformatted(gamePath.ToString());
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue