Add lenient model export mode

This commit is contained in:
Ridan Vandenbergh 2025-08-02 01:15:49 +02:00
parent 6689e326ee
commit 1166c9e297
No known key found for this signature in database
5 changed files with 42 additions and 6 deletions

View file

@ -3,4 +3,5 @@ namespace Penumbra.Import.Models.Export;
public struct ExportConfig public struct ExportConfig
{ {
public bool GenerateMissingBones; public bool GenerateMissingBones;
public bool LenientMode;
} }

View file

@ -445,7 +445,7 @@ public class MeshExporter
return new VertexTexture2ColorFfxiv( return new VertexTexture2ColorFfxiv(
new Vector2(uv.X, uv.Y), new Vector2(uv.X, uv.Y),
new Vector2(uv.Z, uv.W), new Vector2(uv.Z, uv.W),
ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)) ToVector4(_config.LenientMode ? GetFirstUnsafe(attributes, MdlFile.VertexUsage.Color) : GetFirstSafe(attributes, MdlFile.VertexUsage.Color))
); );
} }
if (_materialType == typeof(VertexTexture3)) if (_materialType == typeof(VertexTexture3))
@ -468,7 +468,7 @@ public class MeshExporter
new Vector2(uv0.X, uv0.Y), new Vector2(uv0.X, uv0.Y),
new Vector2(uv0.Z, uv0.W), new Vector2(uv0.Z, uv0.W),
new Vector2(uv1.X, uv1.Y), new Vector2(uv1.X, uv1.Y),
ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)) ToVector4(_config.LenientMode ? GetFirstUnsafe(attributes, MdlFile.VertexUsage.Color) : GetFirstSafe(attributes, MdlFile.VertexUsage.Color))
); );
} }
@ -537,6 +537,23 @@ public class MeshExporter
return list[0]; return list[0];
} }
/// <summary> Check that the list has length 1 for any case where this is expected and return the one entry. Otherwise, report a warning. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private T GetFirstUnsafe<T>(IReadOnlyDictionary<MdlFile.VertexUsage, List<T>> attributes, MdlFile.VertexUsage usage)
{
var list = attributes[usage];
switch (list.Count)
{
case > 1:
_notifier.Warning($"Multiple usage indices encountered for {usage}.", true);
break;
case 0:
throw _notifier.Exception($"No usage indices encountered for {usage}");
}
return list[0];
}
/// <summary> Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. </summary> /// <summary> Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. </summary>
private static Vector2 ToVector2(object data) private static Vector2 ToVector2(object data)

View file

@ -12,8 +12,8 @@ public record class IoNotifier
=> this with { _context = $"{_context}{context}: "}; => this with { _context = $"{_context}{context}: "};
/// <summary> Send a warning with any current context to notification channels. </summary> /// <summary> Send a warning with any current context to notification channels. </summary>
public void Warning(string content) public void Warning(string content, bool ignoreDuplicates = false)
=> SendMessage(content, Logger.LogLevel.Warning); => SendMessage(content, Logger.LogLevel.Warning, ignoreDuplicates);
/// <summary> Get the current warnings for this notifier. </summary> /// <summary> Get the current warnings for this notifier. </summary>
/// <remarks> This does not currently filter to notifications with the current notifier's context - it will return all IO notifications from all notifiers. </remarks> /// <remarks> This does not currently filter to notifications with the current notifier's context - it will return all IO notifications from all notifiers. </remarks>
@ -31,9 +31,15 @@ public record class IoNotifier
where TException : Exception, new() where TException : Exception, new()
=> (TException)Activator.CreateInstance(typeof(TException), $"{_context}{message}")!; => (TException)Activator.CreateInstance(typeof(TException), $"{_context}{message}")!;
private void SendMessage(string message, Logger.LogLevel type) private void SendMessage(string message, Logger.LogLevel type, bool ignoreDuplicates = false)
{ {
var fullText = $"{_context}{message}"; var fullText = $"{_context}{message}";
if (ignoreDuplicates && _messages.Contains(fullText))
{
return;
}
Penumbra.Log.Message(type, fullText); Penumbra.Log.Message(type, fullText);
_messages.Add(fullText); _messages.Add(fullText);
} }

View file

@ -16,6 +16,7 @@ using Penumbra.Meta;
using Penumbra.Meta.Files; using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using SharpGLTF.Scenes; using SharpGLTF.Scenes;
using SharpGLTF.Validation;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -216,7 +217,10 @@ public sealed class ModelManager(IFramework framework, MetaFileManager metaFileM
Penumbra.Log.Debug("[GLTF Export] Saving..."); Penumbra.Log.Debug("[GLTF Export] Saving...");
var gltfModel = scene.ToGltf2(); var gltfModel = scene.ToGltf2();
gltfModel.Save(outputPath); gltfModel.Save(outputPath, new Schema2.WriteSettings()
{
Validation = config.LenientMode ? ValidationMode.TryFix : ValidationMode.Strict,
});
Penumbra.Log.Debug("[GLTF Export] Done."); Penumbra.Log.Debug("[GLTF Export] Done.");
} }

View file

@ -168,6 +168,14 @@ public partial class ModEditWindow
+ "It is primarily intended to allow exporting models weighted to bones that do not exist.\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."); + "Before enabling, ensure dependencies are enabled in the current collection, and EST metadata is correctly configured.");
ImGui.SameLine(200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X);
ImGui.Checkbox("##tryFixValidation", ref tab.ExportConfig.LenientMode);
ImGui.SameLine();
ImGuiUtil.LabeledHelpMarker("Lenient mode",
"Try fixing potential errors during model validation and ignore superfluous color information.\n"
+ "This may result in a broken model, or one missing information, so use with care!");
var gamePath = tab.GamePathIndex >= 0 && tab.GamePathIndex < tab.GamePaths.Count var gamePath = tab.GamePathIndex >= 0 && tab.GamePathIndex < tab.GamePaths.Count
? tab.GamePaths[tab.GamePathIndex] ? tab.GamePaths[tab.GamePathIndex]
: _customGamePath; : _customGamePath;