From beae632721b06ec5cba76efcd88fb1ec012afc21 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Fri, 27 Sep 2024 21:02:29 +1000 Subject: [PATCH] Model space support - package updates - cleanup unused parts - move export type to common options instead of just layout - use export type for character exports - add option to toggle pose export - add option to include model scaling (used by c+ in meddle exports) --- Meddle/Meddle.Plugin/Meddle.Plugin.csproj | 2 +- .../Models/Composer/CharacterComposer.cs | 10 +- Meddle/Meddle.Plugin/Models/Enums.cs | 18 + Meddle/Meddle.Plugin/Models/Groups.cs | 40 - Meddle/Meddle.Plugin/Models/MenuType.cs | 8 - .../Models/Skeletons/ParsedHkaPose.cs | 25 +- Meddle/Meddle.Plugin/Plugin.cs | 32 +- .../Services/AnimationExportService.cs | 106 +++ .../Meddle.Plugin/Services/ComposerFactory.cs | 4 +- .../Meddle.Plugin/Services/ExportService.cs | 473 ---------- Meddle/Meddle.Plugin/Services/UI/CommonUI.cs | 8 +- Meddle/Meddle.Plugin/UI/AnimationTab.cs | 8 +- .../Meddle.Plugin/UI/Archive/CharacterTab.cs | 884 ------------------ .../Meddle.Plugin/UI/Archive/WorldOverlay.cs | 325 ------- .../Meddle.Plugin/UI/Archive/WorldService.cs | 101 -- Meddle/Meddle.Plugin/UI/Archive/WorldTab.cs | 430 --------- Meddle/Meddle.Plugin/UI/DebugTab.cs | 54 +- Meddle/Meddle.Plugin/UI/Layout/Config.cs | 54 +- Meddle/Meddle.Plugin/UI/Layout/Instance.cs | 1 - .../Meddle.Plugin/UI/Layout/LayoutWindow.cs | 2 +- Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs | 93 +- Meddle/Meddle.Plugin/UI/OptionsTab.cs | 100 +- Meddle/Meddle.Plugin/Utils/ExportUtil.cs | 25 + Meddle/Meddle.Plugin/Utils/SkeletonUtils.cs | 133 ++- Meddle/Meddle.Plugin/Utils/UIUtil.cs | 10 +- Meddle/Meddle.Plugin/packages.lock.json | 6 +- Meddle/Meddle.Utils/BoneNodeBuilder.cs | 6 +- 27 files changed, 482 insertions(+), 2476 deletions(-) create mode 100644 Meddle/Meddle.Plugin/Models/Enums.cs delete mode 100644 Meddle/Meddle.Plugin/Models/Groups.cs delete mode 100644 Meddle/Meddle.Plugin/Models/MenuType.cs create mode 100644 Meddle/Meddle.Plugin/Services/AnimationExportService.cs delete mode 100644 Meddle/Meddle.Plugin/Services/ExportService.cs delete mode 100644 Meddle/Meddle.Plugin/UI/Archive/CharacterTab.cs delete mode 100644 Meddle/Meddle.Plugin/UI/Archive/WorldOverlay.cs delete mode 100644 Meddle/Meddle.Plugin/UI/Archive/WorldService.cs delete mode 100644 Meddle/Meddle.Plugin/UI/Archive/WorldTab.cs create mode 100644 Meddle/Meddle.Plugin/Utils/ExportUtil.cs diff --git a/Meddle/Meddle.Plugin/Meddle.Plugin.csproj b/Meddle/Meddle.Plugin/Meddle.Plugin.csproj index ef77e47..d3b5f9a 100644 --- a/Meddle/Meddle.Plugin/Meddle.Plugin.csproj +++ b/Meddle/Meddle.Plugin/Meddle.Plugin.csproj @@ -27,7 +27,7 @@ - + diff --git a/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs b/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs index ba41538..196e49b 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs @@ -20,11 +20,15 @@ public class CharacterComposer private static TexFile? CubeMapTex; private static PbdFile? PbdFile; private static readonly object StaticFileLock = new(); + private readonly SkeletonUtils.PoseMode poseMode; + private readonly bool includePose; - public CharacterComposer(DataProvider dataProvider, Action? progress = null) + public CharacterComposer(DataProvider dataProvider, Configuration config, Action? progress = null) { this.dataProvider = dataProvider; this.progress = progress; + includePose = config.IncludePose; + poseMode = config.PoseMode; lock (StaticFileLock) { @@ -306,7 +310,7 @@ private bool HandleRootAttach(ParsedCharacterInfo characterInfo, SceneBuilder sc BoneNodeBuilder? rootBone; try { - bones = SkeletonUtils.GetBoneMap(characterInfo.Skeleton, true, out rootBone); + bones = SkeletonUtils.GetBoneMap(characterInfo.Skeleton, includePose ? poseMode : null, out rootBone); if (rootBone == null) { Plugin.Logger?.LogWarning("Root bone not found"); @@ -402,7 +406,7 @@ public void ComposeModels(ParsedModelInfo[] models, GenderRace genderRace, Custo BoneNodeBuilder? rootBone; try { - bones = SkeletonUtils.GetBoneMap(skeleton, true, out rootBone); + bones = SkeletonUtils.GetBoneMap(skeleton, includePose ? poseMode : null, out rootBone); if (rootBone == null) { Plugin.Logger?.LogWarning("Root bone not found"); diff --git a/Meddle/Meddle.Plugin/Models/Enums.cs b/Meddle/Meddle.Plugin/Models/Enums.cs new file mode 100644 index 0000000..b3c323b --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Enums.cs @@ -0,0 +1,18 @@ +namespace Meddle.Plugin.Models; + +public enum MenuType +{ + Default = 0, + Debug = 1, + Testing = 2, +} + +[Flags] +public enum ExportType +{ + // ReSharper disable InconsistentNaming + GLTF = 1, + GLB = 2, + OBJ = 4 + // ReSharper restore InconsistentNaming +} diff --git a/Meddle/Meddle.Plugin/Models/Groups.cs b/Meddle/Meddle.Plugin/Models/Groups.cs deleted file mode 100644 index ed80f08..0000000 --- a/Meddle/Meddle.Plugin/Models/Groups.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Numerics; -using Meddle.Plugin.Models.Skeletons; -using Meddle.Utils.Export; -using Meddle.Utils.Files; - -namespace Meddle.Plugin.Models; - -public record CharacterGroup( - CustomizeParameter CustomizeParams, - CustomizeData CustomizeData, - GenderRace GenderRace, - MdlFileGroup[] MdlGroups, - ParsedSkeleton Skeleton, - AttachedModelGroup[] AttachedModelGroups); - -public record AttachedModelGroup(ParsedAttach Attach, MdlFileGroup[] MdlGroups, ParsedSkeleton Skeleton); - -public record MdlFileGroup( - string CharacterPath, - string Path, - DeformerGroup? DeformerGroup, - MdlFile MdlFile, - IMtrlFileGroup[] MtrlFiles, - Model.ShapeAttributeGroup? ShapeAttributeGroup); - -public record MtrlFileStubGroup(string Path) : IMtrlFileGroup; - -public interface IMtrlFileGroup; - -public record MtrlFileGroup( - string MdlPath, - string Path, - MtrlFile MtrlFile, - string ShpkPath, - ShpkFile ShpkFile, - TexResourceGroup[] TexFiles) : IMtrlFileGroup; - -public record TexResourceGroup(string MtrlPath, string Path, TextureResource Resource); - -public record DeformerGroup(string Path, ushort RaceSexId, ushort DeformerId); diff --git a/Meddle/Meddle.Plugin/Models/MenuType.cs b/Meddle/Meddle.Plugin/Models/MenuType.cs deleted file mode 100644 index 0d658fb..0000000 --- a/Meddle/Meddle.Plugin/Models/MenuType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Meddle.Plugin.Models; - -public enum MenuType -{ - Default = 0, - Debug = 1, - Testing = 2, -} diff --git a/Meddle/Meddle.Plugin/Models/Skeletons/ParsedHkaPose.cs b/Meddle/Meddle.Plugin/Models/Skeletons/ParsedHkaPose.cs index 15bcf2d..a43ded2 100644 --- a/Meddle/Meddle.Plugin/Models/Skeletons/ParsedHkaPose.cs +++ b/Meddle/Meddle.Plugin/Models/Skeletons/ParsedHkaPose.cs @@ -10,23 +10,32 @@ public unsafe ParsedHkaPose(Pointer pose) : this(pose.Value) { } public unsafe ParsedHkaPose(hkaPose* pose) { + var boneCount = pose->Skeleton->Bones.Length; + var transforms = new List(); - - var boneCount = pose->LocalPose.Length; + var syncedLocalPose = pose->GetSyncedPoseLocalSpace()->Data; for (var i = 0; i < boneCount; ++i) { - var localSpace = pose->AccessBoneLocalSpace(i); - if (localSpace == null) - { - throw new Exception("Failed to access bone local space"); - } + var localSpace = syncedLocalPose[i]; + transforms.Add(new Transform(localSpace)); + } - transforms.Add(new Transform(*localSpace)); + var modelTransforms = new List(); + var modelSpace = pose->GetSyncedPoseModelSpace()->Data; + for (var i = 0; i < boneCount; ++i) + { + var model = modelSpace[i]; + modelTransforms.Add(new Transform(model)); } + Pose = transforms; + ModelPose = modelTransforms; } [JsonIgnore] public IReadOnlyList Pose { get; } + + [JsonIgnore] + public IReadOnlyList ModelPose { get; } } diff --git a/Meddle/Meddle.Plugin/Plugin.cs b/Meddle/Meddle.Plugin/Plugin.cs index 92add16..11bd9b8 100644 --- a/Meddle/Meddle.Plugin/Plugin.cs +++ b/Meddle/Meddle.Plugin/Plugin.cs @@ -4,8 +4,10 @@ using Dalamud.Configuration; using Dalamud.IoC; using Dalamud.Plugin; +using Meddle.Plugin.Models; using Meddle.Plugin.Services; using Meddle.Plugin.UI.Layout; +using Meddle.Plugin.Utils; using Meddle.Utils.Files.SqPack; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -96,6 +98,9 @@ public void Dispose() public class Configuration : IPluginConfiguration { + public const ExportType DefaultExportType = ExportType.GLTF; + public const SkeletonUtils.PoseMode DefaultPoseMode = SkeletonUtils.PoseMode.Model; + [PluginService] [JsonIgnore] private IDalamudPluginInterface PluginInterface { get; set; } = null!; @@ -106,12 +111,33 @@ public class Configuration : IPluginConfiguration public bool OpenOnLoad { get; set; } public bool DisableUserUiHide { get; set; } public bool DisableAutomaticUiHide { get; set; } - public bool DisableCutsceneUiHide { get; set; } - public bool DisableGposeUiHide { get; set; } + public bool DisableCutsceneUiHide { get; set; } = true; + public bool DisableGposeUiHide { get; set; } = true; public float WorldCutoffDistance { get; set; } = 100; public Vector4 WorldDotColor { get; set; } = new(1f, 1f, 1f, 0.5f); + + /// + /// Used to hide names in the UI + /// public string PlayerNameOverride { get; set; } = string.Empty; - + + /// + /// If enabled, pose will be included at 0 on the timeline under the 'pose' track. + /// + public bool IncludePose { get; set; } = true; + + /// + /// Indicates whether scaling should be taken from the model pose rather than the local pose. + /// + public SkeletonUtils.PoseMode PoseMode { get; set; } = DefaultPoseMode; + + /// + /// GLTF = GLTF JSON + /// GLB = GLTF Binary + /// OBJ = Wavefront OBJ + /// + public ExportType ExportType { get; set; } = DefaultExportType; + public int Version { get; set; } = 1; public LayoutWindow.LayoutConfig LayoutConfig { get; set; } = new(); diff --git a/Meddle/Meddle.Plugin/Services/AnimationExportService.cs b/Meddle/Meddle.Plugin/Services/AnimationExportService.cs new file mode 100644 index 0000000..b5b2700 --- /dev/null +++ b/Meddle/Meddle.Plugin/Services/AnimationExportService.cs @@ -0,0 +1,106 @@ +using System.Diagnostics; +using System.Numerics; +using Meddle.Plugin.Models; +using Meddle.Plugin.Utils; +using Microsoft.Extensions.Logging; +using SharpGLTF.Geometry; +using SharpGLTF.Geometry.VertexTypes; +using SharpGLTF.Materials; +using SharpGLTF.Scenes; + +namespace Meddle.Plugin.Services; + +public class AnimationExportService : IDisposable, IService +{ + private static readonly ActivitySource ActivitySource = new("Meddle.Plugin.Utils.ExportUtil"); + private readonly Configuration configuration; + private readonly ILogger logger; + + public AnimationExportService(Configuration configuration, ILogger logger) + { + this.configuration = configuration; + this.logger = logger; + } + + public void Dispose() + { + logger.LogDebug("Disposing ExportUtil"); + } + + public void ExportAnimation(List<(DateTime, AttachSet[])> frames, bool includePositionalData, string path, CancellationToken token = default) + { + try + { + using var activity = ActivitySource.StartActivity(); + var boneSets = SkeletonUtils.GetAnimatedBoneMap(frames.ToArray(), configuration.PoseMode); + var startTime = frames.Min(x => x.Item1); + //var folder = GetPathForOutput(); + var folder = path; + Directory.CreateDirectory(folder); + foreach (var (id, (bones, root, timeline)) in boneSets) + { + var scene = new SceneBuilder(); + if (root == null) throw new InvalidOperationException("Root bone not found"); + logger.LogInformation("Adding bone set {Id}", id); + + if (includePositionalData) + { + var startPos = timeline.First().Attach.Transform.Translation; + foreach (var frameTime in timeline) + { + if (token.IsCancellationRequested) return; + var pos = frameTime.Attach.Transform.Translation; + var rot = frameTime.Attach.Transform.Rotation; + var scale = frameTime.Attach.Transform.Scale; + var time = SkeletonUtils.TotalSeconds(frameTime.Time, startTime); + root.UseTranslation().UseTrackBuilder("pose").WithPoint(time, pos - startPos); + root.UseRotation().UseTrackBuilder("pose").WithPoint(time, rot); + root.UseScale().UseTrackBuilder("pose").WithPoint(time, scale); + } + } + + scene.AddNode(root); + scene.AddSkinnedMesh(GetDummyMesh(id), Matrix4x4.Identity, bones.Cast().ToArray()); + var sceneGraph = scene.ToGltf2(); + var outputPath = Path.Combine(folder, $"motion_{id}.gltf"); + sceneGraph.SaveGLTF(outputPath); + } + + Process.Start("explorer.exe", folder); + logger.LogInformation("Export complete"); + } + catch (Exception e) + { + logger.LogError(e, "Failed to export animation"); + throw; + } + } + + // https://github.com/0ceal0t/Dalamud-VFXEditor/blob/be00131b93b3c6dd4014a4f27c2661093daf3a85/VFXEditor/Utils/Gltf/GltfSkeleton.cs#L132 + public static MeshBuilder GetDummyMesh(string name = "DUMMY_MESH") + { + var dummyMesh = new MeshBuilder(name); + var material = new MaterialBuilder("material"); + + var p1 = new VertexPosition + { + Position = new Vector3(0.000001f, 0, 0) + }; + var p2 = new VertexPosition + { + Position = new Vector3(0, 0.000001f, 0) + }; + var p3 = new VertexPosition + { + Position = new Vector3(0, 0, 0.000001f) + }; + + dummyMesh.UsePrimitive(material).AddTriangle( + (p1, new VertexEmpty(), new VertexJoints4(0)), + (p2, new VertexEmpty(), new VertexJoints4(0)), + (p3, new VertexEmpty(), new VertexJoints4(0)) + ); + + return dummyMesh; + } +} diff --git a/Meddle/Meddle.Plugin/Services/ComposerFactory.cs b/Meddle/Meddle.Plugin/Services/ComposerFactory.cs index bec167b..2270571 100644 --- a/Meddle/Meddle.Plugin/Services/ComposerFactory.cs +++ b/Meddle/Meddle.Plugin/Services/ComposerFactory.cs @@ -45,11 +45,11 @@ public CharacterComposer CreateCharacterComposer(string? cacheDir = null, Action Directory.CreateDirectory(cacheDir); var dataProvider = CreateDataProvider(cacheDir, cancellationToken); - return new CharacterComposer(dataProvider, progress); + return new CharacterComposer(dataProvider, configuration, progress); } public CharacterComposer CreateCharacterComposer(DataProvider dataProvider) { - return new CharacterComposer(dataProvider); + return new CharacterComposer(dataProvider, configuration); } } diff --git a/Meddle/Meddle.Plugin/Services/ExportService.cs b/Meddle/Meddle.Plugin/Services/ExportService.cs deleted file mode 100644 index 89ac5d1..0000000 --- a/Meddle/Meddle.Plugin/Services/ExportService.cs +++ /dev/null @@ -1,473 +0,0 @@ -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Numerics; -using Meddle.Plugin.Models; -using Meddle.Plugin.Models.Layout; -using Meddle.Plugin.Models.Skeletons; -using Meddle.Plugin.Utils; -using Meddle.Utils; -using Meddle.Utils.Export; -using Meddle.Utils.Files; -using Meddle.Utils.Files.SqPack; -using Meddle.Utils.Materials; -using Microsoft.Extensions.Logging; -using SharpGLTF.Geometry; -using SharpGLTF.Geometry.VertexTypes; -using SharpGLTF.Materials; -using SharpGLTF.Scenes; -using SharpGLTF.Transforms; -using SkiaSharp; -using Material = Meddle.Utils.Export.Material; -using Model = Meddle.Utils.Export.Model; - -namespace Meddle.Plugin.Services; - -public class ExportService : IDisposable, IService -{ - private static readonly ActivitySource ActivitySource = new("Meddle.Plugin.Utils.ExportUtil"); - private readonly TexFile catchlightTex; - private readonly SqPack pack; - private readonly ILogger logger; - private readonly ParseService parseService; - private readonly PbdFile pbdFile; - private readonly TexFile tileNormTex; - private readonly TexFile tileOrbTex; - - public ExportService(SqPack pack, ILogger logger, ParseService parseService) - { - this.pack = pack; - this.logger = logger; - this.parseService = parseService; - - // chara/xls/boneDeformer/human.pbd - var pbdData = pack.GetFile("chara/xls/boneDeformer/human.pbd"); - if (pbdData == null) throw new Exception("Failed to load human.pbd"); - pbdFile = new PbdFile(pbdData.Value.file.RawData); - - var catchlight = pack.GetFile("chara/common/texture/sphere_d_array.tex"); - if (catchlight == null) throw new InvalidOperationException("Failed to get catchlight texture"); - - var tileNorm = pack.GetFile("chara/common/texture/tile_norm_array.tex"); - if (tileNorm == null) throw new InvalidOperationException("Failed to get tile norm texture"); - - var tileOrb = pack.GetFile("chara/common/texture/tile_orb_array.tex"); - if (tileOrb == null) throw new InvalidOperationException("Failed to get tile orb texture"); - tileNormTex = new TexFile(tileNorm.Value.file.RawData); - tileOrbTex = new TexFile(tileOrb.Value.file.RawData); - catchlightTex = new TexFile(catchlight.Value.file.RawData); - } - - public void Dispose() - { - logger.LogDebug("Disposing ExportUtil"); - } - - private static string GetPathForOutput() - { - var now = DateTime.Now; - var folder = Path.Combine(Plugin.TempDirectory, "output", now.ToString("yyyy-MM-dd-HH-mm-ss")); - if (!Directory.Exists(folder)) - { - Directory.CreateDirectory(folder); - } - - return folder; - } - - public static void ExportTexture(SKBitmap bitmap, string path) - { - using var activity = ActivitySource.StartActivity(); - activity?.SetTag("path", path); - var folder = GetPathForOutput(); - var outputPath = Path.Combine(folder, $"{Path.GetFileNameWithoutExtension(path)}.png"); - - using var str = new SKDynamicMemoryWStream(); - bitmap.Encode(str, SKEncodedImageFormat.Png, 100); - - var data = str.DetachAsData().AsSpan(); - File.WriteAllBytes(outputPath, data.ToArray()); - Process.Start("explorer.exe", folder); - } - - public void ExportAnimation(List<(DateTime, AttachSet[])> frames, bool includePositionalData, string path, CancellationToken token = default) - { - try - { - using var activity = ActivitySource.StartActivity(); - var boneSets = SkeletonUtils.GetAnimatedBoneMap(frames.ToArray()); - var startTime = frames.Min(x => x.Item1); - //var folder = GetPathForOutput(); - var folder = path; - Directory.CreateDirectory(folder); - foreach (var (id, (bones, root, timeline)) in boneSets) - { - var scene = new SceneBuilder(); - if (root == null) throw new InvalidOperationException("Root bone not found"); - logger.LogInformation("Adding bone set {Id}", id); - - if (includePositionalData) - { - var startPos = timeline.First().Attach.Transform.Translation; - foreach (var frameTime in timeline) - { - if (token.IsCancellationRequested) return; - var pos = frameTime.Attach.Transform.Translation; - var rot = frameTime.Attach.Transform.Rotation; - var scale = frameTime.Attach.Transform.Scale; - var time = SkeletonUtils.TotalSeconds(frameTime.Time, startTime); - root.UseTranslation().UseTrackBuilder("pose").WithPoint(time, pos - startPos); - root.UseRotation().UseTrackBuilder("pose").WithPoint(time, rot); - root.UseScale().UseTrackBuilder("pose").WithPoint(time, scale); - } - } - - scene.AddNode(root); - scene.AddSkinnedMesh(GetDummyMesh(id), Matrix4x4.Identity, bones.Cast().ToArray()); - var sceneGraph = scene.ToGltf2(); - var outputPath = Path.Combine(folder, $"motion_{id}.gltf"); - sceneGraph.SaveGLTF(outputPath); - } - - Process.Start("explorer.exe", folder); - logger.LogInformation("Export complete"); - } - catch (Exception e) - { - logger.LogError(e, "Failed to export animation"); - throw; - } - } - - // https://github.com/0ceal0t/Dalamud-VFXEditor/blob/be00131b93b3c6dd4014a4f27c2661093daf3a85/VFXEditor/Utils/Gltf/GltfSkeleton.cs#L132 - public static MeshBuilder GetDummyMesh(string name = "DUMMY_MESH") - { - var dummyMesh = new MeshBuilder(name); - var material = new MaterialBuilder("material"); - - var p1 = new VertexPosition - { - Position = new Vector3(0.000001f, 0, 0) - }; - var p2 = new VertexPosition - { - Position = new Vector3(0, 0.000001f, 0) - }; - var p3 = new VertexPosition - { - Position = new Vector3(0, 0, 0.000001f) - }; - - dummyMesh.UsePrimitive(material).AddTriangle( - (p1, new VertexEmpty(), new VertexJoints4(0)), - (p2, new VertexEmpty(), new VertexJoints4(0)), - (p3, new VertexEmpty(), new VertexJoints4(0)) - ); - - return dummyMesh; - } - - public void Export(CharacterGroup characterGroup, string? outputFolder = null, CancellationToken token = default) - { - try - { - using var activity = ActivitySource.StartActivity(); - var scene = new SceneBuilder(); - - var bones = SkeletonUtils.GetBoneMap(characterGroup.Skeleton, true, out var root); - //var bones = XmlUtils.GetBoneMap(characterGroup.Skeletons, out var root); - if (root != null) - { - scene.AddNode(root); - } - - var meshOutput = new List<(Model model, ModelBuilder.MeshExport mesh)>(); - foreach (var mdlGroup in characterGroup.MdlGroups) - { - if (token.IsCancellationRequested) return; - if (mdlGroup.Path.Contains("b0003_top")) continue; - try - { - var meshes = HandleModel(characterGroup.CustomizeData, characterGroup.CustomizeParams, - characterGroup.GenderRace, mdlGroup, ref bones, root, token); - foreach (var mesh in meshes) - { - meshOutput.Add((mesh.model, mesh.mesh)); - } - } - catch (Exception e) - { - logger.LogError(e, "Failed to export model {Path}", mdlGroup.Path); - throw; - } - } - - if (token.IsCancellationRequested) return; - - var meshOutputAttach = - new List<(Matrix4x4 Position, Model Model, ModelBuilder.MeshExport Mesh, BoneNodeBuilder[] Bones)>(); - for (var i = 0; i < characterGroup.AttachedModelGroups.Length; i++) - { - var attachedModelGroup = characterGroup.AttachedModelGroups[i]; - var attachName = characterGroup.Skeleton.PartialSkeletons[attachedModelGroup.Attach.PartialSkeletonIdx] - .HkSkeleton!.BoneNames[(int)attachedModelGroup.Attach.BoneIdx]; - var attachBones = SkeletonUtils.GetBoneMap(attachedModelGroup.Skeleton, true, out var attachRoot); - logger.LogDebug("Adding attach {AttachName} to {ParentBone}", attachName, attachRoot?.BoneName); - if (attachRoot == null) - { - throw new InvalidOperationException("Failed to get attach root"); - } - - attachRoot.SetSuffixRecursively(i); - - if (attachedModelGroup.Attach.OffsetTransform is { } ct) - { - attachRoot.WithLocalScale(ct.Scale); - attachRoot.WithLocalRotation(ct.Rotation); - attachRoot.WithLocalTranslation(ct.Translation); - if (attachRoot.AnimationTracksNames.Contains("pose")) - { - attachRoot.UseScale().UseTrackBuilder("pose").WithPoint(0, ct.Scale); - attachRoot.UseRotation().UseTrackBuilder("pose").WithPoint(0, ct.Rotation); - attachRoot.UseTranslation().UseTrackBuilder("pose").WithPoint(0, ct.Translation); - } - } - - var attachPointBone = - bones.FirstOrDefault(b => b.BoneName.Equals(attachName, StringComparison.Ordinal)); - if (attachPointBone == null) - { - scene.AddNode(attachRoot); - } - else - { - attachPointBone.AddNode(attachRoot); - } - - - var transform = Matrix4x4.Identity; - NodeBuilder c = attachRoot; - while (c != null) - { - transform *= c.LocalMatrix; - c = c.Parent; - } - - foreach (var mdlGroup in attachedModelGroup.MdlGroups) - { - var meshes = HandleModel(characterGroup.CustomizeData, - characterGroup.CustomizeParams, characterGroup.GenderRace, - mdlGroup, ref attachBones, attachPointBone, token); - foreach (var mesh in meshes) - { - meshOutputAttach.Add((transform, mesh.model, mesh.mesh, attachBones.ToArray())); - } - } - } - - foreach (var (model, mesh) in meshOutput) - { - if (token.IsCancellationRequested) return; - AddMesh(scene, Matrix4x4.Identity, model, mesh, bones.ToArray()); - } - - foreach (var (position, model, mesh, attachBones) in meshOutputAttach) - { - if (token.IsCancellationRequested) return; - AddMesh(scene, position, model, mesh, attachBones); - } - - var sceneGraph = scene.ToGltf2(); - if (outputFolder != null) - { - Directory.CreateDirectory(outputFolder); - } - - var folder = outputFolder ?? GetPathForOutput(); - var outputPath = Path.Combine(folder, "character.gltf"); - sceneGraph.SaveGLTF(outputPath); - Process.Start("explorer.exe", folder); - logger.LogInformation("Export complete"); - } - catch (Exception e) - { - logger.LogError(e, "Failed to export character"); - throw; - } - } - - public static void AddMesh( - SceneBuilder scene, Matrix4x4 position, Model model, ModelBuilder.MeshExport mesh, BoneNodeBuilder[] bones) - { - InstanceBuilder instance; - if (bones.Length > 0) - { - instance = scene.AddSkinnedMesh(mesh.Mesh, position, bones.Cast().ToArray()); - } - else - { - instance = scene.AddRigidMesh(mesh.Mesh, position); - } - - ApplyMeshShapes(instance, model, mesh.Shapes); - - // Remove subMeshes that are not enabled - if (mesh.Submesh != null) - { - var enabledAttributes = Model.GetEnabledValues(model.EnabledAttributeMask, - model.AttributeMasks); - - if (!mesh.Submesh.Attributes.All(enabledAttributes.Contains)) - { - instance.Remove(); - } - } - } - - private MaterialBuilder HandleMaterial(CustomizeData customizeData, CustomizeParameter customizeParams, MtrlFileGroup mtrlGroup, string? suffix = null) - { - var material = new Material(mtrlGroup.MdlPath, mtrlGroup.MtrlFile, mtrlGroup.TexFiles.ToDictionary(x => x.MtrlPath, x => x.Resource), mtrlGroup.ShpkFile); - using var activityMtrl = ActivitySource.StartActivity(); - activityMtrl?.SetTag("mtrlPath", material.HandlePath); - - logger.LogInformation("Exporting {HandlePath} => {Path}", material.HandlePath, mtrlGroup.Path); - activityMtrl?.SetTag("shaderPackageName", material.ShaderPackageName); - var name = - $"{Path.GetFileNameWithoutExtension(material.HandlePath)}_{Path.GetFileNameWithoutExtension(material.ShaderPackageName)}"; - if (suffix != null) - { - name += $"_{suffix}"; - } - var builder = material.ShaderPackageName switch - { - "bg.shpk" => MaterialUtility.BuildBg(material, name), - "bgprop.shpk" => MaterialUtility.BuildBgProp(material, name), - "character.shpk" => MaterialUtility.BuildCharacter(material, name), - "characterocclusion.shpk" => MaterialUtility.BuildCharacterOcclusion(material, name), - "characterlegacy.shpk" => MaterialUtility.BuildCharacterLegacy(material, name), - "charactertattoo.shpk" => MaterialUtility.BuildCharacterTattoo( - material, name, customizeParams, customizeData), - "hair.shpk" => MaterialUtility.BuildHair(material, name, customizeParams, - customizeData), - "skin.shpk" => MaterialUtility.BuildSkin(material, name, customizeParams, - customizeData, (tileNormTex, tileOrbTex)), - "iris.shpk" => MaterialUtility.BuildIris(material, name, catchlightTex, customizeParams, - customizeData), - "water.shpk" => MaterialUtility.BuildWater(material, name), - "lightshaft.shpk" => MaterialUtility.BuildLightShaft(material, name), - _ => BuildAndLogFallbackMaterial(material, name) - }; - - return builder; - } - - private MaterialBuilder BuildAndLogFallbackMaterial(Material material, string name) - { - logger.LogWarning("[{Shpk}] Using fallback material for {Path}", material.ShaderPackageName, material.HandlePath); - return MaterialUtility.BuildFallback(material, name); - } - - private List<(Model model, ModelBuilder.MeshExport mesh)> HandleModel( - CustomizeData customizeData, - CustomizeParameter customizeParams, - GenderRace genderRace, - MdlFileGroup mdlGroup, - ref List bones, - BoneNodeBuilder? root, - CancellationToken token, - string? suffix = null) - { - using var activity = ActivitySource.StartActivity(); - activity?.SetTag("characterPath", mdlGroup.CharacterPath); - activity?.SetTag("path", mdlGroup.Path); - logger.LogInformation("Exporting {CharacterPath} => {Path}", mdlGroup.CharacterPath, mdlGroup.Path); - var model = new Model(mdlGroup.CharacterPath, mdlGroup.MdlFile, mdlGroup.ShapeAttributeGroup); - - foreach (var mesh in model.Meshes) - { - if (mesh.BoneTable == null) continue; - - foreach (var boneName in mesh.BoneTable) - { - if (bones.All(b => !b.BoneName.Equals(boneName, StringComparison.Ordinal))) - { - logger.LogInformation("Adding bone {BoneName} from mesh {MeshPath}", boneName, - mdlGroup.Path); - var bone = new BoneNodeBuilder(boneName); - if (root == null) throw new InvalidOperationException("Root bone not found"); - root.AddNode(bone); - logger.LogInformation("Added bone {BoneName} to {ParentBone}", boneName, root.BoneName); - - bones.Add(bone); - } - } - } - - var materials = new List(); - var meshOutput = new List<(Model, ModelBuilder.MeshExport)>(); - foreach (var mtrlFile in mdlGroup.MtrlFiles) - { - if (mtrlFile is MtrlFileGroup mtrlGroup) - { - if (token.IsCancellationRequested) return meshOutput; - var builder = HandleMaterial(customizeData, customizeParams, mtrlGroup, suffix); - materials.Add(builder); - } - else if (mtrlFile is MtrlFileStubGroup stub) - { - var stubName = Path.GetFileNameWithoutExtension(stub.Path); - var builder = new MaterialBuilder($"{stubName}_stub"); - materials.Add(builder); - } - else - { - throw new InvalidOperationException("Unknown mtrl file type"); - } - } - - if (token.IsCancellationRequested) return meshOutput; - - (GenderRace from, GenderRace to, RaceDeformer deformer)? deform; - if (mdlGroup.DeformerGroup != null) - { - // custom pbd may exist - var pbdFileData = pack.GetFileOrReadFromDisk(mdlGroup.DeformerGroup.Path); - if (pbdFileData == null) - throw new InvalidOperationException($"Failed to get deformer pbd {mdlGroup.DeformerGroup.Path}"); - deform = ((GenderRace)mdlGroup.DeformerGroup.DeformerId, (GenderRace)mdlGroup.DeformerGroup.RaceSexId, new RaceDeformer(new PbdFile(pbdFileData), bones)); - logger.LogDebug("Using deformer pbd {Path}", mdlGroup.DeformerGroup.Path); - } - else - { - var parsed = RaceDeformer.ParseRaceCode(mdlGroup.CharacterPath); - if (Enum.IsDefined(parsed)) - { - deform = (parsed, genderRace, new RaceDeformer(pbdFile, bones)); - } - else - { - deform = null; - } - } - - var meshes = ModelBuilder.BuildMeshes(model, materials, bones, deform); - foreach (var mesh in meshes) - { - meshOutput.Add((model, mesh)); - } - - return meshOutput; - } - - private static void ApplyMeshShapes(InstanceBuilder builder, Model model, IReadOnlyList? appliedShapes) - { - if (model.Shapes.Count == 0 || appliedShapes == null) return; - - // This will set the morphing value to 1 if the shape is enabled, 0 if not - var enabledShapes = Model.GetEnabledValues(model.EnabledShapeMask, model.ShapeMasks) - .ToArray(); - var shapes = model.Shapes - .Where(x => appliedShapes.Contains(x.Name)) - .Select(x => (x, enabledShapes.Contains(x.Name))); - builder.Content.UseMorphing().SetValue(shapes.Select(x => x.Item2 ? 1f : 0).ToArray()); - } -} diff --git a/Meddle/Meddle.Plugin/Services/UI/CommonUI.cs b/Meddle/Meddle.Plugin/Services/UI/CommonUI.cs index b18a140..399c224 100644 --- a/Meddle/Meddle.Plugin/Services/UI/CommonUI.cs +++ b/Meddle/Meddle.Plugin/Services/UI/CommonUI.cs @@ -1,5 +1,6 @@ using System.Numerics; using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; @@ -77,8 +78,11 @@ public unsafe void DrawCharacterSelect(ref ICharacter? selectedCharacter) if (clientState.IsGPosing) { - ImGui.SameLine(); - ImGui.TextDisabled("?"); + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGui.Text(FontAwesomeIcon.QuestionCircle.ToIconString()); + } if (ImGui.IsItemHovered()) { ImGui.BeginTooltip(); diff --git a/Meddle/Meddle.Plugin/UI/AnimationTab.cs b/Meddle/Meddle.Plugin/UI/AnimationTab.cs index 1140494..d866633 100644 --- a/Meddle/Meddle.Plugin/UI/AnimationTab.cs +++ b/Meddle/Meddle.Plugin/UI/AnimationTab.cs @@ -16,7 +16,7 @@ namespace Meddle.Plugin.UI; public class AnimationTab : ITab { - private readonly ExportService exportService; + private readonly AnimationExportService animationExportService; private readonly CommonUi commonUi; private readonly List<(DateTime Time, AttachSet[])> frames = []; private readonly IFramework framework; @@ -32,12 +32,12 @@ public class AnimationTab : ITab public AnimationTab( IFramework framework, ILogger logger, - ExportService exportService, + AnimationExportService animationExportService, CommonUi commonUi) { this.framework = framework; this.logger = logger; - this.exportService = exportService; + this.animationExportService = animationExportService; this.commonUi = commonUi; this.framework.Update += OnFrameworkUpdate; } @@ -83,7 +83,7 @@ public void Draw() fileDialog.SaveFolderDialog("Save Animation", folderName, (result, path) => { if (!result) return; - exportService.ExportAnimation(frames, includePositionalData, path); + animationExportService.ExportAnimation(frames, includePositionalData, path); }, Plugin.TempDirectory); diff --git a/Meddle/Meddle.Plugin/UI/Archive/CharacterTab.cs b/Meddle/Meddle.Plugin/UI/Archive/CharacterTab.cs deleted file mode 100644 index 9b9b482..0000000 --- a/Meddle/Meddle.Plugin/UI/Archive/CharacterTab.cs +++ /dev/null @@ -1,884 +0,0 @@ -/*using System.Numerics; -using System.Runtime.InteropServices; -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Interface.Textures; -using Dalamud.Interface.Textures.TextureWraps; -using Dalamud.Interface.Utility.Raii; -using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using FFXIVClientStructs.Interop; -using ImGuiNET; -using Meddle.Plugin.Models; -using Meddle.Plugin.Services; -using Meddle.Plugin.Utils; -using Meddle.Utils; -using Meddle.Utils.Export; -using Meddle.Utils.Files.Structs.Material; -using Meddle.Utils.Materials; -using Meddle.Utils.Models; -using Microsoft.Extensions.Logging; -using CSCharacter = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; -using CustomizeParameter = Meddle.Utils.Export.CustomizeParameter; -using Model = Meddle.Utils.Export.Model; - -namespace Meddle.Plugin.UI; - -public unsafe class CharacterTab : ITab -{ - private readonly Dictionary channelCache = new(); - - private readonly IClientState clientState; - private readonly Configuration config; - private readonly ExportService exportService; - private readonly ILogger log; - private readonly IObjectTable objectTable; - private readonly ParseService parseService; - - private readonly Dictionary textureCache = new(); - private readonly ITextureProvider textureProvider; - - private CharacterGroup? characterGroup; - private Task? exportTask; - private CharacterGroup? selectedSetGroup; - - public CharacterTab( - IObjectTable objectTable, - IClientState clientState, - ILogger log, - ExportService exportService, - ITextureProvider textureProvider, - ParseService parseService, - Configuration config) - { - this.log = log; - this.exportService = exportService; - this.parseService = parseService; - this.config = config; - this.objectTable = objectTable; - this.clientState = clientState; - this.textureProvider = textureProvider; - } - - private ICharacter? SelectedCharacter { get; set; } - - private bool ExportTaskIncomplete => exportTask?.IsCompleted == false; - - - private bool IsDisposed { get; set; } - public string Name => "(Old)Character"; - public int Order => 999; - public bool DisplayTab => config.ShowTesting; - - public void Draw() - { - DrawObjectPicker(); - } - - public void Dispose() - { - if (!IsDisposed) - { - log.LogDebug("Disposing CharacterTab"); - foreach (var (_, textureImage) in textureCache) - { - textureImage.Wrap.Dispose(); - } - - textureCache.Clear(); - IsDisposed = true; - } - } - - private void DrawObjectPicker() - { - // Warning text: - ImGui.TextWrapped("NOTE: Exported models use a rudimentary approximation of the games pixel shaders, " + - "they will likely not match 1:1 to the in-game appearance."); - - ICharacter[] objects; - if (clientState.LocalPlayer != null) - { - objects = objectTable.OfType() - .Where(obj => obj.IsValid() && obj.IsValidCharacterBase()) - .OrderBy(c => clientState.GetDistanceToLocalPlayer(c).LengthSquared()) - .ToArray(); - } - else - { - // login/char creator produces "invalid" characters but are still usable I guess - objects = objectTable.OfType() - .Where(obj => obj.IsValidHuman()) - .OrderBy(c => clientState.GetDistanceToLocalPlayer(c).LengthSquared()) - .ToArray(); - } - - SelectedCharacter ??= objects.FirstOrDefault() ?? clientState.LocalPlayer; - - ImGui.Text("Select Character"); - var preview = SelectedCharacter != null - ? clientState.GetCharacterDisplayText(SelectedCharacter, config.PlayerNameOverride) - : "None"; - using (var combo = ImRaii.Combo("##Character", preview)) - { - if (combo) - { - foreach (var character in objects) - { - if (ImGui.Selectable(clientState.GetCharacterDisplayText(character, config.PlayerNameOverride))) - { - SelectedCharacter = character; - } - } - } - } - - if (exportTask is {IsFaulted: true}) - { - ImGui.Text("An Error Occured: " + exportTask.Exception?.Message); - } - - if (SelectedCharacter != null) - { - ImGui.BeginDisabled(ExportTaskIncomplete); - if (ImGui.Button("Parse")) - { - exportTask = ParseCharacter(SelectedCharacter); - } - - ImGui.EndDisabled(); - } - else - { - ImGui.TextWrapped("No character selected"); - } - - - if (characterGroup == null) - { - ImGui.Text("Parse a character to view data"); - return; - } - - DrawCharacterGroup(); - } - - private void DrawCharacterGroup() - { - ImGui.PushID(characterGroup!.GetHashCode()); - var availWidth = ImGui.GetContentRegionAvail().X; - ImGui.Columns(2, "Customize", true); - try - { - ImGui.SetColumnWidth(0, availWidth * 0.8f); - ImGui.Text("Customize Parameters"); - ImGui.NextColumn(); - ImGui.Text("Customize Data"); - ImGui.NextColumn(); - ImGui.Separator(); - // draw customizeparams - var customizeParams = characterGroup.CustomizeParams; - UiUtil.DrawCustomizeParams(ref customizeParams); - ImGui.NextColumn(); - // draw customize data - - var customizeData = characterGroup.CustomizeData; - UiUtil.DrawCustomizeData(customizeData); - ImGui.Text(characterGroup.GenderRace.ToString()); - - ImGui.NextColumn(); - } finally - { - ImGui.Columns(1); - } - - ImGui.Separator(); - - DrawExportOptions(); - - if (ImGui.BeginTable("CharacterTable", 2, ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable)) - { - ImGui.TableSetupColumn("Options", ImGuiTableColumnFlags.WidthFixed, 100); - // Export button, export toggle checkbox - ImGui.TableSetupColumn("Character Data", ImGuiTableColumnFlags.WidthStretch); - // Character data - ImGui.TableHeadersRow(); - - foreach (var mdlGroup in characterGroup.MdlGroups) - { - ImGui.PushID(mdlGroup.GetHashCode()); - ImGui.TableNextRow(); - ImGui.TableSetColumnIndex(0); - if (ImGui.Button("Export")) - { - exportTask = Task.Run(() => - { - try - { - exportService.Export(characterGroup with - { - MdlGroups = [mdlGroup], AttachedModelGroups = [] - }); - } - catch (Exception e) - { - log.LogError(e, "Failed to export mdl group"); - throw; - } - }); - } - - if (selectedSetGroup != null) - { - ImGui.SameLine(); - // checkbox for selecting - var selected = selectedSetGroup.MdlGroups.Contains(mdlGroup); - if (ImGui.Checkbox($"##{mdlGroup.GetHashCode()}", ref selected)) - { - // if selected, make sure mdlgroup is in selectedSetGroup - if (selected) - { - selectedSetGroup = selectedSetGroup with - { - MdlGroups = selectedSetGroup.MdlGroups.Append(mdlGroup).ToArray() - }; - } - else - { - selectedSetGroup = selectedSetGroup with - { - MdlGroups = selectedSetGroup.MdlGroups.Where(m => m != mdlGroup).ToArray() - }; - } - } - } - - ImGui.TableSetColumnIndex(1); - if (ImGui.CollapsingHeader(mdlGroup.CharacterPath)) - { - ImGui.Indent(); - DrawMdlGroup(mdlGroup); - ImGui.Unindent(); - } - - ImGui.PopID(); - } - - ImGui.EndTable(); - } - - - if (characterGroup.AttachedModelGroups.Length > 0) - { - if (ImGui.BeginTable("AttachedCharacterTable", 2, ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable)) - { - ImGui.TableSetupColumn("Options", ImGuiTableColumnFlags.WidthFixed, 100); - // Export button, export toggle checkbox - ImGui.TableSetupColumn("Attached Character Data", ImGuiTableColumnFlags.WidthStretch); - // Character data - ImGui.TableHeadersRow(); - - foreach (var attachedModelGroup in characterGroup.AttachedModelGroups) - { - ImGui.PushID(attachedModelGroup.GetHashCode()); - ImGui.TableNextRow(); - ImGui.TableSetColumnIndex(0); - if (ImGui.Button("Export")) - { - exportTask = Task.Run(() => - { - try - { - exportService.Export(characterGroup with - { - AttachedModelGroups = [attachedModelGroup], MdlGroups = [] - }); - } - catch (Exception e) - { - log.LogError(e, "Failed to export attached model group"); - throw; - } - }); - } - - if (selectedSetGroup != null) - { - ImGui.SameLine(); - // checkbox for selecting - var selected = selectedSetGroup.AttachedModelGroups.Contains(attachedModelGroup); - if (ImGui.Checkbox($"##{attachedModelGroup.GetHashCode()}", ref selected)) - { - // if selected, make sure mdlgroup is in selectedSetGroup - if (selected) - { - selectedSetGroup = selectedSetGroup with - { - AttachedModelGroups = selectedSetGroup.AttachedModelGroups - .Append(attachedModelGroup).ToArray() - }; - } - else - { - selectedSetGroup = selectedSetGroup with - { - AttachedModelGroups = selectedSetGroup.AttachedModelGroups - .Where(m => m != attachedModelGroup).ToArray() - }; - } - } - } - - ImGui.TableSetColumnIndex(1); - foreach (var mdlGroup in attachedModelGroup.MdlGroups) - { - ImGui.PushID(mdlGroup.GetHashCode()); - if (ImGui.CollapsingHeader(mdlGroup.CharacterPath)) - { - ImGui.Indent(); - DrawMdlGroup(mdlGroup); - ImGui.Unindent(); - } - - ImGui.PopID(); - } - - ImGui.PopID(); - } - - ImGui.EndTable(); - } - } - - - ImGui.PopID(); - } - - private void DrawExportOptions() - { - if (characterGroup == null) return; - var availWidth = ImGui.GetContentRegionAvail().X; - - ImGui.BeginDisabled(ExportTaskIncomplete); - if (ImGui.Button("Export All")) - { - exportTask = Task.Run(() => - { - try - { - exportService.Export(characterGroup); - } - catch (Exception e) - { - log.LogError(e, "Failed to export character"); - throw; - } - }); - } - - ImGui.SameLine(); - var selectedCount = (selectedSetGroup?.MdlGroups.Length ?? 0) + - (selectedSetGroup?.AttachedModelGroups.Length ?? 0); - if (ImGui.Button($"Export {selectedCount} Selected##ExportSelected")) - { - if (selectedSetGroup == null) - { - log.LogWarning("No selected set group"); - return; - } - - exportTask = Task.Run(() => - { - try - { - exportService.Export(selectedSetGroup); - } - catch (Exception e) - { - log.LogError(e, "Failed to export selected set group"); - throw; - } - }); - } - - ImGui.SameLine(); - if (ImGui.Button("Export Raw Textures")) - { - exportTask = Task.Run(() => - { - try - { - exportService.ExportRawTextures(characterGroup); - } - catch (Exception e) - { - log.LogError(e, "Failed to export raw textures"); - throw; - } - }); - } - - ImGui.EndDisabled(); - } - - private Task ParseCharacter(ICharacter character) - { - if (!exportTask?.IsCompleted ?? false) - { - return exportTask; - } - - var charPtr = (CSCharacter*)character.Address; - var drawObject = charPtr->GameObject.DrawObject; - if (drawObject == null) - { - return Task.CompletedTask; - } - - var objectType = drawObject->Object.GetObjectType(); - if (objectType != ObjectType.CharacterBase) - throw new InvalidOperationException($"Object type is not CharacterBase: {objectType}"); - - var modelType = ((CharacterBase*)drawObject)->GetModelType(); - var characterBase = (CharacterBase*)drawObject; - CustomizeParameter customizeParams; - CustomizeData customizeData; - GenderRace genderRace; - if (modelType == CharacterBase.ModelType.Human) - { - var human = (Human*)drawObject; - var customizeCBuf = human->CustomizeParameterCBuffer->TryGetBuffer()[0]; - customizeParams = new CustomizeParameter - { - SkinColor = customizeCBuf.SkinColor, - MuscleTone = customizeCBuf.MuscleTone, - SkinFresnelValue0 = customizeCBuf.SkinFresnelValue0, - LipColor = customizeCBuf.LipColor, - MainColor = customizeCBuf.MainColor, - FacePaintUVMultiplier = customizeCBuf.FacePaintUVMultiplier, - HairFresnelValue0 = customizeCBuf.HairFresnelValue0, - MeshColor = customizeCBuf.MeshColor, - FacePaintUVOffset = customizeCBuf.FacePaintUVOffset, - LeftColor = customizeCBuf.LeftColor, - RightColor = customizeCBuf.RightColor, - OptionColor = customizeCBuf.OptionColor - }; - customizeData = new CustomizeData - { - LipStick = human->Customize.Lipstick, - Highlights = human->Customize.Highlights - }; - genderRace = (GenderRace)human->RaceSexId; - } - else - { - customizeParams = new CustomizeParameter(); - customizeData = new CustomizeData(); - genderRace = GenderRace.Unknown; - } - - var colorTableTextures = parseService.ParseColorTableTextures(characterBase); - - var attachDict = new Dictionary, Dictionary>(); - if (charPtr->Mount.MountObject != null) - { - var mountDrawObject = charPtr->Mount.MountObject->GameObject.DrawObject; - if (mountDrawObject != null && mountDrawObject->Object.GetObjectType() == ObjectType.CharacterBase) - { - var mountBase = (CharacterBase*)mountDrawObject; - var mountColorTableTextures = parseService.ParseColorTableTextures(mountBase); - attachDict[mountBase] = mountColorTableTextures; - } - } - - if (charPtr->OrnamentData.OrnamentObject != null) - { - var ornamentDrawObject = charPtr->OrnamentData.OrnamentObject->DrawObject; - if (ornamentDrawObject != null && ornamentDrawObject->Object.GetObjectType() == ObjectType.CharacterBase) - { - var ornamentBase = (CharacterBase*)ornamentDrawObject; - var ornamentColorTableTextures = parseService.ParseColorTableTextures(ornamentBase); - attachDict[ornamentBase] = ornamentColorTableTextures; - } - } - - if (charPtr->DrawData.IsWeaponHidden == false) - { - var weaponDataSpan = charPtr->DrawData.WeaponData; - foreach (var weaponData in weaponDataSpan) - { - var draw = weaponData.DrawObject; - if (draw == null) - { - continue; - } - - if (draw->Object.GetObjectType() != ObjectType.CharacterBase) - { - continue; - } - - var weaponBase = (CharacterBase*)draw; - var weaponColorTableTextures = parseService.ParseColorTableTextures(weaponBase); - attachDict[weaponBase] = weaponColorTableTextures; - } - } - - // begin background work - try - { - characterGroup = parseService.HandleCharacterGroup(characterBase, colorTableTextures, attachDict, - customizeParams, customizeData, genderRace); - selectedSetGroup = characterGroup; - } - catch (Exception e) - { - log.LogError(e, "Failed to parse character"); - throw; - } - - return Task.CompletedTask; - } - - private void DrawMdlGroup(MdlFileGroup mdlGroup) - { - ImGui.Text($"Character Path: {mdlGroup.CharacterPath}"); - ImGui.Text($"Path: {mdlGroup.Path}"); - ImGui.Text($"Mtrl Files: {mdlGroup.MtrlFiles.Length}"); - - if (mdlGroup.DeformerGroup != null) - { - ImGui.Text($"Deformer Path: {mdlGroup.DeformerGroup.Path}"); - ImGui.Text($"RaceSexId: {mdlGroup.DeformerGroup.RaceSexId}"); - ImGui.Text($"DeformerId: {mdlGroup.DeformerGroup.DeformerId}"); - } - else - { - ImGui.Text("No Deformer Group"); - } - - var shouldShowShapeAttributeMenu = - mdlGroup.ShapeAttributeGroup is {ShapeMasks.Length: > 0} or {AttributeMasks.Length: > 0}; - - if (shouldShowShapeAttributeMenu && ImGui.CollapsingHeader("Shape/Attribute Masks")) - { - var enabledShapes = Model.GetEnabledValues(mdlGroup.ShapeAttributeGroup!.EnabledShapeMask, - mdlGroup.ShapeAttributeGroup.ShapeMasks).ToArray(); - var enabledAttributes = Model.GetEnabledValues(mdlGroup.ShapeAttributeGroup.EnabledAttributeMask, - mdlGroup.ShapeAttributeGroup.AttributeMasks).ToArray(); - - if (ImGui.BeginTable("ShapeAttributeTable", 2, ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable)) - { - ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthFixed, 100); - ImGui.TableSetupColumn("Enabled", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableHeadersRow(); - - foreach (var shape in mdlGroup.ShapeAttributeGroup.ShapeMasks) - { - ImGui.TableNextRow(); - ImGui.TableSetColumnIndex(0); - ImGui.Text($"[{shape.id}] {shape.name}"); - ImGui.TableSetColumnIndex(1); - ImGui.Text(enabledShapes.Contains(shape.name) ? "Yes" : "No"); - } - - foreach (var attribute in mdlGroup.ShapeAttributeGroup.AttributeMasks) - { - ImGui.TableNextRow(); - ImGui.TableSetColumnIndex(0); - ImGui.Text($"[{attribute.id}] {attribute.name}"); - ImGui.TableSetColumnIndex(1); - ImGui.Text(enabledAttributes.Contains(attribute.name) ? "Yes" : "No"); - } - - ImGui.EndTable(); - } - } - - foreach (var mtrlGroup in mdlGroup.MtrlFiles) - { - ImGui.PushID(mtrlGroup.GetHashCode()); - if (ImGui.CollapsingHeader($"{mtrlGroup.MdlPath}")) - { - try - { - ImGui.Indent(); - DrawMtrlGroup(mtrlGroup); - } finally - { - ImGui.Unindent(); - } - } - - ImGui.PopID(); - } - } - - private void DrawMtrlGroup(MtrlFileGroup mtrlGroup) - { - ImGui.Text($"Mdl Path: {mtrlGroup.MdlPath}"); - ImGui.Text($"Path: {mtrlGroup.Path}"); - ImGui.Text($"Shpk Path: {mtrlGroup.ShpkPath}"); - ImGui.Text($"Tex Files: {mtrlGroup.TexFiles.Length}"); - - if (ImGui.CollapsingHeader($"Constants##{mtrlGroup.GetHashCode()}")) - { - if (ImGui.BeginTable("ConstantsTable", 4, ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable)) - { - ImGui.TableSetupColumn("ID", ImGuiTableColumnFlags.WidthFixed, 100); - ImGui.TableSetupColumn("Offset", ImGuiTableColumnFlags.WidthFixed, 100); - ImGui.TableSetupColumn("Size", ImGuiTableColumnFlags.WidthFixed, 100); - ImGui.TableSetupColumn("Values", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableHeadersRow(); - - foreach (var constant in mtrlGroup.MtrlFile.Constants) - { - var index = constant.ValueOffset / 4; - var count = constant.ValueSize / 4; - var buf = new List(128); - for (var j = 0; j < count; j++) - { - var value = mtrlGroup.MtrlFile.ShaderValues[index + j]; - var bytes = BitConverter.GetBytes(value); - buf.AddRange(bytes); - } - - // Display as floats - var floats = MemoryMarshal.Cast(buf.ToArray()); - - ImGui.TableNextRow(); - ImGui.TableSetColumnIndex(0); - ImGui.Text($"0x{constant.ConstantId:X4}"); - // If has named value in MaterialConstant enum, display - if (Enum.IsDefined(typeof(MaterialConstant), constant.ConstantId)) - { - ImGui.SameLine(); - ImGui.Text($"({(MaterialConstant)constant.ConstantId})"); - } - - ImGui.TableSetColumnIndex(1); - ImGui.Text($"{constant.ValueOffset:X4}"); - - ImGui.TableSetColumnIndex(2); - ImGui.Text($"{count}"); - - ImGui.TableSetColumnIndex(3); - ImGui.Text(string.Join(", ", floats.ToArray())); - } - - ImGui.EndTable(); - } - } - - if (ImGui.CollapsingHeader($"Shader Keys##{mtrlGroup.GetHashCode()}")) - { - var keys = mtrlGroup.MtrlFile.ShaderKeys; - if (ImGui.BeginTable("ShaderKeysTable", 2, ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable)) - { - ImGui.TableSetupColumn("Category", ImGuiTableColumnFlags.WidthFixed, 150); - ImGui.TableSetupColumn("Value", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableHeadersRow(); - - foreach (var key in keys) - { - ImGui.TableNextRow(); - ImGui.TableSetColumnIndex(0); - ImGui.Text($"0x{key.Category:X8}"); - if (Enum.IsDefined(typeof(ShaderCategory), key.Category)) - { - ImGui.SameLine(); - ImGui.Text($"({(ShaderCategory)key.Category})"); - } - - ImGui.TableSetColumnIndex(1); - var catText = $"0x{key.Value:X8}"; - var catSuffix = (ShaderCategory)key.Category switch - { - ShaderCategory.CategoryHairType => $" ({(HairType)key.Value})", - ShaderCategory.CategorySkinType => $" ({(SkinType)key.Value})", - ShaderCategory.CategoryFlowMapType => $" ({(FlowType)key.Value})", - ShaderCategory.CategoryTextureType => $" ({(TextureMode)key.Value})", - ShaderCategory.CategorySpecularType => $" ({(SpecularMode)key.Value})", - _ => "" - }; - - ImGui.Text($"{catText}{catSuffix}"); - } - - ImGui.EndTable(); - } - } - - - if (ImGui.CollapsingHeader($"Color Table##{mtrlGroup.GetHashCode()}")) - { - UiUtil.DrawColorTable(mtrlGroup.MtrlFile); - } - - foreach (var texGroup in mtrlGroup.TexFiles) - { - if (ImGui.CollapsingHeader($"{texGroup.MtrlPath}##{texGroup.GetHashCode()}")) - { - DrawTexGroup(texGroup); - } - } - } - - private void DrawTexGroup(TexResourceGroup texGroup) - { - ImGui.Text($"Mtrl Path: {texGroup.MtrlPath}"); - ImGui.Text($"Path: {texGroup.Path}"); - - ImGui.PushID(texGroup.GetHashCode()); - try - { - DrawTexFile(texGroup.MtrlPath, texGroup.Resource); - } finally - { - ImGui.PopID(); - } - } - - private void DrawTexFile(string path, TextureResource file) - { - ImGui.Text($"Width: {file.Width}"); - ImGui.Text($"Height: {file.Height}"); - //ImGui.Text($"Depth: {file}"); - ImGui.Text($"Mipmaps: {file.MipLevels}"); - - // select channels - if (!channelCache.TryGetValue(path, out var channels)) - { - channels = Channel.Rgb; - channelCache[path] = channels; - } - - var channelsInt = (int)channels; - var changed = false; - ImGui.Text("Channels"); - if (ImGui.CheckboxFlags($"Red##{path}", ref channelsInt, (int)Channel.Red)) - { - changed = true; - } - - ImGui.SameLine(); - if (ImGui.CheckboxFlags($"Green##{path}", ref channelsInt, (int)Channel.Green)) - { - changed = true; - } - - ImGui.SameLine(); - if (ImGui.CheckboxFlags($"Blue##{path}", ref channelsInt, (int)Channel.Blue)) - { - changed = true; - } - - ImGui.SameLine(); - if (ImGui.CheckboxFlags($"Alpha##{path}", ref channelsInt, (int)Channel.Alpha)) - { - changed = true; - } - - channels = (Channel)channelsInt; - channelCache[path] = channels; - - if (changed) - { - textureCache.Remove(path); - } - - if (!textureCache.TryGetValue(path, out var textureImage)) - { - var texture = new Texture(file, path, null, null, null); - var bitmap = texture.ToTexture(); - - // remove channels - if (channels != Channel.All) - { - for (var x = 0; x < bitmap.Width; x++) - for (var y = 0; y < bitmap.Height; y++) - { - var pixel = bitmap[x, y]; - var newPixel = new Vector4(); - if (channels.HasFlag(Channel.Red)) - { - newPixel.X = pixel.Red / 255f; - } - - if (channels.HasFlag(Channel.Green)) - { - newPixel.Y = pixel.Green / 255f; - } - - if (channels.HasFlag(Channel.Blue)) - { - newPixel.Z = pixel.Blue / 255f; - } - - if (channels.HasFlag(Channel.Alpha)) - { - newPixel.W = pixel.Alpha / 255f; - } - else - { - newPixel.W = 1f; - } - - // if only alpha, set rgb to alpha and alpha to 1 - if (channels == Channel.Alpha) - { - newPixel.X = newPixel.W; - newPixel.Y = newPixel.W; - newPixel.Z = newPixel.W; - newPixel.W = 1f; - } - - bitmap[x, y] = newPixel.ToSkColor(); - } - } - - var pixelSpan = bitmap.Bitmap.GetPixelSpan(); - var pixelsCopy = new byte[pixelSpan.Length]; - pixelSpan.CopyTo(pixelsCopy); - var wrap = textureProvider.CreateFromRaw( - RawImageSpecification.Rgba32(file.Width, file.Height), pixelsCopy, - "Meddle.Texture"); - - textureImage = new TextureImage(bitmap, wrap); - textureCache[path] = textureImage; - } - - var availableWidth = ImGui.GetContentRegionAvail().X; - float displayWidth; - float displayHeight; - if (file.Width > availableWidth) - { - displayWidth = availableWidth; - displayHeight = file.Height * (displayWidth / file.Width); - } - else - { - displayWidth = file.Width; - displayHeight = file.Height; - } - - ImGui.Image(textureImage.Wrap.ImGuiHandle, new Vector2(displayWidth, displayHeight)); - - if (ImGui.Button("Export as .png")) - { - ExportService.ExportTexture(textureImage.Bitmap.Bitmap, path); - } - } - - private enum Channel - { - Red = 1, - Green = 2, - Blue = 4, - Alpha = 8, - Rgb = Red | Green | Blue, - All = Red | Green | Blue | Alpha - } - - private record TextureImage(SKTexture Bitmap, IDalamudTextureWrap Wrap); -} -*/ diff --git a/Meddle/Meddle.Plugin/UI/Archive/WorldOverlay.cs b/Meddle/Meddle.Plugin/UI/Archive/WorldOverlay.cs deleted file mode 100644 index e46f657..0000000 --- a/Meddle/Meddle.Plugin/UI/Archive/WorldOverlay.cs +++ /dev/null @@ -1,325 +0,0 @@ -/* -using System.Numerics; -using Dalamud.Interface.Utility; -using Dalamud.Interface.Utility.Raii; -using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using FFXIVClientStructs.Interop; -using ImGuiNET; -using Meddle.Plugin.Models.Structs; -using Meddle.Plugin.Services; -using Microsoft.Extensions.Logging; -using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object; - -namespace Meddle.Plugin.UI; - -public class WorldOverlay : IOverlay -{ - private readonly ILogger log; - private readonly WorldService worldService; - private readonly IClientState clientState; - private readonly IGameGui gui; - private readonly SigUtil sigUtil; - private readonly HousingService housingService; - - public WorldOverlay( - ILogger log, - WorldService worldService, - IClientState clientState, - IGameGui gui, - SigUtil sigUtil, - HousingService housingService) - { - this.log = log; - this.worldService = worldService; - this.clientState = clientState; - this.gui = gui; - this.sigUtil = sigUtil; - this.housingService = housingService; - } - - private unsafe List> RecurseWorldObjects( - Object.SiblingEnumerator siblingEnumerator, HashSet> visited) - { - var worldObjects = new List>(); - foreach (var childObject in siblingEnumerator) - { - if (childObject == null) - continue; - if (!visited.Add(childObject)) - continue; - worldObjects.Add(childObject); - worldObjects.AddRange(RecurseWorldObjects(childObject->ChildObjects, visited)); - } - - return worldObjects; - } - - public unsafe bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos) { - var device = sigUtil.GetDevice(); - var camera = sigUtil.GetCamera(); - float width = device->Width; - float height = device->Height; - var pCoords = Vector4.Transform(new Vector4(worldPos, 1f), camera->ViewMatrix * camera->RenderCamera->ProjectionMatrix); - if (Math.Abs(pCoords.W) < float.Epsilon) { - screenPos = Vector2.Zero; - return false; - } - - pCoords *= MathF.Abs(1.0f / pCoords.W); - screenPos = new Vector2 { - X = (pCoords.X + 1.0f) * width * 0.5f, - Y = (1.0f - pCoords.Y) * height * 0.5f - }; - - return IsOnScreen(new Vector3(pCoords.X, pCoords.Y, pCoords.Z)); - - static bool IsOnScreen(Vector3 pos) { - return -1.0 <= pos.X && pos.X <= 1.0 && -1.0 <= pos.Y && pos.Y <= 1.0 && pos.Z <= 1.0 && 0.0 <= pos.Z; - } - } - - public unsafe void DrawWorldOverlay() - { - var world = sigUtil.GetWorld(); - if (world == null) - { - log.LogError("World instance is null"); - return; - } - - var camera = sigUtil.GetCamera(); - var localPos = clientState.LocalPlayer?.Position ?? Vector3.Zero; - var worldObjects = RecurseWorldObjects(world->ChildObjects, []); - - var hoveredInFrame = new List>(); - foreach (var wo in worldObjects) - { - if (wo == null || wo.Value == null) - continue; - - var childObject = wo.Value; - var drawObject = (DrawObject*)childObject; - if (!drawObject->IsVisible) continue; - if (Vector3.Abs(childObject->Position - camera->Position).Length() > worldService.CutoffDistance) continue; - Vector2 screenPos; - if (worldService.ResolveUsingGameGui) - { - if (!gui.WorldToScreen(childObject->Position, out screenPos)) continue; - } - else - { - if (!camera->WorldToScreen(childObject->Position, out var csScreenPos)) continue; - screenPos = csScreenPos; - } - - var bg = ImGui.GetBackgroundDrawList(); - bg.AddCircleFilled(screenPos, 5, ImGui.GetColorU32(worldService.DotColor)); - - if (ImGui.IsMouseHoveringRect(screenPos - new Vector2(5, 5), - screenPos + new Vector2(5, 5)) && - !ImGui.IsWindowHovered(ImGuiHoveredFlags.AnyWindow)) - { - hoveredInFrame.Add(childObject); - } - } - - if (worldService.ShouldAddAllInRange) - { - worldService.ShouldAddAllInRange = false; - foreach (var wo in worldObjects) - { - if (wo == null || wo.Value == null) - continue; - - var childObject = wo.Value; - if (Vector3.Abs((Vector3)childObject->Position - localPos).Length() > worldService.CutoffDistance) - continue; - worldService.SelectedObjects[(nint)childObject] = ParseObject(wo); - } - } - - if (hoveredInFrame.Count != 0) - { - var housingItems = housingService.GetHousingItems(); - var housingMap = new Dictionary(); - foreach (var housingItem in housingItems) - { - foreach (var bgPart in housingItem.Value.BgParts) - { - if (bgPart == null || bgPart.Value == null) - continue; - housingMap[(nint)bgPart.Value] = housingItem.Value; - } - } - - using var tt = ImRaii.Tooltip(); - for (var i = 0; i < hoveredInFrame.Count; i++) - { - var worldObj = hoveredInFrame[i]; - var childObject = worldObj.Value; - var type = childObject->GetObjectType(); - ImGui.Separator(); - ImGui.Text($"Address: {(nint)childObject:X8}"); - ImGui.Text($"Type: {type}"); - if (WorldService.IsSupportedObject(type)) - { - var path = WorldService.GetPath(childObject); - ImGui.Text($"Path: {path}"); - } - - ImGui.Text($"Position: {childObject->Position}"); - ImGui.Text($"Rotation: {childObject->Rotation}"); - ImGui.Text($"Scale: {childObject->Scale}"); - - if (housingMap.TryGetValue((nint)childObject, out var item)) - { - ImGui.Text($"Furniture: {item.Object->NameString}"); - ImGui.Text($"Index: {item.Furniture.Index}"); - if (item.Stain != null) - { - ImGui.Text($"Stain: {item.Furniture.Stain}"); - ImGui.SameLine(); - var stainColor = ImGui.ColorConvertU32ToFloat4(item.Stain.Color); - ImGui.ColorButton("Stain", stainColor, ImGuiColorEditFlags.NoPicker | ImGuiColorEditFlags.NoTooltip); - } - } - - ImGui.SetNextFrameWantCaptureMouse(true); - if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) - { - worldService.SelectedObjects[(nint)childObject] = ParseObject(worldObj); - } - } - } - } - - public unsafe void DrawHousingOverlay() - { - var housingItems = housingService.GetHousingItems(); - if (housingItems.Count == 0) return; - - var camera = sigUtil.GetCamera(); - var localPos = clientState.LocalPlayer?.Position ?? Vector3.Zero; - - foreach (var item in housingItems.Values) - { - var childObject = item.Object; - if (Vector3.Abs((Vector3)childObject->Position - localPos).Length() > worldService.CutoffDistance) - continue; - Vector2 screenPos; - if (worldService.ResolveUsingGameGui) - { - if (!gui.WorldToScreen(childObject->Position, out screenPos)) continue; - } - else - { - if (!camera->WorldToScreen(childObject->Position, out var csScreenPos)) continue; - screenPos = csScreenPos; - } - - var bg = ImGui.GetBackgroundDrawList(); - bg.AddCircleFilled(screenPos, 5, ImGui.GetColorU32(worldService.DotColor)); - - if (ImGui.IsMouseHoveringRect(screenPos - new Vector2(5, 5), - screenPos + new Vector2(5, 5)) && - !ImGui.IsWindowHovered(ImGuiHoveredFlags.AnyWindow)) - { - using var tt = ImRaii.Tooltip(); - ImGui.Separator(); - ImGui.Text($"Address: {(nint)childObject:X8}"); - ImGui.Text($"Kind: {item.Object->GetObjectKind()}"); - ImGui.Text($"Furniture: {item.Object->NameString}"); - ImGui.Text($"Index: {item.Furniture.Index}"); - if (item.Stain != null) - { - ImGui.Text($"Stain: {item.Furniture.Stain}"); - ImGui.SameLine(); - var stainColor = ImGui.ColorConvertU32ToFloat4(item.Stain.Color); - ImGui.ColorButton("Stain", stainColor, ImGuiColorEditFlags.NoPicker | ImGuiColorEditFlags.NoTooltip); - } - - foreach (var bgObject in item.BgParts) - { - if (bgObject == null || bgObject.Value == null) - continue; - - var draw = bgObject.Value; - - ImGui.Text($"File: {draw->ModelResourceHandle->ResourceHandle.FileName}"); - ImGui.Text($"Position: {draw->DrawObject.Position}"); - ImGui.Text($"Rotation: {draw->DrawObject.Rotation}"); - ImGui.Text($"Scale: {draw->DrawObject.Scale}"); - } - - ImGui.SetNextFrameWantCaptureMouse(true); - if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) - { - foreach (var bgObject in item.BgParts) - { - if (bgObject == null || bgObject.Value == null) - continue; - worldService.SelectedObjects[(nint)bgObject.Value] = ParseHousingBgObject((Object*)bgObject.Value, item); - } - } - } - } - } - - public void DrawOverlay() - { - if (!worldService.ShouldDrawOverlay) - return; - worldService.ShouldDrawOverlay = false; - - switch (worldService.Overlay) - { - case WorldService.OverlayType.World: - DrawWorldOverlay(); - break; - case WorldService.OverlayType.Housing: - DrawHousingOverlay(); - break; - } - } - - private unsafe WorldService.ObjectSnapshot ParseObject(Pointer obj) - { - var type = obj.Value->GetObjectType(); - return type switch - { - ObjectType.BgObject => ParseBgObject(obj), - ObjectType.Terrain => ParseTerrain(obj), - _ => ParseUnknownObject(obj) - }; - } - - private unsafe WorldService.BgObjectSnapshot ParseBgObject(Pointer obj) - { - var bgObj = (BgObject*)obj.Value; - var path = WorldService.GetBgObjectPath(bgObj); - return new WorldService.BgObjectSnapshot(path, obj.Value->Position, obj.Value->Rotation, obj.Value->Scale, null); - } - - private unsafe WorldService.HousingObjectSnapshot ParseHousingBgObject(Pointer obj, HousingService.HousingItem item) - { - var bgObj = (BgObject*)obj.Value; - var path = WorldService.GetBgObjectPath(bgObj); - var color = ImGui.ColorConvertU32ToFloat4(item.Stain?.Color ?? 0); - return new WorldService.HousingObjectSnapshot(path, obj.Value->Position, obj.Value->Rotation, obj.Value->Scale, null, color); - } - - private unsafe WorldService.TerrainObjectSnapshot ParseTerrain(Pointer obj) - { - var terrain = (Terrain*)obj.Value; - var path = WorldService.GetTerrainPath(terrain); - return new WorldService.TerrainObjectSnapshot(path, obj.Value->Position, obj.Value->Rotation, obj.Value->Scale); - } - - private unsafe WorldService.ObjectSnapshot ParseUnknownObject(Pointer obj) - { - return new WorldService.ObjectSnapshot(obj.Value->GetObjectType(), obj.Value->Position, obj.Value->Rotation, obj.Value->Scale); - } -} -*/ diff --git a/Meddle/Meddle.Plugin/UI/Archive/WorldService.cs b/Meddle/Meddle.Plugin/UI/Archive/WorldService.cs deleted file mode 100644 index 79d658b..0000000 --- a/Meddle/Meddle.Plugin/UI/Archive/WorldService.cs +++ /dev/null @@ -1,101 +0,0 @@ -/* -using System.Numerics; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Meddle.Plugin.Models; -using Meddle.Plugin.Models.Structs; -using Meddle.Plugin.Utils; -using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object; - -namespace Meddle.Plugin.Services; - -public class WorldService : IService, IDisposable -{ - - public record ObjectSnapshot(ObjectType Type, Vector3 Position, Quaternion Rotation, Vector3 Scale) - { - public Matrix4x4 Transform => Matrix4x4.CreateScale(Scale) * Matrix4x4.CreateFromQuaternion(Rotation) * Matrix4x4.CreateTranslation(Position); - } - public record BgObjectSnapshot(string Path, Vector3 Position, Quaternion Rotation, Vector3 Scale, MdlFileGroup? MdlFileGroup) : - ObjectSnapshot(ObjectType.BgObject, Position, Rotation, Scale); - - public record HousingObjectSnapshot(string Path, Vector3 Position, Quaternion Rotation, Vector3 Scale, MdlFileGroup? MdlFileGroup, Vector4 StainColor) : - BgObjectSnapshot(Path, Position, Rotation, Scale, MdlFileGroup); - - public record TerrainObjectSnapshot(string Path, Vector3 Position, Quaternion Rotation, Vector3 Scale) : - ObjectSnapshot(ObjectType.Terrain, Position, Rotation, Scale); - - private readonly Configuration config; - public float CutoffDistance; - public Vector4 DotColor; - public readonly Dictionary SelectedObjects = []; - - // TODO: This isn't great, should find a better way to link drawing the World Overlay to the World Tab - public bool ShouldDrawOverlay; - public bool ShouldAddAllInRange; - - public enum OverlayType - { - Disabled, // hide overlay - Housing, // draw only housing items - World // draw all items in the world - } - - public OverlayType Overlay = OverlayType.World; - - public bool ResolveUsingGameGui = true; - - public void SaveOptions() - { - config.WorldCutoffDistance = CutoffDistance; - config.WorldDotColor = DotColor; - config.Save(); - } - - public WorldService(Configuration config) - { - this.config = config; - CutoffDistance = config.WorldCutoffDistance; - DotColor = config.WorldDotColor; - } - - public static bool IsSupportedObject(ObjectType type) - { - return type switch - { - ObjectType.BgObject => true, - // ObjectType.Terrain => true, - _ => false - }; - } - - public static unsafe string GetPath(Object* obj) - { - var type = obj->GetObjectType(); - var path = type switch - { - ObjectType.BgObject => GetBgObjectPath((BgObject*)obj), - ObjectType.Terrain => GetTerrainPath((Terrain*)obj), - _ => "Unknown" - }; - - return path; - } - - public static unsafe string GetBgObjectPath(BgObject* bgObject) - { - if (bgObject->ModelResourceHandle == null) return "Unknown"; - return bgObject->ModelResourceHandle->ResourceHandle.FileName.ParseString(); - } - - public static unsafe string GetTerrainPath(Terrain* terrain) - { - if (terrain->ResourceHandle == null) return "Unknown"; - return terrain->ResourceHandle->FileName.ParseString(); - } - - public void Dispose() - { - SelectedObjects.Clear(); - } -} -*/ diff --git a/Meddle/Meddle.Plugin/UI/Archive/WorldTab.cs b/Meddle/Meddle.Plugin/UI/Archive/WorldTab.cs deleted file mode 100644 index e5443af..0000000 --- a/Meddle/Meddle.Plugin/UI/Archive/WorldTab.cs +++ /dev/null @@ -1,430 +0,0 @@ -/* -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.ImGuiFileDialog; -using Dalamud.Interface.Textures; -using Dalamud.Interface.Utility.Raii; -using Dalamud.Plugin.Services; -using ImGuiNET; -using Meddle.Plugin.Models; -using Meddle.Plugin.Models.Skeletons; -using Meddle.Plugin.Services; -using Meddle.Plugin.Utils; -using Meddle.Utils; -using Meddle.Utils.Files.SqPack; -using Meddle.Utils.Models; -using Microsoft.Extensions.Logging; - -namespace Meddle.Plugin.UI; - -public class WorldTab : ITab -{ - private readonly IClientState clientState; - private readonly Configuration config; - private readonly TextureCache textureCache; - private readonly ITextureProvider textureProvider; - private readonly WorldService worldService; - private readonly ILogger log; - private readonly ExportService exportService; - private readonly ParseService parseService; - private ExportService.ModelExportProgress? exportProgress; - private Task? exportTask; - private readonly SqPack pack; - private readonly FileDialogManager fileDialog = new() - { - AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking - }; - - public WorldTab( - IClientState clientState, - ILogger log, - ExportService exportService, - ParseService parseService, - SqPack pack, - Configuration config, - TextureCache textureCache, - ITextureProvider textureProvider, - WorldService worldService) - { - this.clientState = clientState; - this.log = log; - this.exportService = exportService; - this.parseService = parseService; - this.pack = pack; - this.config = config; - this.textureCache = textureCache; - this.textureProvider = textureProvider; - this.worldService = worldService; - } - - public bool DisplayTab => config.ShowTesting; - - public void Dispose() - { - } - - public string Name => "World"; - public int Order => 4; - - public void Draw() - { - worldService.ShouldDrawOverlay = true; - - fileDialog.Draw(); - ImGui.Text("This is a testing menu, functionality may not work as expected."); - ImGui.Text("Pixel shader approximation for most non-character shaders is not properly supported at this time."); - ImGui.Text("Items added from the overlay will keep a snapshot of their transform at the time of being added."); - - if (ImGui.CollapsingHeader("Options")) - { - if (ImGui.DragFloat("Cutoff Distance", ref worldService.CutoffDistance, 1, 0, 10000)) - { - worldService.SaveOptions(); - } - - if (ImGui.ColorEdit4("Dot Color", ref worldService.DotColor, ImGuiColorEditFlags.NoInputs)) - { - worldService.SaveOptions(); - } - - if (ImGui.BeginCombo("Overlay Type##OverlayType", worldService.Overlay.ToString())) - { - foreach (var overlayType in Enum.GetValues()) - { - var isSelected = worldService.Overlay == overlayType; - if (ImGui.Selectable(overlayType.ToString(), isSelected)) - { - worldService.Overlay = overlayType; - } - } - ImGui.EndCombo(); - } - - ImGui.Checkbox("Resolve using GameGui", ref worldService.ResolveUsingGameGui); - if (ImGui.IsItemHovered()) - { - ImGui.SetTooltip("If disabled, will resolve using your current camera, " + - "you should only need this if trying to resolve environments while not logged in."); - } - ImGui.Separator(); - } - - - if (ImGui.Button("Clear")) - { - worldService.SelectedObjects.Clear(); - } - - if (ImGui.Button("Add all in range")) - { - worldService.ShouldAddAllInRange = true; - } - - if (exportProgress is {DistinctPaths: > 0} && exportTask?.IsCompleted == false) - { - var width = ImGui.GetContentRegionAvail().X; - ImGui.Text($"Models parsed: {exportProgress.ModelsParsed} / {exportProgress.DistinctPaths}"); - ImGui.ProgressBar(exportProgress.ModelsParsed / (float)exportProgress.DistinctPaths, new Vector2(width, 0)); - } - - if (ImGui.Button("Export All")) - { - exportProgress = new ExportService.ModelExportProgress(); - var folderName = "ExportedModels"; - fileDialog.SaveFolderDialog("Save Model", folderName, - (result, path) => - { - if (!result) return; - exportTask = Task.Run(async () => - { - var bgObjects = worldService.SelectedObjects.Select(x => x.Value) - .OfType() - .Select(x => (new Transform(x.Transform), x.Path)).ToArray(); - await exportService.Export(bgObjects, exportProgress, path); - }); - }, Plugin.TempDirectory); - } - - using var modelTable = ImRaii.Table("##worldObjectTable", 2, ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable); - ImGui.TableSetupColumn("Options", ImGuiTableColumnFlags.WidthFixed, 80); - ImGui.TableSetupColumn("Data", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableHeadersRow(); - - foreach (var (ptr, obj) in worldService.SelectedObjects.ToArray()) - { - using var objId = ImRaii.PushId(ptr); - switch (obj) - { - case WorldService.BgObjectSnapshot bgObj: - DrawBgObject(ptr, bgObj); - break; - default: - DrawUnknownObject(ptr, obj); - break; - } - } - } - - private void DrawUnknownObject(nint ptr, WorldService.ObjectSnapshot obj) - { - ImGui.TableNextRow(); - ImGui.TableSetColumnIndex(0); - using (ImRaii.PushFont(UiBuilder.IconFont)) - { - // disable button but still show tooltip - using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f)) - { - ImGui.Button(FontAwesomeIcon.FileExport.ToIconString()); - } - if (ImGui.IsItemHovered()) - { - using (ImRaii.PushFont(UiBuilder.DefaultFont)) - { - ImGui.BeginTooltip(); - ImGui.Text("Exporting this object type from the world tab is currently not supported."); - ImGui.EndTooltip(); - } - } - - ImGui.SameLine(); - if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString())) - { - worldService.SelectedObjects.Remove(ptr); - } - } - ImGui.TableSetColumnIndex(1); - if (ImGui.CollapsingHeader($"{obj.Type} ({ptr:X8})##{ptr}")) - { - UiUtil.Text($"Address: {ptr:X8}", $"{ptr:X8}"); - ImGui.Text($"Position: {obj.Position}"); - ImGui.Text($"Rotation: {obj.Rotation}"); - ImGui.Text($"Scale: {obj.Scale}"); - } - } - - private void DrawBgObject(nint ptr, WorldService.BgObjectSnapshot obj) - { - ImGui.TableNextRow(); - ImGui.TableSetColumnIndex(0); - using (ImRaii.PushFont(UiBuilder.IconFont)) - { - if (ImGui.Button(FontAwesomeIcon.FileExport.ToIconString())) - { - ImGui.OpenPopup("ExportBgObjectPopup"); - } - - // remove icon - ImGui.SameLine(); - if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString())) - { - worldService.SelectedObjects.Remove(ptr); - } - } - - if (ImGui.BeginPopupContextItem("ExportBgObjectPopup")) - { - if (ImGui.MenuItem("Export as mdl")) - { - var fileName = Path.GetFileName(obj.Path); - fileDialog.SaveFileDialog("Save Model", "Model File{.mdl}", fileName, ".mdl", - (result, path) => - { - if (!result) return; - var data = pack.GetFileOrReadFromDisk(path); - if (data == null) - { - log.LogError("Failed to get model data from pack or disk for {FileName}", path); - return; - } - - File.WriteAllBytes(path, data); - }, Plugin.TempDirectory); - } - - if (ImGui.MenuItem("Export as glTF")) - { - exportProgress = new ExportService.ModelExportProgress(); - var folderName = Path.GetFileNameWithoutExtension(obj.Path); - fileDialog.SaveFolderDialog("Save Model", folderName, - (result, path) => - { - if (!result) return; - exportTask = Task.Run(async () => - { - await exportService.Export([(new Transform(obj.Transform), obj.Path)], exportProgress, path); - }); - }, Plugin.TempDirectory); - } - - ImGui.EndPopup(); - } - - ImGui.TableSetColumnIndex(1); - if (ImGui.CollapsingHeader($"{obj.Path} ({ptr:X8})##{ptr}")) - { - UiUtil.Text($"Address: {ptr:X8}", $"{ptr:X8}"); - ImGui.Text($"Position: {obj.Position}"); - ImGui.Text($"Rotation: {obj.Rotation}"); - ImGui.Text($"Scale: {obj.Scale}"); - if (obj.MdlFileGroup == null) - { - if (ImGui.Button("Parse Model")) - { - Task.Run(async () => - { - var mdlFileGroup = await parseService.ParseFromPath(obj.Path); - if (worldService.SelectedObjects.TryGetValue(ptr, out var snapshot) && snapshot is WorldService.BgObjectSnapshot bgObj) - { - worldService.SelectedObjects[ptr] = bgObj with {MdlFileGroup = mdlFileGroup}; - } - }); - } - } - else - { - var mdl = obj.MdlFileGroup; - ImGui.Text($"Lods: {mdl.MdlFile.Lods.Length}"); - ImGui.Text($"Attribute Count: {mdl.MdlFile.ModelHeader.AttributeCount}"); - ImGui.Text($"Bone Count: {mdl.MdlFile.ModelHeader.BoneCount}"); - ImGui.Text($"Bone Table Count: {mdl.MdlFile.ModelHeader.BoneTableCount}"); - ImGui.Text($"Mesh Count: {mdl.MdlFile.ModelHeader.MeshCount}"); - ImGui.Text($"Submesh Count: {mdl.MdlFile.ModelHeader.SubmeshCount}"); - ImGui.Text($"Material Count: {mdl.MdlFile.ModelHeader.MaterialCount}"); - ImGui.Text($"Radius: {mdl.MdlFile.ModelHeader.Radius}"); - ImGui.Text($"Shapemesh Count: {mdl.MdlFile.ModelHeader.ShapeMeshCount}"); - ImGui.Text($"Shape Count: {mdl.MdlFile.ModelHeader.ShapeCount}"); - ImGui.Text($"Vertex declarations: {mdl.MdlFile.VertexDeclarations.Length}"); - - for (int i = 0; i < mdl.MtrlFiles.Length; i++) - { - using var mtrlId = ImRaii.PushId($"Mtrl{i}"); - var mtrl = mdl.MtrlFiles[i]; - if (mtrl is MtrlFileGroup mtrlGroup) - { - if (ImGui.CollapsingHeader($"Material {i}: {mtrlGroup.Path}")) - { - using var mtrlIndent = ImRaii.PushIndent(); - ImGui.Text($"Mdl Path: {mtrlGroup.MdlPath}"); - ImGui.Text($"Shader Path: {mtrlGroup.ShpkPath}"); - if (ImGui.CollapsingHeader("Color Table")) - { - UiUtil.DrawColorTable(mtrlGroup.MtrlFile.ColorTable, mtrlGroup.MtrlFile.ColorDyeTable); - } - - for (int j = 0; j < mtrlGroup.TexFiles.Length; j++) - { - using var texId = ImRaii.PushId($"Tex{j}"); - var tex = mtrlGroup.TexFiles[j]; - if (ImGui.CollapsingHeader($"Texture {j}: {tex.Path}")) - { - using var texIndent = ImRaii.PushIndent(); - ImGui.Text($"Mtrl Path: {tex.MtrlPath}"); - ImGui.Text($"Width: {tex.Resource.Width}"); - ImGui.Text($"Height: {tex.Resource.Height}"); - ImGui.Text($"Format: {tex.Resource.Format}"); - ImGui.Text($"Mipmap Count: {tex.Resource.MipLevels}"); - ImGui.Text($"Array Count: {tex.Resource.ArraySize}"); - - var availableWidth = ImGui.GetContentRegionAvail().X; - float displayWidth = tex.Resource.Width; - float displayHeight = tex.Resource.Height; - if (displayWidth > availableWidth) - { - var ratio = availableWidth / displayWidth; - displayWidth *= ratio; - displayHeight *= ratio; - } - - var wrap = textureCache.GetOrAdd($"{mtrlGroup.Path}_{tex.Path}", () => - { - var textureData = tex.Resource.ToBitmap().GetPixelSpan(); - var wrap = textureProvider.CreateFromRaw( - RawImageSpecification.Rgba32(tex.Resource.Width, tex.Resource.Height), textureData, - $"Meddle_World_{mtrlGroup.Path}_{tex.Path}"); - return wrap; - }); - - ImGui.Image(wrap.ImGuiHandle, new Vector2(displayWidth, displayHeight)); - } - } - } - } - } - } - } - } -} -*/ - - -/* - public async Task Export((Transform transform, string mdlPath)[] models, ModelExportProgress progress, string? outputFolder = null, CancellationToken token = default) - { - try - { - using var activity = ActivitySource.StartActivity(); - var scene = new SceneBuilder(); - - var distinctPaths = models.Select(x => x.mdlPath).Distinct().ToArray(); - progress.DistinctPaths = distinctPaths.Length; - var caches = new ConcurrentDictionary(); - - var bones = new List(); - await Parallel.ForEachAsync(distinctPaths, new ParallelOptions {CancellationToken = token}, async (path, tkn) => - { - if (token.IsCancellationRequested) return; - if (tkn.IsCancellationRequested) return; - try - { - var mdlGroup = await parseService.ParseFromPath(path); - var cache = HandleModel(new CustomizeData(), - new CustomizeParameter(), - GenderRace.Unknown, - mdlGroup, - ref bones, - null, - true, - token).ToArray(); - caches[path] = cache; - - progress.ModelsParsed++; - } - catch (Exception e) - { - logger.LogError(e, "Error handling model {Path}", path); - } - }); - - foreach (var (transform, path) in models) - { - if (token.IsCancellationRequested) return; - if (!caches.TryGetValue(path, out var cache)) - { - logger.LogWarning("Cache not found for {Path}", path); - continue; - } - foreach (var (model, mesh) in cache) - { - AddMesh(scene, transform.AffineTransform.Matrix, model, mesh, []); - } - } - - var sceneGraph = scene.ToGltf2(); - if (outputFolder != null) - { - Directory.CreateDirectory(outputFolder); - } - - var folder = outputFolder ?? GetPathForOutput(); - var outputPath = Path.Combine(folder, "scene.gltf"); - sceneGraph.SaveGLTF(outputPath); - Process.Start("explorer.exe", folder); - logger.LogInformation("Export complete"); - } - catch (Exception e) - { - logger.LogError(e, "Failed to export model set"); - throw; - } - } - */ diff --git a/Meddle/Meddle.Plugin/UI/DebugTab.cs b/Meddle/Meddle.Plugin/UI/DebugTab.cs index b2126b9..779d371 100644 --- a/Meddle/Meddle.Plugin/UI/DebugTab.cs +++ b/Meddle/Meddle.Plugin/UI/DebugTab.cs @@ -5,6 +5,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.Havok.Animation.Rig; using ImGuiNET; using Meddle.Plugin.Models; using Meddle.Plugin.Services; @@ -24,9 +25,18 @@ public class DebugTab : ITab private readonly LayoutService layoutService; private readonly ParseService parseService; private readonly PbdHooks pbdHooks; + private string boneSearch = ""; private ICharacter? selectedCharacter; - private bool showLocalTransforms = false; + private enum BoneMode + { + Local, + ModelPropagate, + ModelNoPropagate, + ModelRaw + } + + private BoneMode boneMode = BoneMode.ModelPropagate; public DebugTab(Configuration config, SigUtil sigUtil, CommonUi commonUi, IGameGui gui, IClientState clientState, @@ -344,7 +354,28 @@ private unsafe void DrawCharacterBase(CharacterBase* cBase, string name) ImGui.Text($"Partial Skeleton Count: {cBase->Skeleton->PartialSkeletonCount}"); if (ImGui.CollapsingHeader("Draw Bones")) { - ImGui.Checkbox("Show Local Transforms", ref showLocalTransforms); + // boneMode + ImGui.Text("Bone Mode"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(200); + using (var combo = ImRaii.Combo("##BoneMode", boneMode.ToString())) + { + if (combo.Success) + { + foreach (BoneMode mode in Enum.GetValues(typeof(BoneMode))) + { + if (ImGui.Selectable(mode.ToString(), mode == boneMode)) + { + boneMode = mode; + } + } + } + } + + ImGui.Text("Bone Search"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(200); + ImGui.InputText("##BoneSearch", ref boneSearch, 100); // imgui select partial skeleton by index for (int i = 0; i < skeleton->PartialSkeletonCount; i++) @@ -364,14 +395,14 @@ private unsafe void DrawCharacterBase(CharacterBase* cBase, string name) ImGui.TableSetupColumn("Rotation", ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetupColumn("Scale", ImGuiTableColumnFlags.WidthStretch); ImGui.TableHeadersRow(); - DrawBoneTransformsOnScreen(partialSkeleton, showLocalTransforms); + DrawBoneTransformsOnScreen(partialSkeleton, boneMode); } } } } } - private unsafe void DrawBoneTransformsOnScreen(PartialSkeleton partialSkeleton, bool displayLocal = false) + private unsafe void DrawBoneTransformsOnScreen(PartialSkeleton partialSkeleton, BoneMode boneMode) { var rootPos = partialSkeleton.Skeleton->Transform; var rootTransform = new Transform(rootPos); @@ -380,7 +411,11 @@ private unsafe void DrawBoneTransformsOnScreen(PartialSkeleton partialSkeleton, for (var i = 0; i < boneCount; i++) { var bone = pose->Skeleton->Bones[i]; - var localTransform = new Transform(pose->LocalPose[i]); + if (!string.IsNullOrEmpty(boneSearch) && bone.Name.String != null && !bone.Name.String.Contains(boneSearch)) + { + continue; + } + var modelTransform = new Transform(pose->ModelPose[i]); var worldMatrix = modelTransform.AffineTransform.Matrix * rootTransform.AffineTransform.Matrix; ImGui.TableNextRow(); @@ -392,7 +427,14 @@ private unsafe void DrawBoneTransformsOnScreen(PartialSkeleton partialSkeleton, dotColorRgb = new Vector4(1, 0, 0, 0.5f); } - var t = displayLocal ? localTransform : modelTransform; + var t = boneMode switch + { + BoneMode.Local => new Transform(pose->LocalPose[i]), + BoneMode.ModelPropagate => new Transform(*pose->AccessBoneModelSpace(i, hkaPose.PropagateOrNot.Propagate)), + BoneMode.ModelNoPropagate => new Transform(*pose->AccessBoneModelSpace(i, hkaPose.PropagateOrNot.DontPropagate)), + BoneMode.ModelRaw => new Transform(pose->ModelPose[i]), + _ => new Transform(pose->ModelPose[i]) + }; ImGui.TableSetColumnIndex(1); var parentIndex = pose->Skeleton->ParentIndices[i]; diff --git a/Meddle/Meddle.Plugin/UI/Layout/Config.cs b/Meddle/Meddle.Plugin/UI/Layout/Config.cs index e0249c8..e695831 100644 --- a/Meddle/Meddle.Plugin/UI/Layout/Config.cs +++ b/Meddle/Meddle.Plugin/UI/Layout/Config.cs @@ -6,28 +6,16 @@ namespace Meddle.Plugin.UI.Layout; public partial class LayoutWindow { - [Flags] - public enum ExportType - { - // ReSharper disable InconsistentNaming - GLTF = 1, - GLB = 2, - OBJ = 4 - // ReSharper restore InconsistentNaming - } - - private const ParsedInstanceType DefaultDrawTypes = ParsedInstanceType.Character | - ParsedInstanceType.Housing | - ParsedInstanceType.Terrain | - ParsedInstanceType.BgPart | + private const ParsedInstanceType DefaultDrawTypes = ParsedInstanceType.Character | + ParsedInstanceType.Housing | + ParsedInstanceType.Terrain | + ParsedInstanceType.BgPart | ParsedInstanceType.Light | ParsedInstanceType.SharedGroup; - private const ExportType DefaultExportType = ExportType.GLTF; public class LayoutConfig { public ParsedInstanceType DrawTypes { get; set; } = DefaultDrawTypes; - public ExportType ExportType { get; set; } = DefaultExportType; public bool DrawOverlay { get; set; } = true; public bool DrawChildren { get; set; } public bool TraceToParent { get; set; } = true; @@ -132,45 +120,11 @@ private void DrawOptions() drawTypes = DefaultDrawTypes; } - var exportType = config.LayoutConfig.ExportType; - if (ImGui.BeginCombo("Export Type", exportType.ToString())) - { - foreach (var type in Enum.GetValues()) - { - var selected = exportType.HasFlag(type); - using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, selected ? 1 : 0.5f); - if (ImGui.Selectable(type.ToString(), selected, ImGuiSelectableFlags.DontClosePopups)) - { - if (selected) - { - exportType &= ~type; - } - else - { - exportType |= type; - } - } - } - - ImGui.EndCombo(); - } - - if (exportType == 0) - { - exportType = DefaultExportType; - } - if (drawTypes != config.LayoutConfig.DrawTypes) { config.LayoutConfig.DrawTypes = drawTypes; config.Save(); } - - if (exportType != config.LayoutConfig.ExportType) - { - config.LayoutConfig.ExportType = exportType; - config.Save(); - } } // private static T[] GetFlags(T flags) where T : Enum diff --git a/Meddle/Meddle.Plugin/UI/Layout/Instance.cs b/Meddle/Meddle.Plugin/UI/Layout/Instance.cs index 9ad5519..c27fc3f 100644 --- a/Meddle/Meddle.Plugin/UI/Layout/Instance.cs +++ b/Meddle/Meddle.Plugin/UI/Layout/Instance.cs @@ -151,7 +151,6 @@ private void DrawTerrain(ParsedTerrainInstance terrain) terrain.Data.ResolvedPlates[i] = plate; } - // TODO: Draw plate if (plate == null) { ImGui.Text("No plate data"); diff --git a/Meddle/Meddle.Plugin/UI/Layout/LayoutWindow.cs b/Meddle/Meddle.Plugin/UI/Layout/LayoutWindow.cs index 3276503..f1b83ba 100644 --- a/Meddle/Meddle.Plugin/UI/Layout/LayoutWindow.cs +++ b/Meddle/Meddle.Plugin/UI/Layout/LayoutWindow.cs @@ -314,7 +314,7 @@ private void InstanceExport(ParsedInstance[] instances) resolverService.ResolveInstances(instances); var defaultName = $"InstanceExport-{DateTime.Now:yyyy-MM-dd-HH-mm-ss}"; - var currentExportType = config.LayoutConfig.ExportType; + var currentExportType = config.ExportType; cancelToken = new CancellationTokenSource(); fileDialog.SaveFolderDialog("Save Instances", defaultName, (result, path) => diff --git a/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs b/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs index 56b5dbb..b4d013b 100644 --- a/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs +++ b/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs @@ -46,9 +46,7 @@ public unsafe class LiveCharacterTab : ITab { AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking }; - - private const LayoutWindow.ExportType DefaultExportType = LayoutWindow.ExportType.GLTF; private readonly ILogger log; private readonly SqPack pack; private readonly ParseService parseService; @@ -135,51 +133,11 @@ public void Draw() progressEvents.Clear(); } - DrawOptions(); commonUi.DrawCharacterSelect(ref selectedCharacter); DrawSelectedCharacter(); fileDialog.Draw(); } - - private void DrawOptions() - { - if (!ImGui.CollapsingHeader("Options")) return; - - var exportType = config.LayoutConfig.ExportType; - if (ImGui.BeginCombo("Export Type", exportType.ToString())) - { - foreach (var type in Enum.GetValues()) - { - var selected = exportType.HasFlag(type); - using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, selected ? 1 : 0.5f); - if (ImGui.Selectable(type.ToString(), selected, ImGuiSelectableFlags.DontClosePopups)) - { - if (selected) - { - exportType &= ~type; - } - else - { - exportType |= type; - } - } - } - - ImGui.EndCombo(); - } - - if (exportType == 0) - { - exportType = DefaultExportType; - } - - if (exportType != config.LayoutConfig.ExportType) - { - config.LayoutConfig.ExportType = exportType; - config.Save(); - } - } public void Dispose() { @@ -348,7 +306,7 @@ private void DrawDrawObject(DrawObject* drawObject, CustomizeData? customizeData exportCancelTokenSource = new CancellationTokenSource(); exportTask = Task.Run(() => { - var exportType = config.LayoutConfig.ExportType; + var exportType = config.ExportType; var composer = composerFactory.CreateCharacterComposer(Path.Combine(path, "cache"), HandleProgressEvent, exportCancelTokenSource.Token); var scene = new SceneBuilder(); var root = new NodeBuilder(); @@ -356,20 +314,7 @@ private void DrawDrawObject(DrawObject* drawObject, CustomizeData? customizeData customizeData, skeleton, scene, root); scene.AddNode(root); var gltf = scene.ToGltf2(); - if (exportType.HasFlag(LayoutWindow.ExportType.GLTF)) - { - gltf.SaveGLTF(Path.Combine(path, "models.gltf")); - } - - if (exportType.HasFlag(LayoutWindow.ExportType.GLB)) - { - gltf.SaveGLB(Path.Combine(path, "models.glb")); - } - - if (exportType.HasFlag(LayoutWindow.ExportType.OBJ)) - { - gltf.SaveAsWavefront(Path.Combine(path, "models.obj")); - } + gltf.SaveAsType(exportType, path, "models"); Process.Start("explorer.exe", path); }, exportCancelTokenSource.Token); }, Plugin.TempDirectory); @@ -420,28 +365,14 @@ private void ExportAllModelsWithAttaches(CSCharacter* character, CustomizeParame exportCancelTokenSource = new CancellationTokenSource(); exportTask = Task.Run(() => { - var exportType = config.LayoutConfig.ExportType; + var exportType = config.ExportType; var composer = composerFactory.CreateCharacterComposer(Path.Combine(path, "cache"), HandleProgressEvent, exportCancelTokenSource.Token); var scene = new SceneBuilder(); var root = new NodeBuilder(); composer.ComposeCharacterInfo(info, null, scene, root); scene.AddNode(root); var gltf = scene.ToGltf2(); - if (exportType.HasFlag(LayoutWindow.ExportType.GLTF)) - { - gltf.SaveGLTF(Path.Combine(path, "character.gltf")); - } - - if (exportType.HasFlag(LayoutWindow.ExportType.GLB)) - { - gltf.SaveGLB(Path.Combine(path, "character.glb")); - } - - if (exportType.HasFlag(LayoutWindow.ExportType.OBJ)) - { - gltf.SaveAsWavefront(Path.Combine(path, "character.obj")); - } - + gltf.SaveAsType(exportType, path, "character"); Process.Start("explorer.exe", path); }, exportCancelTokenSource.Token); }, Plugin.TempDirectory); @@ -475,24 +406,24 @@ private void ExportAllModels(CSCharacterBase* cBase, CustomizeParameter? customi exportCancelTokenSource = new CancellationTokenSource(); exportTask = Task.Run(() => { - var exportType = config.LayoutConfig.ExportType; + var exportType = config.ExportType; var composer = composerFactory.CreateCharacterComposer(Path.Combine(path, "cache"), HandleProgressEvent, exportCancelTokenSource.Token); var scene = new SceneBuilder(); var root = new NodeBuilder(); composer.ComposeCharacterInfo(info, null, scene, root); scene.AddNode(root); var gltf = scene.ToGltf2(); - if (exportType.HasFlag(LayoutWindow.ExportType.GLTF)) + if (exportType.HasFlag(ExportType.GLTF)) { gltf.SaveGLTF(Path.Combine(path, "character.gltf")); } - if (exportType.HasFlag(LayoutWindow.ExportType.GLB)) + if (exportType.HasFlag(ExportType.GLB)) { gltf.SaveGLB(Path.Combine(path, "character.glb")); } - if (exportType.HasFlag(LayoutWindow.ExportType.OBJ)) + if (exportType.HasFlag(ExportType.OBJ)) { gltf.SaveAsWavefront(Path.Combine(path, "character.obj")); } @@ -605,7 +536,7 @@ private void DrawModel(Pointer cPtr, Pointer mPtr, Custo exportCancelTokenSource = new CancellationTokenSource(); exportTask = Task.Run(() => { - var exportType = config.LayoutConfig.ExportType; + var exportType = config.ExportType; var composer = composerFactory.CreateCharacterComposer( Path.Combine(path, "cache"), HandleProgressEvent, @@ -619,17 +550,17 @@ private void DrawModel(Pointer cPtr, Pointer mPtr, Custo root); scene.AddNode(root); var gltf = scene.ToGltf2(); - if (exportType.HasFlag(LayoutWindow.ExportType.GLTF)) + if (exportType.HasFlag(ExportType.GLTF)) { gltf.SaveGLTF(Path.Combine(path, "model.gltf")); } - if (exportType.HasFlag(LayoutWindow.ExportType.GLB)) + if (exportType.HasFlag(ExportType.GLB)) { gltf.SaveGLB(Path.Combine(path, "model.glb")); } - if (exportType.HasFlag(LayoutWindow.ExportType.OBJ)) + if (exportType.HasFlag(ExportType.OBJ)) { gltf.SaveAsWavefront(Path.Combine(path, "model.obj")); } diff --git a/Meddle/Meddle.Plugin/UI/OptionsTab.cs b/Meddle/Meddle.Plugin/UI/OptionsTab.cs index 24ea019..9f33ece 100644 --- a/Meddle/Meddle.Plugin/UI/OptionsTab.cs +++ b/Meddle/Meddle.Plugin/UI/OptionsTab.cs @@ -1,6 +1,9 @@ using System.Diagnostics; +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; using ImGuiNET; using Meddle.Plugin.Models; +using Meddle.Plugin.Utils; using Microsoft.Extensions.Logging; namespace Meddle.Plugin.UI; @@ -47,7 +50,44 @@ public void Draw() // config.OpenLayoutMenuOnLoad = test; // config.Save(); // } + + ImGui.Separator(); + + DrawExportType(); + var includePose = config.IncludePose; + if (ImGui.Checkbox("Include Pose", ref includePose)) + { + config.IncludePose = includePose; + config.Save(); + } + + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGui.Text(FontAwesomeIcon.QuestionCircle.ToIconString()); + } + + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.Text("Includes pose as a track on exported models with frame 0 as the currently applied pose."); + ImGui.EndTooltip(); + } + + DrawPoseMode(); + + ImGui.Separator(); + + var playerNameOverride = config.PlayerNameOverride; + if (ImGui.InputText("Player Name Override", ref playerNameOverride, 64)) + { + config.PlayerNameOverride = playerNameOverride; + config.Save(); + } + + ImGui.Separator(); + var minimumNotificationLogLevel = config.MinimumNotificationLogLevel; if (ImGui.BeginCombo("Minimum Notification Log Level", minimumNotificationLogLevel.ToString())) { @@ -90,11 +130,65 @@ public void Draw() config.DisableGposeUiHide = disableGposeUiHide; config.Save(); } + } - var playerNameOverride = config.PlayerNameOverride; - if (ImGui.InputText("Player Name Override", ref playerNameOverride, 64)) + private void DrawPoseMode() + { + var poseMode = config.PoseMode; + if (ImGui.BeginCombo("Pose Mode", poseMode.ToString())) { - config.PlayerNameOverride = playerNameOverride; + foreach (var mode in Enum.GetValues()) + { + if (ImGui.Selectable(mode.ToString(), mode == poseMode)) + { + config.PoseMode = mode; + config.Save(); + } + } + + ImGui.EndCombo(); + } + + if (!Enum.IsDefined(typeof(SkeletonUtils.PoseMode), config.PoseMode)) + { + config.PoseMode = Configuration.DefaultPoseMode; + config.Save(); + } + } + + private void DrawExportType() + { + var exportType = config.ExportType; + if (ImGui.BeginCombo("Export Type", exportType.ToString())) + { + foreach (var type in Enum.GetValues()) + { + var selected = exportType.HasFlag(type); + using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, selected ? 1 : 0.5f); + if (ImGui.Selectable(type.ToString(), selected, ImGuiSelectableFlags.DontClosePopups)) + { + if (selected) + { + exportType &= ~type; + } + else + { + exportType |= type; + } + } + } + + ImGui.EndCombo(); + } + + if (exportType == 0) + { + exportType = Configuration.DefaultExportType; + } + + if (exportType != config.ExportType) + { + config.ExportType = exportType; config.Save(); } } diff --git a/Meddle/Meddle.Plugin/Utils/ExportUtil.cs b/Meddle/Meddle.Plugin/Utils/ExportUtil.cs new file mode 100644 index 0000000..43fb9a1 --- /dev/null +++ b/Meddle/Meddle.Plugin/Utils/ExportUtil.cs @@ -0,0 +1,25 @@ +using Meddle.Plugin.Models; +using SharpGLTF.Schema2; + +namespace Meddle.Plugin.Utils; + +public static class ExportUtil +{ + public static void SaveAsType(this ModelRoot? gltf, ExportType typeFlags, string path, string name) + { + if (typeFlags.HasFlag(ExportType.GLTF)) + { + gltf?.SaveGLTF(Path.Combine(path, name + ".gltf")); + } + + if (typeFlags.HasFlag(ExportType.GLB)) + { + gltf?.SaveGLB(Path.Combine(path, name + ".glb")); + } + + if (typeFlags.HasFlag(ExportType.OBJ)) + { + gltf?.SaveAsWavefront(Path.Combine(path, name + ".obj")); + } + } +} diff --git a/Meddle/Meddle.Plugin/Utils/SkeletonUtils.cs b/Meddle/Meddle.Plugin/Utils/SkeletonUtils.cs index aefe5c3..2244fcc 100644 --- a/Meddle/Meddle.Plugin/Utils/SkeletonUtils.cs +++ b/Meddle/Meddle.Plugin/Utils/SkeletonUtils.cs @@ -1,13 +1,21 @@ -using Meddle.Plugin.Models; +using System.Numerics; +using Meddle.Plugin.Models; using Meddle.Plugin.Models.Skeletons; using Meddle.Utils; using SharpGLTF.Scenes; +using SharpGLTF.Transforms; namespace Meddle.Plugin.Utils; public static class SkeletonUtils { - public static (List List, BoneNodeBuilder Root)[] GetBoneMaps(IReadOnlyList partialSkeletons, bool includePose) + public enum PoseMode + { + Local, + Model + } + + public static (List List, BoneNodeBuilder Root)[] GetBoneMaps(IReadOnlyList partialSkeletons, PoseMode? poseMode) { List boneMap = new(); List rootList = new(); @@ -19,8 +27,6 @@ public static (List List, BoneNodeBuilder Root)[] GetBoneMaps(I if (hkSkeleton == null) continue; - var pose = partial.Poses.FirstOrDefault(); - var skeleBones = new BoneNodeBuilder[hkSkeleton.BoneNames.Count]; for (var i = 0; i < hkSkeleton.BoneNames.Count; i++) { @@ -47,16 +53,9 @@ public static (List List, BoneNodeBuilder Root)[] GetBoneMaps(I PartialSkeletonHandle = partial.HandlePath ?? throw new InvalidOperationException( $"No handle path for {name} [{partialIdx},{i}]"), - PartialSkeletonIndex = partialIdx + PartialSkeletonIndex = partialIdx, }; - if (pose != null && includePose) - { - var transform = pose.Pose[i]; - bone.UseScale().UseTrackBuilder("pose").WithPoint(0, transform.Scale); - bone.UseRotation().UseTrackBuilder("pose").WithPoint(0, transform.Rotation); - bone.UseTranslation().UseTrackBuilder("pose").WithPoint(0, transform.Translation); - } - + bone.SetLocalTransform(hkSkeleton.ReferencePose[i].AffineTransform, false); var parentIdx = hkSkeleton.BoneParents[i]; @@ -73,7 +72,7 @@ public static (List List, BoneNodeBuilder Root)[] GetBoneMaps(I } // create separate lists based on each root - var boneMapList = new List<(List List, BoneNodeBuilder root)>(); + var boneMapList = new List<(List List, BoneNodeBuilder Root)>(); foreach (var root in rootList) { var bones = NodeBuilder.Flatten(root).Cast().ToList(); @@ -85,12 +84,71 @@ public static (List List, BoneNodeBuilder Root)[] GetBoneMaps(I boneMapList.Add((bones, root)); } - return boneMapList.ToArray(); + var boneMaps = boneMapList.ToArray(); + + // set pose scaling + if (poseMode != null) + { + foreach (var map in boneMaps) + { + foreach (var bone in map.List) + { + ApplyPose(bone, partialSkeletons, poseMode.Value, 0); + } + } + } + + return boneMaps; } - public static List GetBoneMap(ParsedSkeleton skeleton, bool includePose, out BoneNodeBuilder? root) + private static void ApplyPose( + BoneNodeBuilder bone, IReadOnlyList partialSkeletons, PoseMode poseMode, float time) + { + var partial = partialSkeletons[bone.PartialSkeletonIndex]; + if (partial.Poses.FirstOrDefault() is not { } pose) + return; + + switch (poseMode) + { + case PoseMode.Model when bone.Parent is BoneNodeBuilder parent: + { + var boneTransform = pose.ModelPose[bone.BoneIndex].AffineTransform; + var parentPose = partialSkeletons[parent.PartialSkeletonIndex].Poses.FirstOrDefault(); + if (parentPose == null) + return; + + var parentMatrix = parentPose.ModelPose[parent.BoneIndex].AffineTransform.Matrix; + var matrix = boneTransform.Matrix * AffineInverse(parentMatrix); + var affine = new AffineTransform(matrix).GetDecomposed(); + bone.UseScale().UseTrackBuilder("pose").WithPoint(time, affine.Scale); + bone.UseRotation().UseTrackBuilder("pose").WithPoint(time, affine.Rotation); + bone.UseTranslation().UseTrackBuilder("pose").WithPoint(time, affine.Translation); + break; + } + case PoseMode.Model: + { + var boneTransform = pose.ModelPose[bone.BoneIndex].AffineTransform; + bone.UseScale().UseTrackBuilder("pose").WithPoint(time, boneTransform.Scale); + bone.UseRotation().UseTrackBuilder("pose").WithPoint(time, boneTransform.Rotation); + bone.UseTranslation().UseTrackBuilder("pose").WithPoint(time, boneTransform.Translation); + break; + } + case PoseMode.Local: + { + var boneTransform = pose.Pose[bone.BoneIndex].AffineTransform; + bone.UseScale().UseTrackBuilder("pose").WithPoint(time, boneTransform.Scale); + bone.UseRotation().UseTrackBuilder("pose").WithPoint(time, boneTransform.Rotation); + bone.UseTranslation().UseTrackBuilder("pose").WithPoint(time, boneTransform.Translation); + break; + } + default: + throw new InvalidOperationException("Pose mode must be set to Local or Model"); + } + } + + public static List GetBoneMap(ParsedSkeleton skeleton, PoseMode? poseMode, out BoneNodeBuilder? root) { - var maps = GetBoneMaps(skeleton.PartialSkeletons, includePose); + var maps = GetBoneMaps(skeleton.PartialSkeletons, poseMode); if (maps.Length == 0) { throw new InvalidOperationException("No roots were found"); @@ -111,7 +169,7 @@ public static List GetBoneMap(ParsedSkeleton skeleton, bool inc } public record AttachGrouping(List Bones, BoneNodeBuilder? Root, List<(DateTime Time, AttachSet Attach)> Timeline); - public static Dictionary GetAnimatedBoneMap((DateTime Time, AttachSet[] Attaches)[] frames) + public static Dictionary GetAnimatedBoneMap((DateTime Time, AttachSet[] Attaches)[] frames, PoseMode poseMode) { var attachDict = new Dictionary(); var attachTimelines = new Dictionary>(); @@ -147,7 +205,7 @@ public static Dictionary GetAnimatedBoneMap((DateTime Ti var frameTime = TotalSeconds(time, startTime); if (frame == default) continue; - var boneMap = GetBoneMap(frame.Attach.Skeleton, false, out var attachRoot); + var boneMap = GetBoneMap(frame.Attach.Skeleton, null, out var attachRoot); if (attachRoot == null) continue; @@ -156,8 +214,6 @@ public static Dictionary GetAnimatedBoneMap((DateTime Ti attachBoneMap = attachBoneMap with { Root = attachRoot }; } - var trackName = frame.Attach.AttachBoneName ?? "pose"; - foreach (var attachBone in boneMap) { var bone = attachBoneMap.Bones.FirstOrDefault( @@ -168,40 +224,31 @@ public static Dictionary GetAnimatedBoneMap((DateTime Ti bone = attachBone; } - var partial = frame.Attach.Skeleton.PartialSkeletons[attachBone.PartialSkeletonIndex]; - if (partial.Poses.Count == 0) - continue; - - var transform = partial.Poses[0].Pose[bone.BoneIndex]; - bone.UseScale().UseTrackBuilder(trackName).WithPoint(frameTime, transform.Scale); - bone.UseRotation().UseTrackBuilder(trackName).WithPoint(frameTime, transform.Rotation); - bone.UseTranslation().UseTrackBuilder(trackName).WithPoint(frameTime, transform.Translation); + ApplyPose(bone, frame.Attach.Skeleton.PartialSkeletons, poseMode, frameTime); } var firstTranslation = firstAttach.Transform.Translation; attachRoot.UseScale().UseTrackBuilder("root").WithPoint(frameTime, frame.Attach.Transform.Scale); attachRoot.UseRotation().UseTrackBuilder("root").WithPoint(frameTime, frame.Attach.Transform.Rotation); attachRoot.UseTranslation().UseTrackBuilder("root").WithPoint(frameTime, frame.Attach.Transform.Translation - firstTranslation); - attachDict[attachId] = attachBoneMap; } - - /* - foreach (var time in allTimes) - { - var frame = timeline.FirstOrDefault(x => x.Time == time); - if (frame != default) continue; - // set scaling to 0 when not present - foreach (var bone in attachBoneMap.Bones) - { - bone.UseScale().UseTrackBuilder("pose").WithPoint(TotalSeconds(time, startTime), Vector3.Zero); - } - } - */ } return attachDict; } + + public static Matrix4x4 AffineInverse(Matrix4x4 matrix) + { + if (!Matrix4x4.Invert(matrix, out var invMatrix)) + throw new InvalidOperationException("Failed to invert matrix"); + + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (matrix.M44 == 1.0) + invMatrix.M44 = 1f; + + return invMatrix; + } public static float TotalSeconds(DateTime time, DateTime startTime) { diff --git a/Meddle/Meddle.Plugin/Utils/UIUtil.cs b/Meddle/Meddle.Plugin/Utils/UIUtil.cs index ecc656f..02b3615 100644 --- a/Meddle/Meddle.Plugin/Utils/UIUtil.cs +++ b/Meddle/Meddle.Plugin/Utils/UIUtil.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; @@ -42,12 +43,15 @@ public static void DrawCustomizeParams(ref CustomizeParameter customize) //ImGui.ColorEdit4("Skin Fresnel Value", ref customize.SkinFresnelValue0); ImGui.EndDisabled(); ImGui.SameLine(); - ImGui.TextDisabled("?"); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGui.Text(FontAwesomeIcon.QuestionCircle.ToIconString()); + } if (ImGui.IsItemHovered()) { ImGui.BeginTooltip(); - ImGui.Text( - "Right Eye Color will not apply to computed textures as it is selected using the vertex shaders"); + ImGui.Text("Right Eye Color will not apply to computed textures as it is " + + "selected using the vertex shaders"); ImGui.EndTooltip(); } diff --git a/Meddle/Meddle.Plugin/packages.lock.json b/Meddle/Meddle.Plugin/packages.lock.json index 38ca243..f5e8c4a 100644 --- a/Meddle/Meddle.Plugin/packages.lock.json +++ b/Meddle/Meddle.Plugin/packages.lock.json @@ -71,9 +71,9 @@ }, "OpenTelemetry.Instrumentation.Process": { "type": "Direct", - "requested": "[0.5.0-beta.6, )", - "resolved": "0.5.0-beta.6", - "contentHash": "mRYkk5H3/cM4fSwhF02PCAo8XtE0Ezn2NWn0KorJlREWvBJXe1lGt7jL1foC9lnfoUULieU7iCudSZTVA9kHqQ==", + "requested": "[0.5.0-beta.7, )", + "resolved": "0.5.0-beta.7", + "contentHash": "v+g5CPU/sJhyc1FTOgTaQTwvrQXquSxdndF+E3fS8SXYPLVAa2INHa8jqCFKvYp1rO+yaVizRw1gCOaDJJzauA==", "dependencies": { "OpenTelemetry.Api": "[1.9.0, 2.0.0)" } diff --git a/Meddle/Meddle.Utils/BoneNodeBuilder.cs b/Meddle/Meddle.Utils/BoneNodeBuilder.cs index 53ff447..24f2447 100644 --- a/Meddle/Meddle.Utils/BoneNodeBuilder.cs +++ b/Meddle/Meddle.Utils/BoneNodeBuilder.cs @@ -1,4 +1,6 @@ using SharpGLTF.Scenes; +using SharpGLTF.Transforms; + namespace Meddle.Utils; @@ -8,7 +10,9 @@ public class BoneNodeBuilder(string name) : NodeBuilder(name) public string PartialSkeletonHandle { get; set; } public int BoneIndex { get; set; } public int PartialSkeletonIndex { get; set; } - + public AffineTransform? MeddleLocalTransform { get; set; } + public AffineTransform? MeddleModelTransform { get; set; } + /// /// Sets the suffix of this bone and all its children. /// If the suffix is null, the bone name will be reset to the original bone name.