From 5c15a3a4ffdac635b82f11aeb7bba4ad8b715899 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 19 Jan 2024 02:09:43 +1100 Subject: [PATCH 1/8] Set up notifier infrastructure --- Penumbra/Import/Models/IoNotifier.cs | 52 +++++++++++++++++++ .../ModEditWindow.Models.MdlTab.cs | 1 + .../UI/AdvancedWindow/ModEditWindow.Models.cs | 37 +++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 Penumbra/Import/Models/IoNotifier.cs diff --git a/Penumbra/Import/Models/IoNotifier.cs b/Penumbra/Import/Models/IoNotifier.cs new file mode 100644 index 00000000..e1d649f6 --- /dev/null +++ b/Penumbra/Import/Models/IoNotifier.cs @@ -0,0 +1,52 @@ +using Dalamud.Interface.Internal.Notifications; +using OtterGui.Classes; + +namespace Penumbra.Import.Models; + +public record class IoNotifier +{ + /// Notification subclass so that we have a distinct type to filter by. + private class LegallyDistinctNotification : Notification + { + public LegallyDistinctNotification(string content, NotificationType type): base(content, type) + {} + } + + private readonly DateTime _startTime = DateTime.UtcNow; + private string _context = ""; + + /// Create a new notifier with the specified context appended to any other context already present. + public IoNotifier WithContext(string context) + => this with { _context = $"{_context}{context}: "}; + + /// Send a warning with any current context to notification channels. + public void Warning(string content) + => SendNotification(content, NotificationType.Warning); + + /// Get the current warnings for this notifier. + /// This does not currently filter to notifications with the current notifier's context - it will return all IO notifications from all notifiers. + public IEnumerable GetWarnings() + => GetFilteredNotifications(NotificationType.Warning); + + /// Create an exception with any current context. + [StackTraceHidden] + public Exception Exception(string message) + => Exception(message); + + /// Create an exception of the provided type with any current context. + [StackTraceHidden] + public TException Exception(string message) + where TException : Exception, new() + => (TException)Activator.CreateInstance(typeof(TException), $"{_context}{message}")!; + + private void SendNotification(string message, NotificationType type) + => Penumbra.Messager.AddMessage( + new LegallyDistinctNotification($"{_context}{message}", type), + true, false, true, false + ); + + private IEnumerable GetFilteredNotifications(NotificationType type) + => Penumbra.Messager + .Where(p => p.Key >= _startTime && p.Value is LegallyDistinctNotification && p.Value.NotificationType == type) + .Select(p => p.Value.PrintMessage); +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 26fcd1ee..15c6cb21 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -25,6 +25,7 @@ public partial class ModEditWindow private bool _dirty; public bool PendingIo { get; private set; } public List IoExceptions { get; private set; } = []; + public List IoWarnings { get; private set; } = []; public MdlTab(ModEditWindow edit, byte[] bytes, string path) { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index ad609285..1a200fdf 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -63,6 +63,7 @@ public partial class ModEditWindow DrawExport(tab, childSize, disabled); DrawIoExceptions(tab); + DrawIoWarnings(tab); } private void DrawImport(MdlTab tab, Vector2 size, bool _1) @@ -148,7 +149,43 @@ public partial class ModEditWindow using var exceptionNode = ImRaii.TreeNode(message); if (exceptionNode) + { + ImGui.Dummy(new Vector2(ImGui.GetStyle().IndentSpacing, 0)); + ImGui.SameLine(); ImGuiUtil.TextWrapped(exception.ToString()); + } + } + } + + private static void DrawIoWarnings(MdlTab tab) + { + if (tab.IoWarnings.Count == 0) + return; + + var size = new Vector2(ImGui.GetContentRegionAvail().X, 0); + using var frame = ImRaii.FramedGroup("Warnings", size, headerPreIcon: FontAwesomeIcon.ExclamationCircle, borderColor: 0xFF40FFFF); + + var spaceAvail = ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X - 100; + foreach (var (warning, index) in tab.IoWarnings.WithIndex()) + { + using var id = ImRaii.PushId(index); + var textSize = ImGui.CalcTextSize(warning).X; + + if (textSize <= spaceAvail) + { + ImRaii.TreeNode(warning, ImGuiTreeNodeFlags.Leaf).Dispose(); + continue; + } + + var firstLine = warning[..(int)Math.Floor(warning.Length * (spaceAvail / textSize))] + "..."; + + using var warningNode = ImRaii.TreeNode(firstLine); + if (warningNode) + { + ImGui.Dummy(new Vector2(ImGui.GetStyle().IndentSpacing, 0)); + ImGui.SameLine(); + ImGuiUtil.TextWrapped(warning.ToString()); + } } } From 6f3be39cb9288f2c7e46559b77b5264ec36b4ca4 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 19 Jan 2024 02:11:50 +1100 Subject: [PATCH 2/8] Wire up notifications through export --- .../Import/Models/Export/MaterialExporter.cs | 8 ++--- Penumbra/Import/Models/Export/MeshExporter.cs | 35 ++++++++++--------- .../Import/Models/Export/ModelExporter.cs | 15 ++++---- Penumbra/Import/Models/ModelManager.cs | 35 ++++++++++++------- .../ModEditWindow.Models.MdlTab.cs | 2 ++ 5 files changed, 55 insertions(+), 40 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 2a49e77f..61609bb5 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -23,7 +23,7 @@ public class MaterialExporter } /// Build a glTF material from a hydrated XIV model, with the provided name. - public static MaterialBuilder Export(Material material, string name) + public static MaterialBuilder Export(Material material, string name, IoNotifier notifier) { Penumbra.Log.Debug($"Exporting material \"{name}\"."); return material.Mtrl.ShaderPackage.Name switch @@ -34,7 +34,7 @@ public class MaterialExporter "hair.shpk" => BuildHair(material, name), "iris.shpk" => BuildIris(material, name), "skin.shpk" => BuildSkin(material, name), - _ => BuildFallback(material, name), + _ => BuildFallback(material, name, notifier), }; } @@ -335,9 +335,9 @@ public class MaterialExporter /// Build a material from a source with unknown semantics. /// Will make a loose effort to fetch common / simple textures. - private static MaterialBuilder BuildFallback(Material material, string name) + private static MaterialBuilder BuildFallback(Material material, string name, IoNotifier notifier) { - Penumbra.Log.Warning($"Unhandled shader package: {material.Mtrl.ShaderPackage.Name}"); + notifier.Warning($"Unhandled shader package: {material.Mtrl.ShaderPackage.Name}"); var materialBuilder = BuildSharedBase(material, name) .WithMetallicRoughnessShader() diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index da6b4df4..71e8f082 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -38,14 +38,16 @@ public class MeshExporter public string[] Attributes; } - public static Mesh Export(MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, GltfSkeleton? skeleton) + public static Mesh Export(MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, GltfSkeleton? skeleton, IoNotifier notifier) { - var self = new MeshExporter(mdl, lod, meshIndex, materials, skeleton?.Names); + var self = new MeshExporter(mdl, lod, meshIndex, materials, skeleton?.Names, notifier); return new Mesh(self.BuildMeshes(), skeleton?.Joints); } private const byte MaximumMeshBufferStreams = 3; + private readonly IoNotifier _notifier; + private readonly MdlFile _mdl; private readonly byte _lod; private readonly ushort _meshIndex; @@ -61,8 +63,9 @@ public class MeshExporter private readonly Type _materialType; private readonly Type _skinningType; - private MeshExporter(MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, IReadOnlyDictionary? boneNameMap) + private MeshExporter(MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, IReadOnlyDictionary? boneNameMap, IoNotifier notifier) { + _notifier = notifier; _mdl = mdl; _lod = lod; _meshIndex = meshIndex; @@ -84,7 +87,7 @@ public class MeshExporter // If there's skinning usages but no bone mapping, there's probably something wrong with the data. if (_skinningType != typeof(VertexEmpty) && _boneIndexMap == null) - Penumbra.Log.Warning($"Mesh {meshIndex} has skinned vertex usages but no bone information was provided."); + _notifier.Warning($"Skinned vertex usages but no bone information was provided."); Penumbra.Log.Debug( $"Mesh {meshIndex} using vertex types geometry: {_geometryType.Name}, material: {_materialType.Name}, skinning: {_skinningType.Name}"); @@ -105,7 +108,7 @@ public class MeshExporter { var boneName = _mdl.Bones[xivBoneIndex]; if (!boneNameMap.TryGetValue(boneName, out var gltfBoneIndex)) - throw new Exception($"Armature does not contain bone \"{boneName}\" requested by mesh {_meshIndex}."); + throw _notifier.Exception($"Armature does not contain bone \"{boneName}\". Ensure all dependencies are enabled in the current collection, and EST entries (if required) are configured."); indexMap.Add((ushort)tableIndex, gltfBoneIndex); } @@ -271,7 +274,7 @@ public class MeshExporter } /// Read a vertex attribute of the specified type from a vertex buffer stream. - private static object ReadVertexAttribute(MdlFile.VertexType type, BinaryReader reader) + private object ReadVertexAttribute(MdlFile.VertexType type, BinaryReader reader) { return type switch { @@ -284,15 +287,15 @@ public class MeshExporter MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf()), - _ => throw new ArgumentOutOfRangeException(), + var other => throw _notifier.Exception($"Unhandled vertex type {other}"), }; } /// Get the vertex geometry type for this mesh's vertex usages. - private static Type GetGeometryType(IReadOnlyDictionary usages) + private Type GetGeometryType(IReadOnlyDictionary usages) { if (!usages.ContainsKey(MdlFile.VertexUsage.Position)) - throw new Exception("Mesh does not contain position vertex elements."); + throw _notifier.Exception("Mesh does not contain position vertex elements."); if (!usages.ContainsKey(MdlFile.VertexUsage.Normal)) return typeof(VertexPosition); @@ -330,11 +333,11 @@ public class MeshExporter ); } - throw new Exception($"Unknown geometry type {_geometryType}."); + throw _notifier.Exception($"Unknown geometry type {_geometryType}."); } /// Get the vertex material type for this mesh's vertex usages. - private static Type GetMaterialType(IReadOnlyDictionary usages) + private Type GetMaterialType(IReadOnlyDictionary usages) { var uvCount = 0; if (usages.TryGetValue(MdlFile.VertexUsage.UV, out var type)) @@ -343,7 +346,7 @@ public class MeshExporter MdlFile.VertexType.Half2 => 1, MdlFile.VertexType.Half4 => 2, MdlFile.VertexType.Single4 => 2, - _ => throw new Exception($"Unexpected UV vertex type {type}."), + _ => throw _notifier.Exception($"Unexpected UV vertex type {type}."), }; var materialUsages = ( @@ -403,7 +406,7 @@ public class MeshExporter ); } - throw new Exception($"Unknown material type {_skinningType}"); + throw _notifier.Exception($"Unknown material type {_skinningType}"); } /// Get the vertex skinning type for this mesh's vertex usages. @@ -424,7 +427,7 @@ public class MeshExporter if (_skinningType == typeof(VertexJoints4)) { if (_boneIndexMap == null) - throw new Exception("Tried to build skinned vertex but no bone mappings are available."); + throw _notifier.Exception("Tried to build skinned vertex but no bone mappings are available."); var indices = ToByteArray(attributes[MdlFile.VertexUsage.BlendIndices]); var weights = ToVector4(attributes[MdlFile.VertexUsage.BlendWeights]); @@ -435,7 +438,7 @@ public class MeshExporter // NOTE: I've not seen any files that throw this error that aren't completely broken. var xivBoneIndex = indices[bindingIndex]; if (!_boneIndexMap.TryGetValue(xivBoneIndex, out var jointIndex)) - throw new Exception($"Vertex contains weight for unknown bone index {xivBoneIndex}."); + throw _notifier.Exception($"Vertex contains weight for unknown bone index {xivBoneIndex}."); return (jointIndex, weights[bindingIndex]); }) @@ -443,7 +446,7 @@ public class MeshExporter return new VertexJoints4(bindings); } - throw new Exception($"Unknown skinning type {_skinningType}"); + throw _notifier.Exception($"Unknown skinning type {_skinningType}"); } /// Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs index da24fbb0..550aaf11 100644 --- a/Penumbra/Import/Models/Export/ModelExporter.cs +++ b/Penumbra/Import/Models/Export/ModelExporter.cs @@ -23,16 +23,16 @@ public class ModelExporter } /// Export a model in preparation for usage in a glTF file. If provided, skeleton will be used to skin the resulting meshes where appropriate. - public static Model Export(MdlFile mdl, IEnumerable? xivSkeleton, Dictionary rawMaterials) + public static Model Export(MdlFile mdl, IEnumerable? xivSkeleton, Dictionary rawMaterials, IoNotifier notifier) { var gltfSkeleton = xivSkeleton != null ? ConvertSkeleton(xivSkeleton) : null; - var materials = ConvertMaterials(mdl, rawMaterials); - var meshes = ConvertMeshes(mdl, materials, gltfSkeleton); + var materials = ConvertMaterials(mdl, rawMaterials, notifier); + var meshes = ConvertMeshes(mdl, materials, gltfSkeleton, notifier); return new Model(meshes, gltfSkeleton); } /// Convert a .mdl to a mesh (group) per LoD. - private static List ConvertMeshes(MdlFile mdl, MaterialBuilder[] materials, GltfSkeleton? skeleton) + private static List ConvertMeshes(MdlFile mdl, MaterialBuilder[] materials, GltfSkeleton? skeleton, IoNotifier notifier) { var meshes = new List(); @@ -43,7 +43,8 @@ public class ModelExporter // TODO: consider other types of mesh? for (ushort meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++) { - var mesh = MeshExporter.Export(mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset), materials, skeleton); + var meshIndex = (ushort)(lod.MeshIndex + meshOffset); + var mesh = MeshExporter.Export(mdl, lodIndex, meshIndex, materials, skeleton, notifier.WithContext($"Mesh {meshIndex}")); meshes.Add(mesh); } } @@ -52,11 +53,11 @@ public class ModelExporter } /// Build materials for each of the material slots in the .mdl. - private static MaterialBuilder[] ConvertMaterials(MdlFile mdl, Dictionary rawMaterials) + private static MaterialBuilder[] ConvertMaterials(MdlFile mdl, Dictionary rawMaterials, IoNotifier notifier) => mdl.Materials // TODO: material generation should be fallible, which means this lookup should be a tryget, with a fallback. // fallback can likely be a static on the material exporter. - .Select(name => MaterialExporter.Export(rawMaterials[name], name)) + .Select(name => MaterialExporter.Export(rawMaterials[name], name, notifier.WithContext($"Material {name}"))) .ToArray(); /// Convert XIV skeleton data into a glTF-compatible node tree, with mappings. diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 7f1171f3..ffcb5bbe 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -37,20 +37,17 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect _tasks.Clear(); } - public Task ExportToGltf(MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) - => Enqueue(new ExportToGltfAction(this, mdl, sklbPaths, read, outputPath)); + public Task ExportToGltf(MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) + => EnqueueWithResult( + new ExportToGltfAction(this, mdl, sklbPaths, read, outputPath), + action => action.Notifier + ); public Task ImportGltf(string inputPath) - { - var action = new ImportGltfAction(inputPath); - return Enqueue(action).ContinueWith(task => - { - if (task is { IsFaulted: true, Exception: not null }) - throw task.Exception; - - return action.Out; - }); - } + => EnqueueWithResult( + new ImportGltfAction(inputPath), + action => action.Out + ); /// Try to find the .sklb paths for a .mdl file. /// .mdl file to look up the skeletons for. @@ -168,6 +165,16 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect return task; } + private Task EnqueueWithResult(TAction action, Func process) + where TAction : IAction + => Enqueue(action).ContinueWith(task => + { + if (task is { IsFaulted: true, Exception: not null }) + throw task.Exception; + + return process(action); + }); + private class ExportToGltfAction( ModelManager manager, MdlFile mdl, @@ -176,6 +183,8 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect string outputPath) : IAction { + public IoNotifier Notifier = new IoNotifier(); + public void Execute(CancellationToken cancel) { Penumbra.Log.Debug($"[GLTF Export] Exporting model to {outputPath}..."); @@ -190,7 +199,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect ); Penumbra.Log.Debug("[GLTF Export] Converting model..."); - var model = ModelExporter.Export(mdl, xivSkeletons, materials); + var model = ModelExporter.Export(mdl, xivSkeletons, materials, Notifier); Penumbra.Log.Debug("[GLTF Export] Building scene..."); var scene = new SceneBuilder(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 15c6cb21..17b46626 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -134,6 +134,8 @@ public partial class ModEditWindow .ContinueWith(task => { RecordIoExceptions(task.Exception); + if (task is { IsCompletedSuccessfully: true, Result: not null }) + IoWarnings = task.Result.GetWarnings().ToList(); PendingIo = false; }); } From 6da725350affe1bf78194acdac6663d510c29709 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 19 Jan 2024 02:56:01 +1100 Subject: [PATCH 3/8] Wire up notifier through import --- Penumbra/Import/Models/Import/MeshImporter.cs | 26 +++++++++++------- .../Import/Models/Import/ModelImporter.cs | 19 ++++++------- .../Import/Models/Import/SubMeshImporter.cs | 25 ++++++++--------- .../Import/Models/Import/VertexAttribute.cs | 27 +++++++++---------- Penumbra/Import/Models/ModelManager.cs | 7 ++--- .../ModEditWindow.Models.MdlTab.cs | 10 ++++--- 6 files changed, 63 insertions(+), 51 deletions(-) diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index 2a461304..28a7a9c1 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -3,7 +3,7 @@ using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Import; -public class MeshImporter(IEnumerable nodes) +public class MeshImporter(IEnumerable nodes, IoNotifier notifier) { public struct Mesh { @@ -33,9 +33,9 @@ public class MeshImporter(IEnumerable nodes) public List ShapeValues; } - public static Mesh Import(IEnumerable nodes) + public static Mesh Import(IEnumerable nodes, IoNotifier notifier) { - var importer = new MeshImporter(nodes); + var importer = new MeshImporter(nodes, notifier); return importer.Create(); } @@ -115,11 +115,11 @@ public class MeshImporter(IEnumerable nodes) var vertexOffset = _vertexCount; var indexOffset = _indices.Count; - var nodeBoneMap = CreateNodeBoneMap(node); - var subMesh = SubMeshImporter.Import(node, nodeBoneMap); - var subMeshName = node.Name ?? node.Mesh.Name; + var nodeBoneMap = CreateNodeBoneMap(node); + var subMesh = SubMeshImporter.Import(node, nodeBoneMap, notifier.WithContext($"Sub-mesh {subMeshName}")); + // TODO: Record a warning if there's a mismatch between current and incoming, as we can't support multiple materials per mesh. _material ??= subMesh.Material; @@ -127,8 +127,11 @@ public class MeshImporter(IEnumerable nodes) if (_vertexDeclaration == null) _vertexDeclaration = subMesh.VertexDeclaration; else if (VertexDeclarationMismatch(subMesh.VertexDeclaration, _vertexDeclaration.Value)) - throw new Exception( - $"Sub-mesh \"{subMeshName}\" vertex declaration mismatch. All sub-meshes of a mesh must have equivalent vertex declarations."); + throw notifier.Exception( + $@"All sub-meshes of a mesh must have equivalent vertex declarations. + Current: {FormatVertexDeclaration(_vertexDeclaration.Value)} + Sub-mesh ""{subMeshName}"": {FormatVertexDeclaration(subMesh.VertexDeclaration)}" + ); // Given that strides are derived from declarations, a lack of mismatch in declarations means the strides are fine. // TODO: I mean, given that strides are derivable, might be worth dropping strides from the sub mesh return structure and computing when needed. @@ -170,6 +173,9 @@ public class MeshImporter(IEnumerable nodes) }); } + private static string FormatVertexDeclaration(MdlStructs.VertexDeclarationStruct vertexDeclaration) + => string.Join(", ", vertexDeclaration.VertexElements.Select(element => $"{element.Usage} ({element.Type}@{element.Stream}:{element.Offset})")); + private static bool VertexDeclarationMismatch(MdlStructs.VertexDeclarationStruct a, MdlStructs.VertexDeclarationStruct b) { var elA = a.VertexElements; @@ -204,13 +210,13 @@ public class MeshImporter(IEnumerable nodes) var meshName = node.Name ?? mesh.Name ?? "(no name)"; var primitiveCount = mesh.Primitives.Count; if (primitiveCount != 1) - throw new Exception($"Mesh \"{meshName}\" has {primitiveCount} primitives, expected 1."); + throw notifier.Exception($"Mesh \"{meshName}\" has {primitiveCount} primitives, expected 1."); var primitive = mesh.Primitives[0]; // Per glTF specification, an asset with a skin MUST contain skinning attributes on its mesh. var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0") - ?? throw new Exception($"Skinned mesh \"{meshName}\" is skinned but does not contain skinning vertex attributes."); + ?? throw notifier.Exception($"Skinned mesh \"{meshName}\" is skinned but does not contain skinning vertex attributes."); // Build a set of joints that are referenced by this mesh. // TODO: Would be neat to omit 0-weighted joints here, but doing so will require some further work on bone mapping behavior to ensure the unweighted joints can still be resolved to valid bone indices during vertex data construction. diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs index 1b7fdfa5..bf59f278 100644 --- a/Penumbra/Import/Models/Import/ModelImporter.cs +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -1,14 +1,15 @@ using Lumina.Data.Parsing; +using OtterGui; using Penumbra.GameData.Files; using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Import; -public partial class ModelImporter(ModelRoot model) +public partial class ModelImporter(ModelRoot model, IoNotifier notifier) { - public static MdlFile Import(ModelRoot model) + public static MdlFile Import(ModelRoot model, IoNotifier notifier) { - var importer = new ModelImporter(model); + var importer = new ModelImporter(model, notifier); return importer.Create(); } @@ -39,8 +40,8 @@ public partial class ModelImporter(ModelRoot model) private MdlFile Create() { // Group and build out meshes in this model. - foreach (var subMeshNodes in GroupedMeshNodes()) - BuildMeshForGroup(subMeshNodes); + foreach (var (subMeshNodes, index) in GroupedMeshNodes().WithIndex()) + BuildMeshForGroup(subMeshNodes, index); // Now that all the meshes have been built, we can build some of the model-wide metadata. var materials = _materials.Count > 0 ? _materials : ["/NO_MATERIAL"]; @@ -128,7 +129,7 @@ public partial class ModelImporter(ModelRoot model) ) .OrderBy(group => group.Key); - private void BuildMeshForGroup(IEnumerable subMeshNodes) + private void BuildMeshForGroup(IEnumerable subMeshNodes, int index) { // Record some offsets we'll be using later, before they get mutated with mesh values. var subMeshOffset = _subMeshes.Count; @@ -136,7 +137,7 @@ public partial class ModelImporter(ModelRoot model) var indexOffset = _indices.Count; var shapeValueOffset = _shapeValues.Count; - var mesh = MeshImporter.Import(subMeshNodes); + var mesh = MeshImporter.Import(subMeshNodes, notifier.WithContext($"Mesh {index}")); var meshStartIndex = (uint)(mesh.MeshStruct.StartIndex + indexOffset); var materialIndex = mesh.Material != null @@ -196,7 +197,7 @@ public partial class ModelImporter(ModelRoot model) // arrays, values is practically guaranteed to be the highest of the // group, so a failure on any of them will be a failure on it. if (_shapeValues.Count > ushort.MaxValue) - throw new Exception($"Importing this file would require more than the maximum of {ushort.MaxValue} shape values.\nTry removing or applying shape keys that do not need to be changed at runtime in-game."); + throw notifier.Exception($"Importing this file would require more than the maximum of {ushort.MaxValue} shape values.\nTry removing or applying shape keys that do not need to be changed at runtime in-game."); } private ushort GetMaterialIndex(string materialName) @@ -232,7 +233,7 @@ public partial class ModelImporter(ModelRoot model) } if (boneIndices.Count > 64) - throw new Exception("XIV does not support meshes weighted to more than 64 bones."); + throw notifier.Exception("XIV does not support meshes weighted to a total of more than 64 bones."); var boneIndicesArray = new ushort[64]; Array.Copy(boneIndices.ToArray(), boneIndicesArray, boneIndices.Count); diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs index 6a5d0d52..a7e0d583 100644 --- a/Penumbra/Import/Models/Import/SubMeshImporter.cs +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -28,12 +28,14 @@ public class SubMeshImporter public Dictionary> ShapeValues; } - public static SubMesh Import(Node node, IDictionary? nodeBoneMap) + public static SubMesh Import(Node node, IDictionary? nodeBoneMap, IoNotifier notifier) { - var importer = new SubMeshImporter(node, nodeBoneMap); + var importer = new SubMeshImporter(node, nodeBoneMap, notifier); return importer.Create(); } + private readonly IoNotifier _notifier; + private readonly MeshPrimitive _primitive; private readonly IDictionary? _nodeBoneMap; private readonly IDictionary? _nodeExtras; @@ -53,16 +55,15 @@ public class SubMeshImporter private readonly List? _morphNames; private Dictionary>? _shapeValues; - private SubMeshImporter(Node node, IDictionary? nodeBoneMap) + private SubMeshImporter(Node node, IDictionary? nodeBoneMap, IoNotifier notifier) { + _notifier = notifier; + var mesh = node.Mesh; var primitiveCount = mesh.Primitives.Count; if (primitiveCount != 1) - { - var name = node.Name ?? mesh.Name ?? "(no name)"; - throw new Exception($"Mesh \"{name}\" has {primitiveCount} primitives, expected 1."); - } + throw _notifier.Exception($"Mesh has {primitiveCount} primitives, expected 1."); _primitive = mesh.Primitives[0]; _nodeBoneMap = nodeBoneMap; @@ -115,7 +116,7 @@ public class SubMeshImporter { < 32 => (1u << _metaAttributes.Length) - 1, 32 => uint.MaxValue, - > 32 => throw new Exception("Models may utilise a maximum of 32 attributes."), + > 32 => throw _notifier.Exception("Models may utilise a maximum of 32 attributes."), }; return new SubMesh() @@ -165,11 +166,11 @@ public class SubMeshImporter // The order here is chosen to match a typical model's element order. var rawAttributes = new[] { - VertexAttribute.Position(accessors, morphAccessors), - VertexAttribute.BlendWeight(accessors), - VertexAttribute.BlendIndex(accessors, _nodeBoneMap), + VertexAttribute.Position(accessors, morphAccessors, _notifier), + VertexAttribute.BlendWeight(accessors, _notifier), + VertexAttribute.BlendIndex(accessors, _nodeBoneMap, _notifier), VertexAttribute.Normal(accessors, morphAccessors), - VertexAttribute.Tangent1(accessors, morphAccessors, _indices), + VertexAttribute.Tangent1(accessors, morphAccessors, _indices, _notifier), VertexAttribute.Color(accessors), VertexAttribute.Uv(accessors), }; diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 7c875162..b73f6a89 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -72,10 +72,10 @@ public class VertexAttribute private byte[] DefaultBuildMorph(int morphIndex, int vertexIndex) => Build(vertexIndex); - public static VertexAttribute Position(Accessors accessors, IEnumerable morphAccessors) + public static VertexAttribute Position(Accessors accessors, IEnumerable morphAccessors, IoNotifier notifier) { if (!accessors.TryGetValue("POSITION", out var accessor)) - throw new Exception("Meshes must contain a POSITION attribute."); + throw notifier.Exception("Meshes must contain a POSITION attribute."); var element = new MdlStructs.VertexElement() { @@ -115,13 +115,13 @@ public class VertexAttribute ); } - public static VertexAttribute? BlendWeight(Accessors accessors) + public static VertexAttribute? BlendWeight(Accessors accessors, IoNotifier notifier) { if (!accessors.TryGetValue("WEIGHTS_0", out var accessor)) return null; if (!accessors.ContainsKey("JOINTS_0")) - throw new Exception("Mesh contained WEIGHTS_0 attribute but no corresponding JOINTS_0 attribute."); + throw notifier.Exception("Mesh contained WEIGHTS_0 attribute but no corresponding JOINTS_0 attribute."); var element = new MdlStructs.VertexElement() { @@ -138,16 +138,16 @@ public class VertexAttribute ); } - public static VertexAttribute? BlendIndex(Accessors accessors, IDictionary? boneMap) + public static VertexAttribute? BlendIndex(Accessors accessors, IDictionary? boneMap, IoNotifier notifier) { if (!accessors.TryGetValue("JOINTS_0", out var accessor)) return null; if (!accessors.ContainsKey("WEIGHTS_0")) - throw new Exception("Mesh contained JOINTS_0 attribute but no corresponding WEIGHTS_0 attribute."); + throw notifier.Exception("Mesh contained JOINTS_0 attribute but no corresponding WEIGHTS_0 attribute."); if (boneMap == null) - throw new Exception("Mesh contained JOINTS_0 attribute but no bone mapping was created."); + throw notifier.Exception("Mesh contained JOINTS_0 attribute but no bone mapping was created."); var element = new MdlStructs.VertexElement() { @@ -242,22 +242,22 @@ public class VertexAttribute ); } - public static VertexAttribute? Tangent1(Accessors accessors, IEnumerable morphAccessors, ushort[] indices) + public static VertexAttribute? Tangent1(Accessors accessors, IEnumerable morphAccessors, ushort[] indices, IoNotifier notifier) { if (!accessors.TryGetValue("NORMAL", out var normalAccessor)) { - Penumbra.Log.Warning("Normals are required to facilitate import or calculation of tangents."); + notifier.Warning("Normals are required to facilitate import or calculation of tangents."); return null; } var normals = normalAccessor.AsVector3Array(); var tangents = accessors.TryGetValue("TANGENT", out var accessor) ? accessor.AsVector4Array() - : CalculateTangents(accessors, indices, normals); + : CalculateTangents(accessors, indices, normals, notifier); if (tangents == null) { - Penumbra.Log.Warning("No tangents available for sub-mesh. This could lead to incorrect lighting, or mismatched vertex attributes."); + notifier.Warning("No tangents available for sub-mesh. This could lead to incorrect lighting, or mismatched vertex attributes."); return null; } @@ -309,7 +309,7 @@ public class VertexAttribute } /// Attempt to calculate tangent values based on other pre-existing data. - private static Vector4[]? CalculateTangents(Accessors accessors, ushort[] indices, IList normals) + private static Vector4[]? CalculateTangents(Accessors accessors, ushort[] indices, IList normals, IoNotifier notifier) { // To calculate tangents, we will also need access to uv data. if (!accessors.TryGetValue("TEXCOORD_0", out var uvAccessor)) @@ -318,8 +318,7 @@ public class VertexAttribute var positions = accessors["POSITION"].AsVector3Array(); var uvs = uvAccessor.AsVector2Array(); - // TODO: Surface this in the UI. - Penumbra.Log.Warning( + notifier.Warning( "Calculating tangents, this may result in degraded light interaction. For best results, ensure tangents are caculated or retained during export from 3D modelling tools."); var vertexCount = positions.Count; diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index ffcb5bbe..c41f28e5 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -43,10 +43,10 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect action => action.Notifier ); - public Task ImportGltf(string inputPath) + public Task<(MdlFile?, IoNotifier)> ImportGltf(string inputPath) => EnqueueWithResult( new ImportGltfAction(inputPath), - action => action.Out + action => (action.Out, action.Notifier) ); /// Try to find the .sklb paths for a .mdl file. @@ -273,12 +273,13 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect private partial class ImportGltfAction(string inputPath) : IAction { public MdlFile? Out; + public IoNotifier Notifier = new IoNotifier(); public void Execute(CancellationToken cancel) { var model = Schema2.ModelRoot.Load(inputPath); - Out = ModelImporter.Import(model); + Out = ModelImporter.Import(model, Notifier); } public bool Equals(IAction? other) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 17b46626..6decd344 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -2,6 +2,7 @@ using Lumina.Data.Parsing; using OtterGui; using Penumbra.GameData; using Penumbra.GameData.Files; +using Penumbra.Import.Models; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; @@ -146,11 +147,14 @@ public partial class ModEditWindow { PendingIo = true; _edit._models.ImportGltf(inputPath) - .ContinueWith(task => + .ContinueWith((Task<(MdlFile?, IoNotifier)> task) => { RecordIoExceptions(task.Exception); - if (task is { IsCompletedSuccessfully: true, Result: not null }) - FinalizeImport(task.Result); + if (task is { IsCompletedSuccessfully: true, Result: (not null, _) }) + { + IoWarnings = task.Result.Item2.GetWarnings().ToList(); + FinalizeImport(task.Result.Item1); + } PendingIo = false; }); } From aa01acd76a8fd9b37cfbadcb45e791a3c4fa43bf Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 19 Jan 2024 19:41:00 +1100 Subject: [PATCH 4/8] Move off messager --- Penumbra/Import/Models/IoNotifier.cs | 34 +++++++++------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/Penumbra/Import/Models/IoNotifier.cs b/Penumbra/Import/Models/IoNotifier.cs index e1d649f6..56ef7103 100644 --- a/Penumbra/Import/Models/IoNotifier.cs +++ b/Penumbra/Import/Models/IoNotifier.cs @@ -1,19 +1,11 @@ -using Dalamud.Interface.Internal.Notifications; -using OtterGui.Classes; +using OtterGui.Log; namespace Penumbra.Import.Models; public record class IoNotifier { - /// Notification subclass so that we have a distinct type to filter by. - private class LegallyDistinctNotification : Notification - { - public LegallyDistinctNotification(string content, NotificationType type): base(content, type) - {} - } - - private readonly DateTime _startTime = DateTime.UtcNow; - private string _context = ""; + private readonly List _messages = []; + private string _context = ""; /// Create a new notifier with the specified context appended to any other context already present. public IoNotifier WithContext(string context) @@ -21,12 +13,12 @@ public record class IoNotifier /// Send a warning with any current context to notification channels. public void Warning(string content) - => SendNotification(content, NotificationType.Warning); + => SendMessage(content, Logger.LogLevel.Warning); /// Get the current warnings for this notifier. /// This does not currently filter to notifications with the current notifier's context - it will return all IO notifications from all notifiers. public IEnumerable GetWarnings() - => GetFilteredNotifications(NotificationType.Warning); + => _messages; /// Create an exception with any current context. [StackTraceHidden] @@ -39,14 +31,10 @@ public record class IoNotifier where TException : Exception, new() => (TException)Activator.CreateInstance(typeof(TException), $"{_context}{message}")!; - private void SendNotification(string message, NotificationType type) - => Penumbra.Messager.AddMessage( - new LegallyDistinctNotification($"{_context}{message}", type), - true, false, true, false - ); - - private IEnumerable GetFilteredNotifications(NotificationType type) - => Penumbra.Messager - .Where(p => p.Key >= _startTime && p.Value is LegallyDistinctNotification && p.Value.NotificationType == type) - .Select(p => p.Value.PrintMessage); + private void SendMessage(string message, Logger.LogLevel type) + { + var fullText = $"{_context}{message}"; + Penumbra.Log.Message(type, fullText); + _messages.Add(fullText); + } } From 0486d049b0dbec021d9f9a3497eecf8980e33473 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 19 Jan 2024 22:20:54 +1100 Subject: [PATCH 5/8] Make material export fallible --- .../Import/Models/Export/MaterialExporter.cs | 6 +++ .../Import/Models/Export/ModelExporter.cs | 11 ++-- Penumbra/Import/Models/ModelManager.cs | 53 +++++++++++++------ .../ModEditWindow.Models.MdlTab.cs | 8 +-- 4 files changed, 54 insertions(+), 24 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 61609bb5..307e9d2b 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -22,6 +22,12 @@ public class MaterialExporter // variant? } + /// Dependency-less material configuration, for use when no material data can be resolved. + public static readonly MaterialBuilder Unknown = new MaterialBuilder("UNKNOWN") + .WithMetallicRoughnessShader() + .WithDoubleSide(true) + .WithBaseColor(Vector4.One); + /// Build a glTF material from a hydrated XIV model, with the provided name. public static MaterialBuilder Export(Material material, string name, IoNotifier notifier) { diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs index 550aaf11..9bc33697 100644 --- a/Penumbra/Import/Models/Export/ModelExporter.cs +++ b/Penumbra/Import/Models/Export/ModelExporter.cs @@ -55,9 +55,14 @@ public class ModelExporter /// Build materials for each of the material slots in the .mdl. private static MaterialBuilder[] ConvertMaterials(MdlFile mdl, Dictionary rawMaterials, IoNotifier notifier) => mdl.Materials - // TODO: material generation should be fallible, which means this lookup should be a tryget, with a fallback. - // fallback can likely be a static on the material exporter. - .Select(name => MaterialExporter.Export(rawMaterials[name], name, notifier.WithContext($"Material {name}"))) + .Select(name => + { + if (rawMaterials.TryGetValue(name, out var rawMaterial)) + return MaterialExporter.Export(rawMaterial, name, notifier.WithContext($"Material {name}")); + + notifier.Warning($"Material \"{name}\" missing, using blank fallback."); + return MaterialExporter.Unknown; + }) .ToArray(); /// Convert XIV skeleton data into a glTF-compatible node tree, with mappings. diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index c41f28e5..bfd55281 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -37,7 +37,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect _tasks.Clear(); } - public Task ExportToGltf(MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) + public Task ExportToGltf(MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) => EnqueueWithResult( new ExportToGltfAction(this, mdl, sklbPaths, read, outputPath), action => action.Notifier @@ -106,7 +106,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect } /// Try to resolve the absolute path to a .mtrl from the potentially-partial path provided by a model. - private string ResolveMtrlPath(string rawPath) + private string? ResolveMtrlPath(string rawPath, IoNotifier notifier) { // TODO: this should probably be chosen in the export settings var variantId = 1; @@ -119,13 +119,18 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect ? rawPath : '/' + Path.GetFileName(rawPath); - // TODO: this should be a recoverable warning if (absolutePath == null) - throw new Exception("Failed to resolve material path."); + { + notifier.Warning($"Material path \"{rawPath}\" could not be resolved."); + return null; + } var info = parser.GetFileInfo(absolutePath); if (info.FileType is not FileType.Material) - throw new Exception($"Material path {rawPath} does not conform to material conventions."); + { + notifier.Warning($"Material path {rawPath} does not conform to material conventions."); + return null; + } var resolvedPath = info.ObjectType switch { @@ -179,7 +184,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect ModelManager manager, MdlFile mdl, IEnumerable sklbPaths, - Func read, + Func read, string outputPath) : IAction { @@ -193,10 +198,10 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect var xivSkeletons = BuildSkeletons(cancel); Penumbra.Log.Debug("[GLTF Export] Reading materials..."); - var materials = mdl.Materials.ToDictionary( - path => path, - path => BuildMaterial(path, cancel) - ); + var materials = mdl.Materials + .Select(path => (path, material: BuildMaterial(path, Notifier, cancel))) + .Where(pair => pair.material != null) + .ToDictionary(pair => pair.path, pair => pair.material!.Value); Penumbra.Log.Debug("[GLTF Export] Converting model..."); var model = ModelExporter.Export(mdl, xivSkeletons, materials, Notifier); @@ -215,7 +220,9 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect private IEnumerable BuildSkeletons(CancellationToken cancel) { var havokTasks = sklbPaths - .Select(path => new SklbFile(read(path))) + .Select(path => read(path) ?? throw new Exception( + $"Resolved skeleton \"{path}\" could not be read. Ensure EST metadata is configured, and/or relevant mods are enabled in the current collection.")) + .Select(bytes => new SklbFile(bytes)) .WithIndex() .Select(CreateHavokTask) .ToArray(); @@ -234,10 +241,15 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect } /// Read a .mtrl and populate its textures. - private MaterialExporter.Material BuildMaterial(string relativePath, CancellationToken cancel) + private MaterialExporter.Material? BuildMaterial(string relativePath, IoNotifier notifier, CancellationToken cancel) { - var path = manager.ResolveMtrlPath(relativePath); - var mtrl = new MtrlFile(read(path)); + var path = manager.ResolveMtrlPath(relativePath, notifier); + if (path == null) + return null; + var bytes = read(path); + if (bytes == null) + return null; + var mtrl = new MtrlFile(bytes); return new MaterialExporter.Material { @@ -254,12 +266,23 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect { // Work out the texture's path - the DX11 material flag controls a file name prefix. GamePaths.Tex.HandleDx11Path(texture, out var texturePath); - using var textureData = new MemoryStream(read(texturePath)); + var bytes = read(texturePath); + if (bytes == null) + return CreateDummyImage(); + + using var textureData = new MemoryStream(bytes); var image = TexFileParser.Parse(textureData); var pngImage = TextureManager.ConvertToPng(image, cancel).AsPng; return pngImage ?? throw new Exception("Failed to convert texture to png."); } + private Image CreateDummyImage() + { + var image = new Image(1, 1); + image[0, 0] = Color.White; + return image; + } + public bool Equals(IAction? other) { if (other is not ExportToGltfAction rhs) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 6decd344..cb8e662f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -260,7 +260,7 @@ public partial class ModEditWindow /// Read a file from the active collection or game. /// Game path to the file to load. // TODO: Also look up files within the current mod regardless of mod state? - private byte[] ReadFile(string path) + private byte[]? ReadFile(string path) { // TODO: if cross-collection lookups are turned off, this conversion can be skipped if (!Utf8GamePath.FromString(path, out var utf8Path, true)) @@ -269,13 +269,9 @@ public partial class ModEditWindow var resolvedPath = _edit._activeCollections.Current.ResolvePath(utf8Path) ?? new FullPath(utf8Path); // TODO: is it worth trying to use streams for these instead? I'll need to do this for mtrl/tex too, so might be a good idea. that said, the mtrl reader doesn't accept streams, so... - var bytes = resolvedPath.IsRooted + return resolvedPath.IsRooted ? File.ReadAllBytes(resolvedPath.FullName) : _edit._gameData.GetFile(resolvedPath.InternalName.ToString())?.Data; - - // TODO: some callers may not care about failures - handle exceptions separately? - return bytes ?? throw new Exception( - $"Resolved path {path} could not be found. If modded, is it enabled in the current collection?"); } /// Remove the material given by the index. From cbd99f833a1fd3f13ed149dea8bd1344049c56be Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 20 Jan 2024 00:03:58 +1100 Subject: [PATCH 6/8] Allow export of missing bones with warnings --- Penumbra/Import/Models/Export/Config.cs | 6 +++ Penumbra/Import/Models/Export/MeshExporter.cs | 39 ++++++++++++------- .../Import/Models/Export/ModelExporter.cs | 12 +++--- Penumbra/Import/Models/Export/Skeleton.cs | 12 +++++- Penumbra/Import/Models/ModelManager.cs | 16 +++++--- .../ModEditWindow.Models.MdlTab.cs | 5 ++- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 8 ++++ 7 files changed, 71 insertions(+), 27 deletions(-) create mode 100644 Penumbra/Import/Models/Export/Config.cs diff --git a/Penumbra/Import/Models/Export/Config.cs b/Penumbra/Import/Models/Export/Config.cs new file mode 100644 index 00000000..58329a1d --- /dev/null +++ b/Penumbra/Import/Models/Export/Config.cs @@ -0,0 +1,6 @@ +namespace Penumbra.Import.Models.Export; + +public struct ExportConfig +{ + public bool GenerateMissingBones; +} diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 71e8f082..83a0c3cf 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -13,14 +13,14 @@ namespace Penumbra.Import.Models.Export; public class MeshExporter { - public class Mesh(IEnumerable meshes, NodeBuilder[]? joints) + public class Mesh(IEnumerable meshes, GltfSkeleton? skeleton) { public void AddToScene(SceneBuilder scene) { foreach (var data in meshes) { - var instance = joints != null - ? scene.AddSkinnedMesh(data.Mesh, Matrix4x4.Identity, joints) + var instance = skeleton != null + ? scene.AddSkinnedMesh(data.Mesh, Matrix4x4.Identity, [.. skeleton.Value.Joints]) : scene.AddRigidMesh(data.Mesh, Matrix4x4.Identity); var extras = new Dictionary(data.Attributes.Length); @@ -38,15 +38,16 @@ public class MeshExporter public string[] Attributes; } - public static Mesh Export(MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, GltfSkeleton? skeleton, IoNotifier notifier) + public static Mesh Export(ExportConfig config, MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, GltfSkeleton? skeleton, IoNotifier notifier) { - var self = new MeshExporter(mdl, lod, meshIndex, materials, skeleton?.Names, notifier); - return new Mesh(self.BuildMeshes(), skeleton?.Joints); + var self = new MeshExporter(config, mdl, lod, meshIndex, materials, skeleton, notifier); + return new Mesh(self.BuildMeshes(), skeleton); } private const byte MaximumMeshBufferStreams = 3; - private readonly IoNotifier _notifier; + private readonly ExportConfig _config; + private readonly IoNotifier _notifier; private readonly MdlFile _mdl; private readonly byte _lod; @@ -63,8 +64,10 @@ public class MeshExporter private readonly Type _materialType; private readonly Type _skinningType; - private MeshExporter(MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, IReadOnlyDictionary? boneNameMap, IoNotifier notifier) + // TODO: This signature is getting out of control. + private MeshExporter(ExportConfig config, MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, GltfSkeleton? skeleton, IoNotifier notifier) { + _config = config; _notifier = notifier; _mdl = mdl; _lod = lod; @@ -72,8 +75,8 @@ public class MeshExporter _material = materials[XivMesh.MaterialIndex]; - if (boneNameMap != null) - _boneIndexMap = BuildBoneIndexMap(boneNameMap); + if (skeleton != null) + _boneIndexMap = BuildBoneIndexMap(skeleton.Value); var usages = _mdl.VertexDeclarations[_meshIndex].VertexElements .ToImmutableDictionary( @@ -94,7 +97,7 @@ public class MeshExporter } /// Build a mapping between indices in this mesh's bone table (if any), and the glTF joint indices provided. - private Dictionary? BuildBoneIndexMap(IReadOnlyDictionary boneNameMap) + private Dictionary? BuildBoneIndexMap(GltfSkeleton skeleton) { // A BoneTableIndex of 255 means that this mesh is not skinned. if (XivMesh.BoneTableIndex == 255) @@ -107,8 +110,18 @@ public class MeshExporter foreach (var (xivBoneIndex, tableIndex) in xivBoneTable.BoneIndex.Take(xivBoneTable.BoneCount).WithIndex()) { var boneName = _mdl.Bones[xivBoneIndex]; - if (!boneNameMap.TryGetValue(boneName, out var gltfBoneIndex)) - throw _notifier.Exception($"Armature does not contain bone \"{boneName}\". Ensure all dependencies are enabled in the current collection, and EST entries (if required) are configured."); + if (!skeleton.Names.TryGetValue(boneName, out var gltfBoneIndex)) + { + if (!_config.GenerateMissingBones) + throw _notifier.Exception( + $@"Armature does not contain bone ""{boneName}"". + Ensure all dependencies are enabled in the current collection, and EST entries (if required) are configured. + If this is a known issue with this model and you would like to export anyway, enable the ""Generate missing bones"" option." + ); + + (_, gltfBoneIndex) = skeleton.GenerateBone(boneName); + _notifier.Warning($"Generated missing bone \"{boneName}\". Vertices weighted to this bone will not move with the rest of the armature."); + } indexMap.Add((ushort)tableIndex, gltfBoneIndex); } diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs index 9bc33697..b3e9c68d 100644 --- a/Penumbra/Import/Models/Export/ModelExporter.cs +++ b/Penumbra/Import/Models/Export/ModelExporter.cs @@ -23,16 +23,16 @@ public class ModelExporter } /// Export a model in preparation for usage in a glTF file. If provided, skeleton will be used to skin the resulting meshes where appropriate. - public static Model Export(MdlFile mdl, IEnumerable? xivSkeleton, Dictionary rawMaterials, IoNotifier notifier) + public static Model Export(ExportConfig config, MdlFile mdl, IEnumerable xivSkeletons, Dictionary rawMaterials, IoNotifier notifier) { - var gltfSkeleton = xivSkeleton != null ? ConvertSkeleton(xivSkeleton) : null; + var gltfSkeleton = ConvertSkeleton(xivSkeletons); var materials = ConvertMaterials(mdl, rawMaterials, notifier); - var meshes = ConvertMeshes(mdl, materials, gltfSkeleton, notifier); + var meshes = ConvertMeshes(config, mdl, materials, gltfSkeleton, notifier); return new Model(meshes, gltfSkeleton); } /// Convert a .mdl to a mesh (group) per LoD. - private static List ConvertMeshes(MdlFile mdl, MaterialBuilder[] materials, GltfSkeleton? skeleton, IoNotifier notifier) + private static List ConvertMeshes(ExportConfig config, MdlFile mdl, MaterialBuilder[] materials, GltfSkeleton? skeleton, IoNotifier notifier) { var meshes = new List(); @@ -44,7 +44,7 @@ public class ModelExporter for (ushort meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++) { var meshIndex = (ushort)(lod.MeshIndex + meshOffset); - var mesh = MeshExporter.Export(mdl, lodIndex, meshIndex, materials, skeleton, notifier.WithContext($"Mesh {meshIndex}")); + var mesh = MeshExporter.Export(config, mdl, lodIndex, meshIndex, materials, skeleton, notifier.WithContext($"Mesh {meshIndex}")); meshes.Add(mesh); } } @@ -105,7 +105,7 @@ public class ModelExporter return new GltfSkeleton { Root = root, - Joints = [.. joints], + Joints = joints, Names = names, }; } diff --git a/Penumbra/Import/Models/Export/Skeleton.cs b/Penumbra/Import/Models/Export/Skeleton.cs index fee107a0..ca72a1f8 100644 --- a/Penumbra/Import/Models/Export/Skeleton.cs +++ b/Penumbra/Import/Models/Export/Skeleton.cs @@ -28,8 +28,18 @@ public struct GltfSkeleton public NodeBuilder Root; /// Flattened list of skeleton nodes. - public NodeBuilder[] Joints; + public List Joints; /// Mapping of bone names to their index within the joints array. public Dictionary Names; + + public (NodeBuilder, int) GenerateBone(string name) + { + var node = new NodeBuilder(name); + var index = Joints.Count; + Names[name] = index; + Joints.Add(node); + Root.AddNode(node); + return (node, index); + } } diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index bfd55281..5340d556 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -37,9 +37,9 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect _tasks.Clear(); } - public Task ExportToGltf(MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) + public Task ExportToGltf(ExportConfig config, MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) => EnqueueWithResult( - new ExportToGltfAction(this, mdl, sklbPaths, read, outputPath), + new ExportToGltfAction(this, config, mdl, sklbPaths, read, outputPath), action => action.Notifier ); @@ -182,6 +182,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect private class ExportToGltfAction( ModelManager manager, + ExportConfig config, MdlFile mdl, IEnumerable sklbPaths, Func read, @@ -204,7 +205,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect .ToDictionary(pair => pair.path, pair => pair.material!.Value); Penumbra.Log.Debug("[GLTF Export] Converting model..."); - var model = ModelExporter.Export(mdl, xivSkeletons, materials, Notifier); + var model = ModelExporter.Export(config, mdl, xivSkeletons, materials, Notifier); Penumbra.Log.Debug("[GLTF Export] Building scene..."); var scene = new SceneBuilder(); @@ -219,10 +220,13 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect /// Attempt to read out the pertinent information from the sklb file paths provided. private IEnumerable BuildSkeletons(CancellationToken cancel) { + // We're intentionally filtering failed reads here - the failure will + // be picked up, if relevant, when the model tries to create mappings + // for a bone in the failed sklb. var havokTasks = sklbPaths - .Select(path => read(path) ?? throw new Exception( - $"Resolved skeleton \"{path}\" could not be read. Ensure EST metadata is configured, and/or relevant mods are enabled in the current collection.")) - .Select(bytes => new SklbFile(bytes)) + .Select(path => read(path)) + .Where(bytes => bytes != null) + .Select(bytes => new SklbFile(bytes!)) .WithIndex() .Select(CreateHavokTask) .ToArray(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index cb8e662f..5b2f024c 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -3,6 +3,7 @@ using OtterGui; using Penumbra.GameData; using Penumbra.GameData.Files; using Penumbra.Import.Models; +using Penumbra.Import.Models.Export; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; @@ -20,6 +21,8 @@ public partial class ModEditWindow public bool ImportKeepMaterials; public bool ImportKeepAttributes; + public ExportConfig ExportConfig; + public List? GamePaths { get; private set; } public int GamePathIndex; @@ -131,7 +134,7 @@ public partial class ModEditWindow } PendingIo = true; - _edit._models.ExportToGltf(Mdl, sklbPaths, ReadFile, outputPath) + _edit._models.ExportToGltf(ExportConfig, Mdl, sklbPaths, ReadFile, outputPath) .ContinueWith(task => { RecordIoExceptions(task.Exception); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 1a200fdf..7304d3dd 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -112,6 +112,14 @@ public partial class ModEditWindow DrawGamePathCombo(tab); + // ImGui.Checkbox("##exportGeneratedMissingBones", ref tab.ExportGenerateMissingBones); + ImGui.Checkbox("##exportGeneratedMissingBones", ref tab.ExportConfig.GenerateMissingBones); + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("Generate missing bones", + "WARNING: Enabling this option can result in unusable exported meshes.\n" + + "It is primarily intended to allow exporting models weighted to bones that do not exist.\n" + + "Before enabling, ensure dependencies are enabled in the current collection, and EST metadata is correctly configured."); + var gamePath = tab.GamePathIndex >= 0 && tab.GamePathIndex < tab.GamePaths.Count ? tab.GamePaths[tab.GamePathIndex] : _customGamePath; From 0d3dde7df39306320a32a9ba31473ca92c01fa09 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 20 Jan 2024 01:09:26 +1100 Subject: [PATCH 7/8] Tweaks --- Penumbra/Import/Models/Export/ModelExporter.cs | 2 +- Penumbra/Import/Models/Import/MeshImporter.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs index b3e9c68d..e0c42d40 100644 --- a/Penumbra/Import/Models/Export/ModelExporter.cs +++ b/Penumbra/Import/Models/Export/ModelExporter.cs @@ -59,7 +59,7 @@ public class ModelExporter { if (rawMaterials.TryGetValue(name, out var rawMaterial)) return MaterialExporter.Export(rawMaterial, name, notifier.WithContext($"Material {name}")); - + notifier.Warning($"Material \"{name}\" missing, using blank fallback."); return MaterialExporter.Unknown; }) diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index 28a7a9c1..b6b146b5 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -216,7 +216,7 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) // Per glTF specification, an asset with a skin MUST contain skinning attributes on its mesh. var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0") - ?? throw notifier.Exception($"Skinned mesh \"{meshName}\" is skinned but does not contain skinning vertex attributes."); + ?? throw notifier.Exception($"Mesh \"{meshName}\" is skinned but does not contain skinning vertex attributes."); // Build a set of joints that are referenced by this mesh. // TODO: Would be neat to omit 0-weighted joints here, but doing so will require some further work on bone mapping behavior to ensure the unweighted joints can still be resolved to valid bone indices during vertex data construction. diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 7304d3dd..43b26f10 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -112,7 +112,6 @@ public partial class ModEditWindow DrawGamePathCombo(tab); - // ImGui.Checkbox("##exportGeneratedMissingBones", ref tab.ExportGenerateMissingBones); ImGui.Checkbox("##exportGeneratedMissingBones", ref tab.ExportConfig.GenerateMissingBones); ImGui.SameLine(); ImGuiUtil.LabeledHelpMarker("Generate missing bones", From 7db95995113a4545f8c0feb3a7d6a1666f2d9e62 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 20 Jan 2024 16:06:33 +0100 Subject: [PATCH 8/8] Auto-format and stuff. --- Penumbra/Import/Models/Export/MeshExporter.cs | 16 +++++--- .../Import/Models/Export/ModelExporter.cs | 4 +- Penumbra/Import/Models/ModelManager.cs | 12 +++--- .../ModEditWindow.Models.MdlTab.cs | 38 +++++++++++-------- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 8 ++-- 5 files changed, 43 insertions(+), 35 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 83a0c3cf..928c8670 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -38,7 +38,9 @@ public class MeshExporter public string[] Attributes; } - public static Mesh Export(ExportConfig config, MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, GltfSkeleton? skeleton, IoNotifier notifier) + public static Mesh Export(in ExportConfig config, MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, + GltfSkeleton? skeleton, + IoNotifier notifier) { var self = new MeshExporter(config, mdl, lod, meshIndex, materials, skeleton, notifier); return new Mesh(self.BuildMeshes(), skeleton); @@ -65,7 +67,8 @@ public class MeshExporter private readonly Type _skinningType; // TODO: This signature is getting out of control. - private MeshExporter(ExportConfig config, MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, GltfSkeleton? skeleton, IoNotifier notifier) + private MeshExporter(in ExportConfig config, MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, + GltfSkeleton? skeleton, IoNotifier notifier) { _config = config; _notifier = notifier; @@ -118,9 +121,10 @@ public class MeshExporter Ensure all dependencies are enabled in the current collection, and EST entries (if required) are configured. If this is a known issue with this model and you would like to export anyway, enable the ""Generate missing bones"" option." ); - + (_, gltfBoneIndex) = skeleton.GenerateBone(boneName); - _notifier.Warning($"Generated missing bone \"{boneName}\". Vertices weighted to this bone will not move with the rest of the armature."); + _notifier.Warning( + $"Generated missing bone \"{boneName}\". Vertices weighted to this bone will not move with the rest of the armature."); } indexMap.Add((ushort)tableIndex, gltfBoneIndex); @@ -144,7 +148,7 @@ public class MeshExporter .Take(XivMesh.SubMeshCount) .WithIndex() .Select(subMesh => BuildMesh($"mesh {_meshIndex}.{subMesh.Index}", indices, vertices, - (int)(subMesh.Value.IndexOffset - XivMesh.StartIndex), (int)subMesh.Value.IndexCount, + (int)(subMesh.Value.IndexOffset - XivMesh.StartIndex), (int)subMesh.Value.IndexCount, subMesh.Value.AttributeIndexMask)) .ToArray(); } @@ -233,7 +237,7 @@ public class MeshExporter return new MeshData { - Mesh = meshBuilder, + Mesh = meshBuilder, Attributes = attributes, }; } diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs index e0c42d40..55997ef8 100644 --- a/Penumbra/Import/Models/Export/ModelExporter.cs +++ b/Penumbra/Import/Models/Export/ModelExporter.cs @@ -23,7 +23,7 @@ public class ModelExporter } /// Export a model in preparation for usage in a glTF file. If provided, skeleton will be used to skin the resulting meshes where appropriate. - public static Model Export(ExportConfig config, MdlFile mdl, IEnumerable xivSkeletons, Dictionary rawMaterials, IoNotifier notifier) + public static Model Export(in ExportConfig config, MdlFile mdl, IEnumerable xivSkeletons, Dictionary rawMaterials, IoNotifier notifier) { var gltfSkeleton = ConvertSkeleton(xivSkeletons); var materials = ConvertMaterials(mdl, rawMaterials, notifier); @@ -32,7 +32,7 @@ public class ModelExporter } /// Convert a .mdl to a mesh (group) per LoD. - private static List ConvertMeshes(ExportConfig config, MdlFile mdl, MaterialBuilder[] materials, GltfSkeleton? skeleton, IoNotifier notifier) + private static List ConvertMeshes(in ExportConfig config, MdlFile mdl, MaterialBuilder[] materials, GltfSkeleton? skeleton, IoNotifier notifier) { var meshes = new List(); diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 5340d556..2c341c8b 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -37,7 +37,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect _tasks.Clear(); } - public Task ExportToGltf(ExportConfig config, MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) + public Task ExportToGltf(in ExportConfig config, MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) => EnqueueWithResult( new ExportToGltfAction(this, config, mdl, sklbPaths, read, outputPath), action => action.Notifier @@ -189,7 +189,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect string outputPath) : IAction { - public IoNotifier Notifier = new IoNotifier(); + public readonly IoNotifier Notifier = new(); public void Execute(CancellationToken cancel) { @@ -224,7 +224,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect // be picked up, if relevant, when the model tries to create mappings // for a bone in the failed sklb. var havokTasks = sklbPaths - .Select(path => read(path)) + .Select(read) .Where(bytes => bytes != null) .Select(bytes => new SklbFile(bytes!)) .WithIndex() @@ -280,7 +280,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect return pngImage ?? throw new Exception("Failed to convert texture to png."); } - private Image CreateDummyImage() + private static Image CreateDummyImage() { var image = new Image(1, 1); image[0, 0] = Color.White; @@ -299,8 +299,8 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect private partial class ImportGltfAction(string inputPath) : IAction { - public MdlFile? Out; - public IoNotifier Notifier = new IoNotifier(); + public MdlFile? Out; + public readonly IoNotifier Notifier = new(); public void Execute(CancellationToken cancel) { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 8b3a7040..f24464d1 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -15,7 +15,7 @@ public partial class ModEditWindow { private readonly ModEditWindow _edit; - public MdlFile Mdl { get; private set; } + public MdlFile Mdl { get; private set; } private List?[] _attributes; public bool ImportKeepMaterials; @@ -43,7 +43,7 @@ public partial class ModEditWindow [MemberNotNull(nameof(Mdl), nameof(_attributes))] private void Initialize(MdlFile mdl) { - Mdl = mdl; + Mdl = mdl; _attributes = CreateAttributes(Mdl); } @@ -95,14 +95,14 @@ public partial class ModEditWindow task.ContinueWith(t => { RecordIoExceptions(t.Exception); - GamePaths = t.Result; - PendingIo = false; + GamePaths = t.Result; + PendingIo = false; }); } private EstManipulation[] GetCurrentEstManipulations() { - var mod = _edit._editor.Mod; + var mod = _edit._editor.Mod; var option = _edit._editor.Option; if (mod == null || option == null) return []; @@ -140,17 +140,17 @@ public partial class ModEditWindow RecordIoExceptions(task.Exception); if (task is { IsCompletedSuccessfully: true, Result: not null }) IoWarnings = task.Result.GetWarnings().ToList(); - PendingIo = false; + PendingIo = false; }); } - - /// Import a model from an interchange format. + + /// Import a model from an interchange format. /// Disk path to load model data from. public void Import(string inputPath) { PendingIo = true; _edit._models.ImportGltf(inputPath) - .ContinueWith((Task<(MdlFile?, IoNotifier)> task) => + .ContinueWith(task => { RecordIoExceptions(task.Exception); if (task is { IsCompletedSuccessfully: true, Result: (not null, _) }) @@ -158,6 +158,7 @@ public partial class ModEditWindow IoWarnings = task.Result.Item2.GetWarnings().ToList(); FinalizeImport(task.Result.Item1); } + PendingIo = false; }); } @@ -178,11 +179,11 @@ public partial class ModEditWindow // TODO: Add flag editing. newMdl.Flags1 = Mdl.Flags1; newMdl.Flags2 = Mdl.Flags2; - + Initialize(newMdl); _dirty = true; } - + /// Merge material configuration from the source onto the target. /// Model that will be updated. /// Model to copy material configuration from. @@ -218,10 +219,12 @@ public partial class ModEditWindow // to maintain semantic connection between mesh index and sub mesh attributes. if (meshIndex >= source.Meshes.Length) continue; + var sourceMesh = source.Meshes[meshIndex]; if (subMeshOffset >= sourceMesh.SubMeshCount) continue; + var sourceSubMesh = source.SubMeshes[sourceMesh.SubMeshIndex + subMeshOffset]; target.SubMeshes[subMeshIndex].AttributeIndexMask = sourceSubMesh.AttributeIndexMask; @@ -237,11 +240,13 @@ public partial class ModEditWindow foreach (var sourceElement in source.ElementIds) { - var sourceBone = source.Bones[sourceElement.ParentBoneName]; + var sourceBone = source.Bones[sourceElement.ParentBoneName]; var targetIndex = target.Bones.IndexOf(sourceBone); // Given that there's no means of authoring these at the moment, this should probably remain a hard error. if (targetIndex == -1) - throw new Exception($"Failed to merge element IDs. Original model contains element IDs targeting bone {sourceBone}, which is not present on the imported model."); + throw new Exception( + $"Failed to merge element IDs. Original model contains element IDs targeting bone {sourceBone}, which is not present on the imported model."); + elementIds.Add(sourceElement with { ParentBoneName = (uint)targetIndex, @@ -253,13 +258,14 @@ public partial class ModEditWindow private void RecordIoExceptions(Exception? exception) { - IoExceptions = exception switch { + IoExceptions = exception switch + { null => [], AggregateException ae => [.. ae.Flatten().InnerExceptions], _ => [exception], }; } - + /// Read a file from the active collection or game. /// Game path to the file to load. // TODO: Also look up files within the current mod regardless of mod state? @@ -297,7 +303,7 @@ public partial class ModEditWindow /// Create a list of attributes per sub mesh. private static List?[] CreateAttributes(MdlFile mdl) - => mdl.SubMeshes.Select(s => + => mdl.SubMeshes.Select(s => { var maxAttribute = 31 - BitOperations.LeadingZeroCount(s.AttributeIndexMask); // TODO: Research what results in this - it seems to primarily be reproducible on bgparts, is it garbage data, or an alternative usage of the value? diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 744e9ea2..561cbed7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -170,8 +170,7 @@ public partial class ModEditWindow using var exceptionNode = ImRaii.TreeNode(message); if (exceptionNode) { - ImGui.Dummy(new Vector2(ImGui.GetStyle().IndentSpacing, 0)); - ImGui.SameLine(); + using var indent = ImRaii.PushIndent(); ImGuiUtil.TextWrapped(exception.ToString()); } } @@ -202,9 +201,8 @@ public partial class ModEditWindow using var warningNode = ImRaii.TreeNode(firstLine); if (warningNode) { - ImGui.Dummy(new Vector2(ImGui.GetStyle().IndentSpacing, 0)); - ImGui.SameLine(); - ImGuiUtil.TextWrapped(warning.ToString()); + using var indent = ImRaii.PushIndent(); + ImGuiUtil.TextWrapped(warning); } } }