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; }); }