From c6642c4fa30eaee0f10dc1cbd208101d76b33630 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 13 Jan 2024 11:32:26 +1100 Subject: [PATCH 01/18] Spike material export workflow --- .../Import/Models/Export/MaterialExporter.cs | 81 +++++++++++++++++++ .../Import/Models/Export/ModelExporter.cs | 18 ++--- Penumbra/Import/Models/ModelManager.cs | 65 ++++++++++++++- 3 files changed, 149 insertions(+), 15 deletions(-) create mode 100644 Penumbra/Import/Models/Export/MaterialExporter.cs diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs new file mode 100644 index 00000000..ef417e35 --- /dev/null +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -0,0 +1,81 @@ +using Lumina.Data.Parsing; +using Penumbra.GameData.Files; +using SharpGLTF.Materials; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.PixelFormats; + +namespace Penumbra.Import.Models.Export; + +public class MaterialExporter +{ + // input stuff + public struct Material + { + public MtrlFile Mtrl; + public Sampler[] Samplers; + // variant? + } + + public struct Sampler + { + public TextureUsage Usage; + public Image Texture; + } + + public static MaterialBuilder Export(Material material, string name) + { + return material.Mtrl.ShaderPackage.Name switch + { + "character.shpk" => BuildCharacter(material, name), + _ => BuildFallback(material, name), + }; + } + + private static MaterialBuilder BuildCharacter(Material material, string name) + { + // TODO: pixelbashing time + var sampler = material.Samplers + .Where(s => s.Usage == TextureUsage.SamplerNormal) + .First(); + + // TODO: clean up this name generation a bunch. probably a method. + var imageName = name.Replace("/", ""); + var baseColor = BuildImage(sampler.Texture, $"{imageName}_basecolor"); + + return BuildSharedBase(material, name) + .WithBaseColor(baseColor); + } + + private static MaterialBuilder BuildFallback(Material material, string name) + { + Penumbra.Log.Warning($"Unhandled shader package: {material.Mtrl.ShaderPackage.Name}"); + return BuildSharedBase(material, name) + .WithMetallicRoughnessShader() + .WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, Vector4.One); + } + + private static MaterialBuilder BuildSharedBase(Material material, string name) + { + // TODO: Move this and potentially the other known stuff into MtrlFile? + const uint backfaceMask = 0x1; + var showBackfaces = (material.Mtrl.ShaderPackage.Flags & backfaceMask) == 0; + + return new MaterialBuilder(name) + .WithDoubleSide(showBackfaces); + } + + private static ImageBuilder BuildImage(Image image, string name) + { + byte[] textureBytes; + using (var memoryStream = new MemoryStream()) + { + image.Save(memoryStream, PngFormat.Instance); + textureBytes = memoryStream.ToArray(); + } + + var imageBuilder = ImageBuilder.From(textureBytes, name); + imageBuilder.AlternateWriteFileName = $"{name}.*"; + return imageBuilder; + } +} diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs index 6a25af61..da24fbb0 100644 --- a/Penumbra/Import/Models/Export/ModelExporter.cs +++ b/Penumbra/Import/Models/Export/ModelExporter.cs @@ -23,10 +23,10 @@ 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) + public static Model Export(MdlFile mdl, IEnumerable? xivSkeleton, Dictionary rawMaterials) { var gltfSkeleton = xivSkeleton != null ? ConvertSkeleton(xivSkeleton) : null; - var materials = ConvertMaterials(mdl); + var materials = ConvertMaterials(mdl, rawMaterials); var meshes = ConvertMeshes(mdl, materials, gltfSkeleton); return new Model(meshes, gltfSkeleton); } @@ -51,16 +51,12 @@ public class ModelExporter return meshes; } - // TODO: Compose textures for use with these materials - /// Build placeholder materials for each of the material slots in the .mdl. - private static MaterialBuilder[] ConvertMaterials(MdlFile mdl) + /// Build materials for each of the material slots in the .mdl. + private static MaterialBuilder[] ConvertMaterials(MdlFile mdl, Dictionary rawMaterials) => mdl.Materials - .Select(name => - new MaterialBuilder(name) - .WithMetallicRoughnessShader() - .WithDoubleSide(true) - .WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, Vector4.One) - ) + // 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)) .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 f099a0e0..ccf56fe8 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -1,4 +1,5 @@ using Dalamud.Plugin.Services; +using Lumina.Data.Parsing; using OtterGui; using OtterGui.Tasks; using Penumbra.Collections.Manager; @@ -9,15 +10,22 @@ using Penumbra.GameData.Files; using Penumbra.GameData.Structs; using Penumbra.Import.Models.Export; using Penumbra.Import.Models.Import; +using Penumbra.Import.Textures; using Penumbra.Meta.Manipulations; using SharpGLTF.Scenes; -using SharpGLTF.Schema2; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; namespace Penumbra.Import.Models; -public sealed class ModelManager(IFramework framework, ActiveCollections collections, GamePathParser parser) : SingleTaskQueue, IDisposable +using Schema2 = SharpGLTF.Schema2; +using LuminaMaterial = Lumina.Models.Materials.Material; + +public sealed class ModelManager(IFramework framework, ActiveCollections collections, IDataManager gameData, GamePathParser parser, TextureManager textureManager) : SingleTaskQueue, IDisposable { private readonly IFramework _framework = framework; + private readonly IDataManager _gameData = gameData; + private readonly TextureManager _textureManager = textureManager; private readonly ConcurrentDictionary _tasks = new(); @@ -132,11 +140,18 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect public void Execute(CancellationToken cancel) { Penumbra.Log.Debug($"[GLTF Export] Exporting model to {outputPath}..."); + Penumbra.Log.Debug("[GLTF Export] Reading skeletons..."); var xivSkeletons = BuildSkeletons(cancel); + Penumbra.Log.Debug("[GLTF Export] Reading materials..."); + var materials = mdl.Materials.ToDictionary( + path => path, + path => BuildMaterial(path, cancel) + ); + Penumbra.Log.Debug("[GLTF Export] Converting model..."); - var model = ModelExporter.Export(mdl, xivSkeletons); + var model = ModelExporter.Export(mdl, xivSkeletons, materials); Penumbra.Log.Debug("[GLTF Export] Building scene..."); var scene = new SceneBuilder(); @@ -169,6 +184,48 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect delayTicks: pair.Index, cancellationToken: cancel); } + private MaterialExporter.Material BuildMaterial(string relativePath, CancellationToken cancel) + { + // TODO: this should probably be chosen in the export settings + var variantId = 1; + + var absolutePath = relativePath.StartsWith("/") + ? LuminaMaterial.ResolveRelativeMaterialPath(relativePath, variantId) + : relativePath; + + // TODO: this should be a recoverable warning - as should the one below it i think + if (absolutePath == null) + throw new Exception("Failed to resolve material path."); + + // TODO: collection lookup and such. this is currently in mdltab (readsklb), and should be wholesale moved in here. + var data = manager._gameData.GetFile(absolutePath); + if (data == null) + throw new Exception("Failed to fetch material game data."); + + var mtrl = new MtrlFile(data.Data); + + return new MaterialExporter.Material + { + Mtrl = mtrl, + Samplers = mtrl.ShaderPackage.Samplers + .Select(sampler => new MaterialExporter.Sampler + { + Usage = (TextureUsage)sampler.SamplerId, + Texture = ConvertImage(mtrl.Textures[sampler.TextureIndex], cancel), + }) + .ToArray(), + }; + } + + private Image ConvertImage(MtrlFile.Texture texture, CancellationToken cancel) + { + var (image, _) = manager._textureManager.Load(texture.Path); + var pngImage = TextureManager.ConvertToPng(image, cancel).AsPng; + if (pngImage == null) + throw new Exception("Failed to convert texture to png."); + return pngImage; + } + public bool Equals(IAction? other) { if (other is not ExportToGltfAction rhs) @@ -185,7 +242,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect public void Execute(CancellationToken cancel) { - var model = ModelRoot.Load(inputPath); + var model = Schema2.ModelRoot.Load(inputPath); Out = ModelImporter.Import(model); } From c8e58c08a064a948b3b3bc333caed3df20b55c4a Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 13 Jan 2024 16:11:51 +1100 Subject: [PATCH 02/18] Compose character diffuse --- .../Import/Models/Export/MaterialExporter.cs | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index ef417e35..4d085f18 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -34,19 +34,51 @@ public class MaterialExporter private static MaterialBuilder BuildCharacter(Material material, string name) { - // TODO: pixelbashing time - var sampler = material.Samplers + var table = material.Mtrl.Table; + var normal = material.Samplers .Where(s => s.Usage == TextureUsage.SamplerNormal) - .First(); + .First() + .Texture; + + var baseColorTarget = new Image(normal.Width, normal.Height); + normal.ProcessPixelRows(baseColorTarget, (sourceAccessor, targetAccessor) => + { + for (int y = 0; y < sourceAccessor.Height; y++) + { + var sourceRow = sourceAccessor.GetRowSpan(y); + var targetRow = targetAccessor.GetRowSpan(y); + + for (int x = 0; x < sourceRow.Length; x++) + { + var (smoothed, stepped) = GetTableRowIndices(sourceRow[x].A / 255f); + var prevRow = table[(int)MathF.Floor(smoothed)]; + var nextRow = table[(int)MathF.Ceiling(smoothed)]; + var lerpedDiffuse = Vector3.Lerp(prevRow.Diffuse, nextRow.Diffuse, smoothed % 1); + targetRow[x].FromVector4(new Vector4(lerpedDiffuse, 1)); + } + } + }); // TODO: clean up this name generation a bunch. probably a method. var imageName = name.Replace("/", ""); - var baseColor = BuildImage(sampler.Texture, $"{imageName}_basecolor"); + var baseColor = BuildImage(baseColorTarget, $"{imageName}_basecolor"); return BuildSharedBase(material, name) .WithBaseColor(baseColor); } + private static (float Smooth, float Stepped) GetTableRowIndices(float input) + { + // These calculations are ported from character.shpk. + var smoothed = MathF.Floor(((input * 7.5f) % 1.0f) * 2) + * (-input * 15 + MathF.Floor(input * 15 + 0.5f)) + + input * 15; + + var stepped = MathF.Floor(smoothed + 0.5f); + + return (smoothed, stepped); + } + private static MaterialBuilder BuildFallback(Material material, string name) { Penumbra.Log.Warning($"Unhandled shader package: {material.Mtrl.ShaderPackage.Name}"); From 509b4c8866bd0ed37a994d9237059b6be0bb3ec2 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 13 Jan 2024 17:05:57 +1100 Subject: [PATCH 03/18] Wire up normals and opacity --- .../Import/Models/Export/MaterialExporter.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 4d085f18..d16e9367 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -50,11 +50,22 @@ public class MaterialExporter for (int x = 0; x < sourceRow.Length; x++) { + ref var sourcePixel = ref sourceRow[x]; + ref var targetPixel = ref targetRow[x]; + var (smoothed, stepped) = GetTableRowIndices(sourceRow[x].A / 255f); var prevRow = table[(int)MathF.Floor(smoothed)]; var nextRow = table[(int)MathF.Ceiling(smoothed)]; + + // Base colour (table[.a], .b) var lerpedDiffuse = Vector3.Lerp(prevRow.Diffuse, nextRow.Diffuse, smoothed % 1); - targetRow[x].FromVector4(new Vector4(lerpedDiffuse, 1)); + targetPixel.FromVector4(new Vector4(lerpedDiffuse, 1)); + targetPixel.A = sourcePixel.B; + + // Normal (.rg) + // TODO: we don't actually need alpha at all for normal, but _not_ using the existing rgba texture means I'll need a new one, with a new accessor. Think about it. + sourcePixel.B = byte.MaxValue; + sourcePixel.A = byte.MaxValue; } } }); @@ -62,9 +73,13 @@ public class MaterialExporter // TODO: clean up this name generation a bunch. probably a method. var imageName = name.Replace("/", ""); var baseColor = BuildImage(baseColorTarget, $"{imageName}_basecolor"); + var normalThing = BuildImage(normal, $"{imageName}_normal"); return BuildSharedBase(material, name) - .WithBaseColor(baseColor); + // NOTE: this isn't particularly precise to game behavior, but good enough for now. + .WithAlpha(AlphaMode.MASK, 0.5f) + .WithBaseColor(baseColor) + .WithNormal(normalThing); } private static (float Smooth, float Stepped) GetTableRowIndices(float input) From 96f40b7ddcb25f19bcd9fe306b473f7c5a9d5e3f Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 13 Jan 2024 20:33:45 +1100 Subject: [PATCH 04/18] Expand to more general-purpose transform codepath --- .../Import/Models/Export/MaterialExporter.cs | 99 ++++++++++++------- 1 file changed, 66 insertions(+), 33 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index d16e9367..c08d954c 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -2,11 +2,15 @@ using Lumina.Data.Parsing; using Penumbra.GameData.Files; using SharpGLTF.Materials; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace Penumbra.Import.Models.Export; +using ImageSharpConfiguration = SixLabors.ImageSharp.Configuration; + public class MaterialExporter { // input stuff @@ -34,52 +38,81 @@ public class MaterialExporter private static MaterialBuilder BuildCharacter(Material material, string name) { + // TODO: handle models with an underlying diffuse var table = material.Mtrl.Table; + // TODO: this should probably be a dict var normal = material.Samplers .Where(s => s.Usage == TextureUsage.SamplerNormal) .First() .Texture; - var baseColorTarget = new Image(normal.Width, normal.Height); - normal.ProcessPixelRows(baseColorTarget, (sourceAccessor, targetAccessor) => + var operation = new CharacterOperation() { - for (int y = 0; y < sourceAccessor.Height; y++) - { - var sourceRow = sourceAccessor.GetRowSpan(y); - var targetRow = targetAccessor.GetRowSpan(y); - - for (int x = 0; x < sourceRow.Length; x++) - { - ref var sourcePixel = ref sourceRow[x]; - ref var targetPixel = ref targetRow[x]; - - var (smoothed, stepped) = GetTableRowIndices(sourceRow[x].A / 255f); - var prevRow = table[(int)MathF.Floor(smoothed)]; - var nextRow = table[(int)MathF.Ceiling(smoothed)]; - - // Base colour (table[.a], .b) - var lerpedDiffuse = Vector3.Lerp(prevRow.Diffuse, nextRow.Diffuse, smoothed % 1); - targetPixel.FromVector4(new Vector4(lerpedDiffuse, 1)); - targetPixel.A = sourcePixel.B; - - // Normal (.rg) - // TODO: we don't actually need alpha at all for normal, but _not_ using the existing rgba texture means I'll need a new one, with a new accessor. Think about it. - sourcePixel.B = byte.MaxValue; - sourcePixel.A = byte.MaxValue; - } - } - }); + Table = table, + Normal = normal, + BaseColor = new Image(normal.Width, normal.Height), + Emissive = new Image(normal.Width, normal.Height), + }; + ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds(), in operation); // TODO: clean up this name generation a bunch. probably a method. var imageName = name.Replace("/", ""); - var baseColor = BuildImage(baseColorTarget, $"{imageName}_basecolor"); - var normalThing = BuildImage(normal, $"{imageName}_normal"); return BuildSharedBase(material, name) + // .WithSpecularGlossinessShader() + // .WithDiffuse() // NOTE: this isn't particularly precise to game behavior, but good enough for now. .WithAlpha(AlphaMode.MASK, 0.5f) - .WithBaseColor(baseColor) - .WithNormal(normalThing); + .WithBaseColor(BuildImage(operation.BaseColor, $"{imageName}_basecolor")) + .WithNormal(BuildImage(operation.Normal, $"{imageName}_normal")) + .WithEmissive(BuildImage(operation.Emissive, $"{imageName}_emissive"), Vector3.One, 1); + } + + private readonly struct CharacterOperation : IRowOperation + { + public required MtrlFile.ColorTable Table { get; init; } + + public required Image Normal { get; init; } + public required Image BaseColor { get; init; } + public required Image Emissive { get; init; } + + private Buffer2D NormalBuffer => Normal.Frames.RootFrame.PixelBuffer; + private Buffer2D BaseColorBuffer => BaseColor.Frames.RootFrame.PixelBuffer; + private Buffer2D EmissiveBuffer => Emissive.Frames.RootFrame.PixelBuffer; + + public void Invoke(int y) + { + var normalSpan = NormalBuffer.DangerousGetRowSpan(y); + var baseColorSpan = BaseColorBuffer.DangerousGetRowSpan(y); + var emissiveSpan = EmissiveBuffer.DangerousGetRowSpan(y); + + for (int x = 0; x < normalSpan.Length; x++) + { + ref var normalPixel = ref normalSpan[x]; + ref var baseColorPixel = ref baseColorSpan[x]; + ref var emissivePixel = ref emissiveSpan[x]; + + // Table row data (.a) + var (smoothed, stepped) = GetTableRowIndices(normalPixel.A / 255f); + var weight = smoothed % 1; + var prevRow = Table[(int)MathF.Floor(smoothed)]; + var nextRow = Table[(int)MathF.Ceiling(smoothed)]; + + // Base colour (table, .b) + var lerpedDiffuse = Vector3.Lerp(prevRow.Diffuse, nextRow.Diffuse, weight); + baseColorPixel.FromVector4(new Vector4(lerpedDiffuse, 1)); + baseColorPixel.A = normalPixel.B; + + // Emissive (table) + var lerpedEmissive = Vector3.Lerp(prevRow.Emissive, nextRow.Emissive, weight); + emissivePixel.FromVector4(new Vector4(lerpedEmissive, 1)); + + // Normal (.rg) + // TODO: we don't actually need alpha at all for normal, but _not_ using the existing rgba texture means I'll need a new one, with a new accessor. Think about it. + normalPixel.B = byte.MaxValue; + normalPixel.A = byte.MaxValue; + } + } } private static (float Smooth, float Stepped) GetTableRowIndices(float input) @@ -112,7 +145,7 @@ public class MaterialExporter .WithDoubleSide(showBackfaces); } - private static ImageBuilder BuildImage(Image image, string name) + private static ImageBuilder BuildImage(Image image, string name) { byte[] textureBytes; using (var memoryStream = new MemoryStream()) From 74ffc56d6c48d10bd91fee053ded67baa8647418 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 13 Jan 2024 20:48:15 +1100 Subject: [PATCH 05/18] Fix errors with same name expanding together --- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index c92e2926..4ac789ad 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -138,14 +138,14 @@ public partial class ModEditWindow using var frame = ImRaii.FramedGroup("Exceptions", size, headerPreIcon: FontAwesomeIcon.TimesCircle, borderColor: Colors.RegexWarningBorder); var spaceAvail = ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X - 100; - foreach (var exception in tab.IoExceptions) + foreach (var (exception, index) in tab.IoExceptions.WithIndex()) { var message = $"{exception.GetType().Name}: {exception.Message}"; var textSize = ImGui.CalcTextSize(message).X; if (textSize > spaceAvail) message = message.Substring(0, (int)Math.Floor(message.Length * (spaceAvail / textSize))) + "..."; - using (var exceptionNode = ImRaii.TreeNode(message)) + using (var exceptionNode = ImRaii.TreeNode($"{message}###exception{index}")) { if (exceptionNode) ImGuiUtil.TextWrapped(exception.ToString()); From 4572cb83f0f1b311510536d5dd1622e98d0d0517 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 13 Jan 2024 20:48:46 +1100 Subject: [PATCH 06/18] Move table calcs into struct --- .../Import/Models/Export/MaterialExporter.cs | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index c08d954c..437b57f7 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -93,18 +93,17 @@ public class MaterialExporter ref var emissivePixel = ref emissiveSpan[x]; // Table row data (.a) - var (smoothed, stepped) = GetTableRowIndices(normalPixel.A / 255f); - var weight = smoothed % 1; - var prevRow = Table[(int)MathF.Floor(smoothed)]; - var nextRow = Table[(int)MathF.Ceiling(smoothed)]; + var tableRow = GetTableRowIndices(normalPixel.A / 255f); + var prevRow = Table[tableRow.Previous]; + var nextRow = Table[tableRow.Next]; // Base colour (table, .b) - var lerpedDiffuse = Vector3.Lerp(prevRow.Diffuse, nextRow.Diffuse, weight); + var lerpedDiffuse = Vector3.Lerp(prevRow.Diffuse, nextRow.Diffuse, tableRow.Weight); baseColorPixel.FromVector4(new Vector4(lerpedDiffuse, 1)); baseColorPixel.A = normalPixel.B; // Emissive (table) - var lerpedEmissive = Vector3.Lerp(prevRow.Emissive, nextRow.Emissive, weight); + var lerpedEmissive = Vector3.Lerp(prevRow.Emissive, nextRow.Emissive, tableRow.Weight); emissivePixel.FromVector4(new Vector4(lerpedEmissive, 1)); // Normal (.rg) @@ -115,7 +114,7 @@ public class MaterialExporter } } - private static (float Smooth, float Stepped) GetTableRowIndices(float input) + private static TableRow GetTableRowIndices(float input) { // These calculations are ported from character.shpk. var smoothed = MathF.Floor(((input * 7.5f) % 1.0f) * 2) @@ -124,7 +123,21 @@ public class MaterialExporter var stepped = MathF.Floor(smoothed + 0.5f); - return (smoothed, stepped); + return new TableRow + { + Stepped = (int)stepped, + Previous = (int)MathF.Floor(smoothed), + Next = (int)MathF.Ceiling(smoothed), + Weight = smoothed % 1, + }; + } + + private ref struct TableRow + { + public int Stepped; + public int Previous; + public int Next; + public float Weight; } private static MaterialBuilder BuildFallback(Material material, string name) From db2081f14d944283f890244f50f1acb4b84ff3da Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 13 Jan 2024 21:36:42 +1100 Subject: [PATCH 07/18] More refactors of operation, extract specular color --- .../Import/Models/Export/MaterialExporter.cs | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 437b57f7..de2f1425 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -46,65 +46,63 @@ public class MaterialExporter .First() .Texture; - var operation = new CharacterOperation() - { - Table = table, - Normal = normal, - BaseColor = new Image(normal.Width, normal.Height), - Emissive = new Image(normal.Width, normal.Height), - }; + var operation = new ProcessCharacterNormalOperation(normal, table); ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds(), in operation); // TODO: clean up this name generation a bunch. probably a method. - var imageName = name.Replace("/", ""); + var imageName = name.Replace("/", "").Replace(".mtrl", ""); return BuildSharedBase(material, name) - // .WithSpecularGlossinessShader() - // .WithDiffuse() // NOTE: this isn't particularly precise to game behavior, but good enough for now. .WithAlpha(AlphaMode.MASK, 0.5f) .WithBaseColor(BuildImage(operation.BaseColor, $"{imageName}_basecolor")) .WithNormal(BuildImage(operation.Normal, $"{imageName}_normal")) + .WithSpecularColor(BuildImage(operation.Specular, $"{imageName}_specular")) .WithEmissive(BuildImage(operation.Emissive, $"{imageName}_emissive"), Vector3.One, 1); } - private readonly struct CharacterOperation : IRowOperation + // TODO: It feels a little silly to request the entire normal here when extrating the normal only needs some of the components. + // As a future refactor, it would be neat to accept a single-channel field here, and then do composition of other stuff later. + private readonly struct ProcessCharacterNormalOperation(Image normal, MtrlFile.ColorTable table) : IRowOperation { - public required MtrlFile.ColorTable Table { get; init; } - - public required Image Normal { get; init; } - public required Image BaseColor { get; init; } - public required Image Emissive { get; init; } + public Image Normal { get; private init; } = normal.Clone(); + public Image BaseColor { get; private init; } = new Image(normal.Width, normal.Height); + public Image Specular { get; private init; } = new Image(normal.Width, normal.Height); + public Image Emissive { get; private init; } = new Image(normal.Width, normal.Height); private Buffer2D NormalBuffer => Normal.Frames.RootFrame.PixelBuffer; private Buffer2D BaseColorBuffer => BaseColor.Frames.RootFrame.PixelBuffer; + private Buffer2D SpecularBuffer => Specular.Frames.RootFrame.PixelBuffer; private Buffer2D EmissiveBuffer => Emissive.Frames.RootFrame.PixelBuffer; public void Invoke(int y) { var normalSpan = NormalBuffer.DangerousGetRowSpan(y); var baseColorSpan = BaseColorBuffer.DangerousGetRowSpan(y); + var specularSpan = SpecularBuffer.DangerousGetRowSpan(y); var emissiveSpan = EmissiveBuffer.DangerousGetRowSpan(y); for (int x = 0; x < normalSpan.Length; x++) { ref var normalPixel = ref normalSpan[x]; - ref var baseColorPixel = ref baseColorSpan[x]; - ref var emissivePixel = ref emissiveSpan[x]; // Table row data (.a) var tableRow = GetTableRowIndices(normalPixel.A / 255f); - var prevRow = Table[tableRow.Previous]; - var nextRow = Table[tableRow.Next]; + var prevRow = table[tableRow.Previous]; + var nextRow = table[tableRow.Next]; // Base colour (table, .b) var lerpedDiffuse = Vector3.Lerp(prevRow.Diffuse, nextRow.Diffuse, tableRow.Weight); - baseColorPixel.FromVector4(new Vector4(lerpedDiffuse, 1)); - baseColorPixel.A = normalPixel.B; + baseColorSpan[x].FromVector4(new Vector4(lerpedDiffuse, 1)); + baseColorSpan[x].A = normalPixel.B; + + // Specular (table) + var lerpedSpecularColor = Vector3.Lerp(prevRow.Specular, nextRow.Specular, tableRow.Weight); + specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, 1)); // Emissive (table) var lerpedEmissive = Vector3.Lerp(prevRow.Emissive, nextRow.Emissive, tableRow.Weight); - emissivePixel.FromVector4(new Vector4(lerpedEmissive, 1)); + emissiveSpan[x].FromVector4(new Vector4(lerpedEmissive, 1)); // Normal (.rg) // TODO: we don't actually need alpha at all for normal, but _not_ using the existing rgba texture means I'll need a new one, with a new accessor. Think about it. From e8fd452b8f12d23a0e30a6c684919edaac1f32fe Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 13 Jan 2024 22:16:47 +1100 Subject: [PATCH 08/18] Improve file reading --- .../Import/Models/Export/MaterialExporter.cs | 19 ++++-------- Penumbra/Import/Models/ModelManager.cs | 30 +++++++----------- .../ModEditWindow.Models.MdlTab.cs | 31 ++++++++++--------- 3 files changed, 34 insertions(+), 46 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index de2f1425..dee386df 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -13,22 +13,16 @@ using ImageSharpConfiguration = SixLabors.ImageSharp.Configuration; public class MaterialExporter { - // input stuff public struct Material { public MtrlFile Mtrl; - public Sampler[] Samplers; + public Dictionary> Textures; // variant? } - public struct Sampler - { - public TextureUsage Usage; - public Image Texture; - } - public static MaterialBuilder Export(Material material, string name) { + Penumbra.Log.Debug($"Exporting material \"{name}\"."); return material.Mtrl.ShaderPackage.Name switch { "character.shpk" => BuildCharacter(material, name), @@ -40,11 +34,10 @@ public class MaterialExporter { // TODO: handle models with an underlying diffuse var table = material.Mtrl.Table; - // TODO: this should probably be a dict - var normal = material.Samplers - .Where(s => s.Usage == TextureUsage.SamplerNormal) - .First() - .Texture; + + // TODO: there's a few normal usages i should check, i think. + // TODO: tryget + var normal = material.Textures[TextureUsage.SamplerNormal]; var operation = new ProcessCharacterNormalOperation(normal, table); ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds(), in operation); diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index ccf56fe8..4f652436 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -39,8 +39,8 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect _tasks.Clear(); } - public Task ExportToGltf(MdlFile mdl, IEnumerable sklbs, string outputPath) - => Enqueue(new ExportToGltfAction(this, mdl, sklbs, outputPath)); + public Task ExportToGltf(MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) + => Enqueue(new ExportToGltfAction(this, mdl, sklbPaths, read, outputPath)); public Task ImportGltf(string inputPath) { @@ -134,7 +134,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect return task; } - private class ExportToGltfAction(ModelManager manager, MdlFile mdl, IEnumerable sklbs, string outputPath) + private class ExportToGltfAction(ModelManager manager, MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) : IAction { public void Execute(CancellationToken cancel) @@ -166,7 +166,8 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect /// Attempt to read out the pertinent information from a .sklb. private IEnumerable BuildSkeletons(CancellationToken cancel) { - var havokTasks = sklbs + var havokTasks = sklbPaths + .Select(path => new SklbFile(read(path))) .WithIndex() .Select(CreateHavokTask) .ToArray(); @@ -197,29 +198,22 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect if (absolutePath == null) throw new Exception("Failed to resolve material path."); - // TODO: collection lookup and such. this is currently in mdltab (readsklb), and should be wholesale moved in here. - var data = manager._gameData.GetFile(absolutePath); - if (data == null) - throw new Exception("Failed to fetch material game data."); - - var mtrl = new MtrlFile(data.Data); + var mtrl = new MtrlFile(read(absolutePath)); return new MaterialExporter.Material { Mtrl = mtrl, - Samplers = mtrl.ShaderPackage.Samplers - .Select(sampler => new MaterialExporter.Sampler - { - Usage = (TextureUsage)sampler.SamplerId, - Texture = ConvertImage(mtrl.Textures[sampler.TextureIndex], cancel), - }) - .ToArray(), + Textures = mtrl.ShaderPackage.Samplers.ToDictionary( + sampler => (TextureUsage)sampler.SamplerId, + sampler => ConvertImage(mtrl.Textures[sampler.TextureIndex], cancel) + ), }; } private Image ConvertImage(MtrlFile.Texture texture, CancellationToken cancel) { - var (image, _) = manager._textureManager.Load(texture.Path); + using var textureData = new MemoryStream(read(texture.Path)); + var image = TexFileParser.Parse(textureData); var pngImage = TextureManager.ConvertToPng(image, cancel).AsPng; if (pngImage == null) throw new Exception("Failed to convert texture to png."); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index cdaf399f..f79d161e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -116,11 +116,10 @@ public partial class ModEditWindow /// .mdl game path to resolve satellite files such as skeletons relative to. public void Export(string outputPath, Utf8GamePath mdlPath) { - IEnumerable skeletons; + IEnumerable sklbPaths; try { - var sklbPaths = _edit._models.ResolveSklbsForMdl(mdlPath.ToString(), GetCurrentEstManipulations()); - skeletons = sklbPaths.Select(ReadSklb).ToArray(); + sklbPaths = _edit._models.ResolveSklbsForMdl(mdlPath.ToString(), GetCurrentEstManipulations()); } catch (Exception exception) { @@ -129,7 +128,7 @@ public partial class ModEditWindow } PendingIo = true; - _edit._models.ExportToGltf(Mdl, skeletons, outputPath) + _edit._models.ExportToGltf(Mdl, sklbPaths, ReadFile, outputPath) .ContinueWith(task => { RecordIoExceptions(task.Exception); @@ -219,22 +218,24 @@ public partial class ModEditWindow Exception other => [other], }; } - - /// Read a .sklb from the active collection or game. - /// Game path to the .sklb to load. - private SklbFile ReadSklb(string sklbPath) + + /// 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) { // TODO: if cross-collection lookups are turned off, this conversion can be skipped - if (!Utf8GamePath.FromString(sklbPath, out var utf8SklbPath, true)) - throw new Exception($"Resolved skeleton path {sklbPath} could not be converted to a game path."); + if (!Utf8GamePath.FromString(path, out var utf8SklbPath, true)) + throw new Exception($"Resolved path {path} could not be converted to a game path."); var resolvedPath = _edit._activeCollections.Current.ResolvePath(utf8SklbPath); // TODO: is it worth trying to use streams for these instead? I'll need to do this for mtrl/tex too, so might be a good idea. that said, the mtrl reader doesn't accept streams, so... - var bytes = resolvedPath == null ? _edit._gameData.GetFile(sklbPath)?.Data : File.ReadAllBytes(resolvedPath.Value.ToPath()); - return bytes != null - ? new SklbFile(bytes) - : throw new Exception( - $"Resolved skeleton path {sklbPath} could not be found. If modded, is it enabled in the current collection?"); + var bytes = resolvedPath == null + ? _edit._gameData.GetFile(path)?.Data + : File.ReadAllBytes(resolvedPath.Value.ToPath()); + + 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 2fa72727622fb872d2c65f761892540d060a389b Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 13 Jan 2024 23:52:26 +1100 Subject: [PATCH 09/18] Add support for explicit diffuse + specular textures --- .../Import/Models/Export/MaterialExporter.cs | 55 +++++++++++++++++-- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index dee386df..3d274ff9 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; namespace Penumbra.Import.Models.Export; @@ -32,25 +33,37 @@ public class MaterialExporter private static MaterialBuilder BuildCharacter(Material material, string name) { - // TODO: handle models with an underlying diffuse var table = material.Mtrl.Table; // TODO: there's a few normal usages i should check, i think. - // TODO: tryget var normal = material.Textures[TextureUsage.SamplerNormal]; var operation = new ProcessCharacterNormalOperation(normal, table); ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds(), in operation); + var baseColor = operation.BaseColor; + if (material.Textures.TryGetValue(TextureUsage.SamplerDiffuse, out var diffuse)) + { + MultiplyOperation.Execute(diffuse, baseColor); + baseColor = diffuse; + } + + // TODO: what about the two specularmaps? + var specular = operation.Specular; + if (material.Textures.TryGetValue(TextureUsage.SamplerSpecular, out var newSpecular)) + { + MultiplyOperation.Execute(newSpecular, specular); + } + // TODO: clean up this name generation a bunch. probably a method. var imageName = name.Replace("/", "").Replace(".mtrl", ""); return BuildSharedBase(material, name) // NOTE: this isn't particularly precise to game behavior, but good enough for now. .WithAlpha(AlphaMode.MASK, 0.5f) - .WithBaseColor(BuildImage(operation.BaseColor, $"{imageName}_basecolor")) + .WithBaseColor(BuildImage(baseColor, $"{imageName}_basecolor")) .WithNormal(BuildImage(operation.Normal, $"{imageName}_normal")) - .WithSpecularColor(BuildImage(operation.Specular, $"{imageName}_specular")) + .WithSpecularColor(BuildImage(specular, $"{imageName}_specular")) .WithEmissive(BuildImage(operation.Emissive, $"{imageName}_emissive"), Vector3.One, 1); } @@ -105,6 +118,40 @@ public class MaterialExporter } } + private readonly struct MultiplyOperation + { + public static void Execute(Image target, Image multiplier) + where TPixel1 : unmanaged, IPixel + where TPixel2 : unmanaged, IPixel + { + // Ensure the images are the same size + var (small, large) = target.Width < multiplier.Width && target.Height < multiplier.Height + ? ((Image)target, (Image)multiplier) + : (multiplier, target); + small.Mutate(context => context.Resize(large.Width, large.Height)); + + var operation = new MultiplyOperation(target, multiplier); + ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, target.Bounds(), in operation); + } + } + + private readonly struct MultiplyOperation(Image target, Image multiplier) : IRowOperation + where TPixel1 : unmanaged, IPixel + where TPixel2 : unmanaged, IPixel + { + + public void Invoke(int y) + { + var targetSpan = target.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + var multiplierSpan = multiplier.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + + for (int x = 0; x < targetSpan.Length; x++) + { + targetSpan[x].FromVector4(targetSpan[x].ToVector4() * multiplierSpan[x].ToVector4()); + } + } + } + private static TableRow GetTableRowIndices(float input) { // These calculations are ported from character.shpk. From ca58c81bcebc051b0ac8e25255e5bc28095adee6 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 14 Jan 2024 00:06:16 +1100 Subject: [PATCH 10/18] Add characterglass support --- Penumbra/Import/Models/Export/MaterialExporter.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 3d274ff9..eff5f835 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -26,8 +26,10 @@ public class MaterialExporter Penumbra.Log.Debug($"Exporting material \"{name}\"."); return material.Mtrl.ShaderPackage.Name switch { - "character.shpk" => BuildCharacter(material, name), - _ => BuildFallback(material, name), + // NOTE: this isn't particularly precise to game behavior (it has some fade around high opacity), but good enough for now. + "character.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f), + "characterglass.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.BLEND), + _ => BuildFallback(material, name), }; } @@ -59,8 +61,6 @@ public class MaterialExporter var imageName = name.Replace("/", "").Replace(".mtrl", ""); return BuildSharedBase(material, name) - // NOTE: this isn't particularly precise to game behavior, but good enough for now. - .WithAlpha(AlphaMode.MASK, 0.5f) .WithBaseColor(BuildImage(baseColor, $"{imageName}_basecolor")) .WithNormal(BuildImage(operation.Normal, $"{imageName}_normal")) .WithSpecularColor(BuildImage(specular, $"{imageName}_specular")) From f71096f8b07fd69f8a505a7a68628784da45a125 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 14 Jan 2024 00:38:28 +1100 Subject: [PATCH 11/18] Handle mask ambient occlusion --- .../Import/Models/Export/MaterialExporter.cs | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index eff5f835..97bfb6bc 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -43,28 +43,50 @@ public class MaterialExporter var operation = new ProcessCharacterNormalOperation(normal, table); ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds(), in operation); - var baseColor = operation.BaseColor; + Image baseColor = operation.BaseColor; if (material.Textures.TryGetValue(TextureUsage.SamplerDiffuse, out var diffuse)) { - MultiplyOperation.Execute(diffuse, baseColor); + MultiplyOperation.Execute(diffuse, operation.BaseColor); baseColor = diffuse; } // TODO: what about the two specularmaps? - var specular = operation.Specular; - if (material.Textures.TryGetValue(TextureUsage.SamplerSpecular, out var newSpecular)) + Image specular = operation.Specular; + if (material.Textures.TryGetValue(TextureUsage.SamplerSpecular, out var specularTexture)) { - MultiplyOperation.Execute(newSpecular, specular); + MultiplyOperation.Execute(specularTexture, operation.Specular); + specular = specularTexture; + } + + Image? occlusion = null; + if (material.Textures.TryGetValue(TextureUsage.SamplerMask, out var maskTexture)) + { + // Extract the red channel for ambient occlusion. + maskTexture.Mutate(context => context.Filter(new ColorMatrix( + 1f, 1f, 1f, 0f, + 0f, 0f, 0f, 0f, + 0f, 0f, 0f, 0f, + 0f, 0f, 0f, 1f, + 0f, 0f, 0f, 0f + ))); + occlusion = maskTexture; + + // TODO: handle other textures stored in the mask? } // TODO: clean up this name generation a bunch. probably a method. var imageName = name.Replace("/", "").Replace(".mtrl", ""); - return BuildSharedBase(material, name) + var materialBuilder = BuildSharedBase(material, name) .WithBaseColor(BuildImage(baseColor, $"{imageName}_basecolor")) .WithNormal(BuildImage(operation.Normal, $"{imageName}_normal")) .WithSpecularColor(BuildImage(specular, $"{imageName}_specular")) .WithEmissive(BuildImage(operation.Emissive, $"{imageName}_emissive"), Vector3.One, 1); + + if (occlusion != null) + materialBuilder.WithOcclusion(BuildImage(occlusion, $"{imageName}_occlusion")); + + return materialBuilder; } // TODO: It feels a little silly to request the entire normal here when extrating the normal only needs some of the components. From b5d4b31301927aff8e4f81dc877d87cb2ffdf693 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 14 Jan 2024 12:35:26 +1100 Subject: [PATCH 12/18] Add skin.shpk support --- .../Import/Models/Export/MaterialExporter.cs | 92 ++++++++++++++----- 1 file changed, 71 insertions(+), 21 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 97bfb6bc..923b9c95 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -29,6 +29,7 @@ public class MaterialExporter // NOTE: this isn't particularly precise to game behavior (it has some fade around high opacity), but good enough for now. "character.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f), "characterglass.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.BLEND), + "skin.shpk" => BuildSkin(material, name), _ => BuildFallback(material, name), }; } @@ -74,17 +75,14 @@ public class MaterialExporter // TODO: handle other textures stored in the mask? } - // TODO: clean up this name generation a bunch. probably a method. - var imageName = name.Replace("/", "").Replace(".mtrl", ""); - var materialBuilder = BuildSharedBase(material, name) - .WithBaseColor(BuildImage(baseColor, $"{imageName}_basecolor")) - .WithNormal(BuildImage(operation.Normal, $"{imageName}_normal")) - .WithSpecularColor(BuildImage(specular, $"{imageName}_specular")) - .WithEmissive(BuildImage(operation.Emissive, $"{imageName}_emissive"), Vector3.One, 1); + .WithBaseColor(BuildImage(baseColor, name, "basecolor")) + .WithNormal(BuildImage(operation.Normal, name, "normal")) + .WithSpecularColor(BuildImage(specular, name, "specular")) + .WithEmissive(BuildImage(operation.Emissive, name, "emissive"), Vector3.One, 1); if (occlusion != null) - materialBuilder.WithOcclusion(BuildImage(occlusion, $"{imageName}_occlusion")); + materialBuilder.WithOcclusion(BuildImage(occlusion, name, "occlusion")); return materialBuilder; } @@ -140,6 +138,24 @@ public class MaterialExporter } } + private static TableRow GetTableRowIndices(float input) + { + // These calculations are ported from character.shpk. + var smoothed = MathF.Floor(((input * 7.5f) % 1.0f) * 2) + * (-input * 15 + MathF.Floor(input * 15 + 0.5f)) + + input * 15; + + var stepped = MathF.Floor(smoothed + 0.5f); + + return new TableRow + { + Stepped = (int)stepped, + Previous = (int)MathF.Floor(smoothed), + Next = (int)MathF.Ceiling(smoothed), + Weight = smoothed % 1, + }; + } + private readonly struct MultiplyOperation { public static void Execute(Image target, Image multiplier) @@ -174,22 +190,54 @@ public class MaterialExporter } } - private static TableRow GetTableRowIndices(float input) + private static MaterialBuilder BuildSkin(Material material, string name) { - // These calculations are ported from character.shpk. - var smoothed = MathF.Floor(((input * 7.5f) % 1.0f) * 2) - * (-input * 15 + MathF.Floor(input * 15 + 0.5f)) - + input * 15; + // Trust me bro. + const uint categorySkinType = 0x380CAED0; + const uint valueFace = 0xF5673524; - var stepped = MathF.Floor(smoothed + 0.5f); + // Face is the default for the skin shader, so a lack of skin type category is also correct. + var isFace = !material.Mtrl.ShaderPackage.ShaderKeys + .Any(key => key.Category == categorySkinType && key.Value != valueFace); - return new TableRow + // TODO: There's more nuance to skin than this, but this should be enough for a baseline reference. + // TODO: Specular? + var diffuse = material.Textures[TextureUsage.SamplerDiffuse]; + var normal = material.Textures[TextureUsage.SamplerNormal]; + + // Create a copy of the normal that's the same size as the diffuse for purposes of copying the opacity across. + var resizedNormal = normal.Clone(context => context.Resize(diffuse.Width, diffuse.Height)); + diffuse.ProcessPixelRows(resizedNormal, (diffuseAccessor, normalAccessor) => { - Stepped = (int)stepped, - Previous = (int)MathF.Floor(smoothed), - Next = (int)MathF.Ceiling(smoothed), - Weight = smoothed % 1, - }; + for (int y = 0; y < diffuseAccessor.Height; y++) + { + var diffuseSpan = diffuseAccessor.GetRowSpan(y); + var normalSpan = normalAccessor.GetRowSpan(y); + + for (int x = 0; x < diffuseSpan.Length; x++) + { + diffuseSpan[x].A = normalSpan[x].B; + } + } + }); + + // Clear the blue channel out of the normal now that we're done with it. + normal.ProcessPixelRows(normalAccessor => { + for (int y = 0; y < normalAccessor.Height; y++) + { + var normalSpan = normalAccessor.GetRowSpan(y); + + for (int x = 0; x < normalSpan.Length; x++) + { + normalSpan[x].B = byte.MaxValue; + } + } + }); + + return BuildSharedBase(material, name) + .WithBaseColor(BuildImage(diffuse, name, "basecolor")) + .WithNormal(BuildImage(normal, name, "normal")) + .WithAlpha(isFace? AlphaMode.MASK : AlphaMode.OPAQUE, 0.5f); } private ref struct TableRow @@ -218,8 +266,10 @@ public class MaterialExporter .WithDoubleSide(showBackfaces); } - private static ImageBuilder BuildImage(Image image, string name) + private static ImageBuilder BuildImage(Image image, string materialName, string suffix) { + var name = materialName.Replace("/", "").Replace(".mtrl", "") + $"_{suffix}"; + byte[] textureBytes; using (var memoryStream = new MemoryStream()) { From a6788c6dd3c69a2f183df1f7af06d1188c9c5ff7 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 14 Jan 2024 17:30:13 +1100 Subject: [PATCH 13/18] Add hair and iris support --- .../Import/Models/Export/MaterialExporter.cs | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 923b9c95..98e4b3b9 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -29,6 +29,8 @@ public class MaterialExporter // NOTE: this isn't particularly precise to game behavior (it has some fade around high opacity), but good enough for now. "character.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f), "characterglass.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.BLEND), + "hair.shpk" => BuildHair(material, name), + "iris.shpk" => BuildIris(material, name), "skin.shpk" => BuildSkin(material, name), _ => BuildFallback(material, name), }; @@ -190,6 +192,84 @@ public class MaterialExporter } } + // TODO: These are hardcoded colours - I'm not keen on supporting highly customiseable exports, but there's possibly some more sensible values to use here. + private static Vector4 _defaultHairColor = new Vector4(130, 64, 13, 255) / new Vector4(255); + private static Vector4 _defaultHighlightColor = new Vector4(77, 126, 240, 255) / new Vector4(255); + + private static MaterialBuilder BuildHair(Material material, string name) + { + // Trust me bro. + const uint categoryHairType = 0x24826489; + const uint valueFace = 0x6E5B8F10; + + var isFace = material.Mtrl.ShaderPackage.ShaderKeys + .Any(key => key.Category == categoryHairType && key.Value == valueFace); + + var normal = material.Textures[TextureUsage.SamplerNormal]; + var mask = material.Textures[TextureUsage.SamplerMask]; + + mask.Mutate(context => context.Resize(normal.Width, normal.Height)); + + var baseColor = new Image(normal.Width, normal.Height); + normal.ProcessPixelRows(mask, baseColor, (normalAccessor, maskAccessor, baseColorAccessor) => + { + for (int y = 0; y < normalAccessor.Height; y++) + { + var normalSpan = normalAccessor.GetRowSpan(y); + var maskSpan = maskAccessor.GetRowSpan(y); + var baseColorSpan = baseColorAccessor.GetRowSpan(y); + + for (int x = 0; x < normalSpan.Length; x++) + { + var color = Vector4.Lerp(_defaultHairColor, _defaultHighlightColor, maskSpan[x].A / 255f); + baseColorSpan[x].FromVector4(color * new Vector4(maskSpan[x].R / 255f)); + baseColorSpan[x].A = normalSpan[x].A; + + normalSpan[x].A = byte.MaxValue; + } + } + }); + + return BuildSharedBase(material, name) + .WithBaseColor(BuildImage(baseColor, name, "basecolor")) + .WithNormal(BuildImage(normal, name, "normal")) + .WithAlpha(isFace? AlphaMode.BLEND : AlphaMode.MASK, 0.5f); + } + + private static Vector4 _defaultEyeColor = new Vector4(21, 176, 172, 255) / new Vector4(255); + + // NOTE: This is largely the same as the hair material, but is also missing a few features that would cause it to diverge. Keeping seperate for now. + private static MaterialBuilder BuildIris(Material material, string name) + { + var normal = material.Textures[TextureUsage.SamplerNormal]; + var mask = material.Textures[TextureUsage.SamplerMask]; + + mask.Mutate(context => context.Resize(normal.Width, normal.Height)); + + var baseColor = new Image(normal.Width, normal.Height); + normal.ProcessPixelRows(mask, baseColor, (normalAccessor, maskAccessor, baseColorAccessor) => + { + for (int y = 0; y < normalAccessor.Height; y++) + { + var normalSpan = normalAccessor.GetRowSpan(y); + var maskSpan = maskAccessor.GetRowSpan(y); + var baseColorSpan = baseColorAccessor.GetRowSpan(y); + + for (int x = 0; x < normalSpan.Length; x++) + { + baseColorSpan[x].FromVector4(_defaultEyeColor * new Vector4(maskSpan[x].R / 255f)); + baseColorSpan[x].A = normalSpan[x].A; + + normalSpan[x].A = byte.MaxValue; + } + } + }); + + return BuildSharedBase(material, name) + .WithBaseColor(BuildImage(baseColor, name, "basecolor")) + .WithNormal(BuildImage(normal, name, "normal")); + } + private static MaterialBuilder BuildSkin(Material material, string name) { // Trust me bro. From 5e6ca8b22c384fa5b4246d09ab3371d671454df3 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 14 Jan 2024 17:37:34 +1100 Subject: [PATCH 14/18] Improve fallback handling --- .../Import/Models/Export/MaterialExporter.cs | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 98e4b3b9..0b109ddf 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -158,6 +158,14 @@ public class MaterialExporter }; } + private ref struct TableRow + { + public int Stepped; + public int Previous; + public int Next; + public float Weight; + } + private readonly struct MultiplyOperation { public static void Execute(Image target, Image multiplier) @@ -320,20 +328,21 @@ public class MaterialExporter .WithAlpha(isFace? AlphaMode.MASK : AlphaMode.OPAQUE, 0.5f); } - private ref struct TableRow - { - public int Stepped; - public int Previous; - public int Next; - public float Weight; - } - private static MaterialBuilder BuildFallback(Material material, string name) { Penumbra.Log.Warning($"Unhandled shader package: {material.Mtrl.ShaderPackage.Name}"); - return BuildSharedBase(material, name) + + var materialBuilder = BuildSharedBase(material, name) .WithMetallicRoughnessShader() - .WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, Vector4.One); + .WithBaseColor(Vector4.One); + + if (material.Textures.TryGetValue(TextureUsage.SamplerDiffuse, out var diffuse)) + materialBuilder.WithBaseColor(BuildImage(diffuse, name, "basecolor")); + + if (material.Textures.TryGetValue(TextureUsage.SamplerNormal, out var normal)) + materialBuilder.WithNormal(BuildImage(normal, name, "normal")); + + return materialBuilder; } private static MaterialBuilder BuildSharedBase(Material material, string name) From 9ff3227cf4105b602c40f3eec2adddf5759cb507 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 14 Jan 2024 20:12:17 +1100 Subject: [PATCH 15/18] Cleanup pass --- .../Import/Models/Export/MaterialExporter.cs | 29 +++++++++++++------ Penumbra/Import/Models/ModelManager.cs | 12 ++++---- .../ModEditWindow.Models.MdlTab.cs | 5 ++-- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 0b109ddf..a189e7bc 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -21,6 +21,7 @@ public class MaterialExporter // variant? } + /// Build a glTF material from a hydrated XIV model, with the provided name. public static MaterialBuilder Export(Material material, string name) { Penumbra.Log.Debug($"Exporting material \"{name}\"."); @@ -36,16 +37,18 @@ public class MaterialExporter }; } + /// Build a material following the semantics of character.shpk. private static MaterialBuilder BuildCharacter(Material material, string name) { + // Build the textures from the color table. var table = material.Mtrl.Table; - // TODO: there's a few normal usages i should check, i think. var normal = material.Textures[TextureUsage.SamplerNormal]; var operation = new ProcessCharacterNormalOperation(normal, table); ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds(), in operation); + // Check if full textures are provided, and merge in if available. Image baseColor = operation.BaseColor; if (material.Textures.TryGetValue(TextureUsage.SamplerDiffuse, out var diffuse)) { @@ -53,7 +56,6 @@ public class MaterialExporter baseColor = diffuse; } - // TODO: what about the two specularmaps? Image specular = operation.Specular; if (material.Textures.TryGetValue(TextureUsage.SamplerSpecular, out var specularTexture)) { @@ -61,6 +63,7 @@ public class MaterialExporter specular = specularTexture; } + // Pull further information from the mask. Image? occlusion = null; if (material.Textures.TryGetValue(TextureUsage.SamplerMask, out var maskTexture)) { @@ -73,7 +76,7 @@ public class MaterialExporter 0f, 0f, 0f, 0f ))); occlusion = maskTexture; - + // TODO: handle other textures stored in the mask? } @@ -89,7 +92,7 @@ public class MaterialExporter return materialBuilder; } - // TODO: It feels a little silly to request the entire normal here when extrating the normal only needs some of the components. + // TODO: It feels a little silly to request the entire normal here when extracting the normal only needs some of the components. // As a future refactor, it would be neat to accept a single-channel field here, and then do composition of other stuff later. private readonly struct ProcessCharacterNormalOperation(Image normal, MtrlFile.ColorTable table) : IRowOperation { @@ -143,7 +146,7 @@ public class MaterialExporter private static TableRow GetTableRowIndices(float input) { // These calculations are ported from character.shpk. - var smoothed = MathF.Floor(((input * 7.5f) % 1.0f) * 2) + var smoothed = MathF.Floor(((input * 7.5f) % 1.0f) * 2) * (-input * 15 + MathF.Floor(input * 15 + 0.5f)) + input * 15; @@ -204,6 +207,7 @@ public class MaterialExporter private static Vector4 _defaultHairColor = new Vector4(130, 64, 13, 255) / new Vector4(255); private static Vector4 _defaultHighlightColor = new Vector4(77, 126, 240, 255) / new Vector4(255); + /// Build a material following the semantics of hair.shpk. private static MaterialBuilder BuildHair(Material material, string name) { // Trust me bro. @@ -241,11 +245,12 @@ public class MaterialExporter return BuildSharedBase(material, name) .WithBaseColor(BuildImage(baseColor, name, "basecolor")) .WithNormal(BuildImage(normal, name, "normal")) - .WithAlpha(isFace? AlphaMode.BLEND : AlphaMode.MASK, 0.5f); + .WithAlpha(isFace ? AlphaMode.BLEND : AlphaMode.MASK, 0.5f); } private static Vector4 _defaultEyeColor = new Vector4(21, 176, 172, 255) / new Vector4(255); + /// Build a material following the semantics of iris.shpk. // NOTE: This is largely the same as the hair material, but is also missing a few features that would cause it to diverge. Keeping seperate for now. private static MaterialBuilder BuildIris(Material material, string name) { @@ -278,6 +283,7 @@ public class MaterialExporter .WithNormal(BuildImage(normal, name, "normal")); } + /// Build a material following the semantics of skin.shpk. private static MaterialBuilder BuildSkin(Material material, string name) { // Trust me bro. @@ -310,7 +316,8 @@ public class MaterialExporter }); // Clear the blue channel out of the normal now that we're done with it. - normal.ProcessPixelRows(normalAccessor => { + normal.ProcessPixelRows(normalAccessor => + { for (int y = 0; y < normalAccessor.Height; y++) { var normalSpan = normalAccessor.GetRowSpan(y); @@ -325,14 +332,16 @@ public class MaterialExporter return BuildSharedBase(material, name) .WithBaseColor(BuildImage(diffuse, name, "basecolor")) .WithNormal(BuildImage(normal, name, "normal")) - .WithAlpha(isFace? AlphaMode.MASK : AlphaMode.OPAQUE, 0.5f); + .WithAlpha(isFace ? AlphaMode.MASK : AlphaMode.OPAQUE, 0.5f); } + /// 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) { Penumbra.Log.Warning($"Unhandled shader package: {material.Mtrl.ShaderPackage.Name}"); - var materialBuilder = BuildSharedBase(material, name) + var materialBuilder = BuildSharedBase(material, name) .WithMetallicRoughnessShader() .WithBaseColor(Vector4.One); @@ -345,6 +354,7 @@ public class MaterialExporter return materialBuilder; } + /// Build a material pre-configured with settings common to all XIV materials/shaders. private static MaterialBuilder BuildSharedBase(Material material, string name) { // TODO: Move this and potentially the other known stuff into MtrlFile? @@ -355,6 +365,7 @@ public class MaterialExporter .WithDoubleSide(showBackfaces); } + /// Convert an ImageSharp Image into an ImageBuilder for use with SharpGLTF. private static ImageBuilder BuildImage(Image image, string materialName, string suffix) { var name = materialName.Replace("/", "").Replace(".mtrl", "") + $"_{suffix}"; diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 4f652436..bbc274a5 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -23,8 +23,8 @@ using LuminaMaterial = Lumina.Models.Materials.Material; public sealed class ModelManager(IFramework framework, ActiveCollections collections, IDataManager gameData, GamePathParser parser, TextureManager textureManager) : SingleTaskQueue, IDisposable { - private readonly IFramework _framework = framework; - private readonly IDataManager _gameData = gameData; + private readonly IFramework _framework = framework; + private readonly IDataManager _gameData = gameData; private readonly TextureManager _textureManager = textureManager; private readonly ConcurrentDictionary _tasks = new(); @@ -163,7 +163,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect Penumbra.Log.Debug("[GLTF Export] Done."); } - /// Attempt to read out the pertinent information from a .sklb. + /// Attempt to read out the pertinent information from the sklb file paths provided. private IEnumerable BuildSkeletons(CancellationToken cancel) { var havokTasks = sklbPaths @@ -185,6 +185,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect delayTicks: pair.Index, cancellationToken: cancel); } + /// Read a .mtrl and hydrate its textures. private MaterialExporter.Material BuildMaterial(string relativePath, CancellationToken cancel) { // TODO: this should probably be chosen in the export settings @@ -194,7 +195,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect ? LuminaMaterial.ResolveRelativeMaterialPath(relativePath, variantId) : relativePath; - // TODO: this should be a recoverable warning - as should the one below it i think + // TODO: this should be a recoverable warning if (absolutePath == null) throw new Exception("Failed to resolve material path."); @@ -202,7 +203,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect return new MaterialExporter.Material { - Mtrl = mtrl, + Mtrl = mtrl, Textures = mtrl.ShaderPackage.Samplers.ToDictionary( sampler => (TextureUsage)sampler.SamplerId, sampler => ConvertImage(mtrl.Textures[sampler.TextureIndex], cancel) @@ -210,6 +211,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect }; } + /// Read a texture referenced by a .mtrl and convert it into an ImageSharp image. private Image ConvertImage(MtrlFile.Texture texture, CancellationToken cancel) { using var textureData = new MemoryStream(read(texture.Path)); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index f79d161e..43a06012 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -225,15 +225,16 @@ public partial class ModEditWindow private byte[] ReadFile(string path) { // TODO: if cross-collection lookups are turned off, this conversion can be skipped - if (!Utf8GamePath.FromString(path, out var utf8SklbPath, true)) + if (!Utf8GamePath.FromString(path, out var utf8Path, true)) throw new Exception($"Resolved path {path} could not be converted to a game path."); - var resolvedPath = _edit._activeCollections.Current.ResolvePath(utf8SklbPath); + var resolvedPath = _edit._activeCollections.Current.ResolvePath(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 == null ? _edit._gameData.GetFile(path)?.Data : File.ReadAllBytes(resolvedPath.Value.ToPath()); + // TODO: some callers may not care about failures - handle exceptions seperately? return bytes ?? throw new Exception( $"Resolved path {path} could not be found. If modded, is it enabled in the current collection?"); } From 0e50cc9c47ddffe5e9e6d6f9019d59402ae74209 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 14 Jan 2024 22:27:23 +1100 Subject: [PATCH 16/18] Handle character mask R same as other shaders --- .../Import/Models/Export/MaterialExporter.cs | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index a189e7bc..0bacb98a 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -49,7 +49,7 @@ public class MaterialExporter ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds(), in operation); // Check if full textures are provided, and merge in if available. - Image baseColor = operation.BaseColor; + Image baseColor = operation.BaseColor; if (material.Textures.TryGetValue(TextureUsage.SamplerDiffuse, out var diffuse)) { MultiplyOperation.Execute(diffuse, operation.BaseColor); @@ -64,32 +64,31 @@ public class MaterialExporter } // Pull further information from the mask. - Image? occlusion = null; if (material.Textures.TryGetValue(TextureUsage.SamplerMask, out var maskTexture)) { - // Extract the red channel for ambient occlusion. - maskTexture.Mutate(context => context.Filter(new ColorMatrix( - 1f, 1f, 1f, 0f, - 0f, 0f, 0f, 0f, - 0f, 0f, 0f, 0f, - 0f, 0f, 0f, 1f, - 0f, 0f, 0f, 0f - ))); - occlusion = maskTexture; + // Extract the red channel for "ambient occlusion". + maskTexture.Mutate(context => context.Resize(baseColor.Width, baseColor.Height)); + maskTexture.ProcessPixelRows(baseColor, (maskAccessor, baseColorAccessor) => + { + for (int y = 0; y < maskAccessor.Height; y++) + { + var maskSpan = maskAccessor.GetRowSpan(y); + var baseColorSpan = baseColorAccessor.GetRowSpan(y); + + for (int x = 0; x < maskSpan.Length; x++) + baseColorSpan[x].FromVector4(baseColorSpan[x].ToVector4() * new Vector4(maskSpan[x].R / 255f)); + } + }); + // TODO: handle other textures stored in the mask? } - var materialBuilder = BuildSharedBase(material, name) + return BuildSharedBase(material, name) .WithBaseColor(BuildImage(baseColor, name, "basecolor")) .WithNormal(BuildImage(operation.Normal, name, "normal")) .WithSpecularColor(BuildImage(specular, name, "specular")) .WithEmissive(BuildImage(operation.Emissive, name, "emissive"), Vector3.One, 1); - - if (occlusion != null) - materialBuilder.WithOcclusion(BuildImage(occlusion, name, "occlusion")); - - return materialBuilder; } // TODO: It feels a little silly to request the entire normal here when extracting the normal only needs some of the components. From ec92f93d229389d901cd57107a8fbd967007e24c Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 14 Jan 2024 22:36:33 +1100 Subject: [PATCH 17/18] Improve material and texture path resolution --- Penumbra/Import/Models/ModelManager.cs | 63 ++++++++++++++++++++------ 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index bbc274a5..c6e2d836 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -108,6 +108,40 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect return [GamePaths.Skeleton.Sklb.Path(info.GenderRace, EstManipulation.ToName(type), targetId)]; } + /// Try to resolve the absolute path to a .mtrl from the potentially-partial path provided by a model. + private string ResolveMtrlPath(string rawPath) + { + // TODO: this should probably be chosen in the export settings + var variantId = 1; + + // Get standardised paths + var absolutePath = rawPath.StartsWith('/') + ? LuminaMaterial.ResolveRelativeMaterialPath(rawPath, variantId) + : rawPath; + var relativePath = rawPath.StartsWith('/') + ? rawPath + : '/' + Path.GetFileName(rawPath); + + // TODO: this should be a recoverable warning + if (absolutePath == null) + throw new Exception("Failed to resolve material path."); + + var info = parser.GetFileInfo(absolutePath); + if (info.FileType is not FileType.Material) + throw new Exception($"Material path {rawPath} does not conform to material conventions."); + + var resolvedPath = info.ObjectType switch + { + ObjectType.Character => GamePaths.Character.Mtrl.Path( + info.GenderRace, info.BodySlot, info.PrimaryId, relativePath, out _, out _, info.Variant), + _ => absolutePath, + }; + + Penumbra.Log.Debug($"Resolved material {rawPath} to {resolvedPath}"); + + return resolvedPath; + } + private Task Enqueue(IAction action) { if (_disposed) @@ -188,18 +222,8 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect /// Read a .mtrl and hydrate its textures. private MaterialExporter.Material BuildMaterial(string relativePath, CancellationToken cancel) { - // TODO: this should probably be chosen in the export settings - var variantId = 1; - - var absolutePath = relativePath.StartsWith("/") - ? LuminaMaterial.ResolveRelativeMaterialPath(relativePath, variantId) - : relativePath; - - // TODO: this should be a recoverable warning - if (absolutePath == null) - throw new Exception("Failed to resolve material path."); - - var mtrl = new MtrlFile(read(absolutePath)); + var path = manager.ResolveMtrlPath(relativePath); + var mtrl = new MtrlFile(read(path)); return new MaterialExporter.Material { @@ -214,7 +238,20 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect /// Read a texture referenced by a .mtrl and convert it into an ImageSharp image. private Image ConvertImage(MtrlFile.Texture texture, CancellationToken cancel) { - using var textureData = new MemoryStream(read(texture.Path)); + // Work out the texture's path - the DX11 material flag controls a file name prefix. + var texturePath = texture.Path; + if (texture.DX11) + { + var lastSlashIndex = texturePath.LastIndexOf('/'); + var directory = lastSlashIndex == -1 ? texturePath : texturePath.Substring(0, lastSlashIndex); + var fileName = Path.GetFileName(texturePath); + if (!fileName.StartsWith("--")) + { + texturePath = $"{directory}/--{fileName}"; + } + } + + using var textureData = new MemoryStream(read(texturePath)); var image = TexFileParser.Parse(textureData); var pngImage = TextureManager.ConvertToPng(image, cancel).AsPng; if (pngImage == null) From 3b9d841014029a38a67c83caff89ddc89cad8be7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 14 Jan 2024 13:35:46 +0100 Subject: [PATCH 18/18] Minor cleanup. --- .../Import/Models/Export/MaterialExporter.cs | 137 +++++++++--------- Penumbra/Import/Models/ModelManager.cs | 67 +++++---- .../ModEditWindow.Models.MdlTab.cs | 12 +- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 17 +-- 4 files changed, 115 insertions(+), 118 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 0bacb98a..2a49e77f 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -17,6 +17,7 @@ public class MaterialExporter public struct Material { public MtrlFile Mtrl; + public Dictionary> Textures; // variant? } @@ -49,7 +50,7 @@ public class MaterialExporter ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds(), in operation); // Check if full textures are provided, and merge in if available. - Image baseColor = operation.BaseColor; + var baseColor = operation.BaseColor; if (material.Textures.TryGetValue(TextureUsage.SamplerDiffuse, out var diffuse)) { MultiplyOperation.Execute(diffuse, operation.BaseColor); @@ -70,24 +71,22 @@ public class MaterialExporter maskTexture.Mutate(context => context.Resize(baseColor.Width, baseColor.Height)); maskTexture.ProcessPixelRows(baseColor, (maskAccessor, baseColorAccessor) => { - for (int y = 0; y < maskAccessor.Height; y++) + for (var y = 0; y < maskAccessor.Height; y++) { - var maskSpan = maskAccessor.GetRowSpan(y); + var maskSpan = maskAccessor.GetRowSpan(y); var baseColorSpan = baseColorAccessor.GetRowSpan(y); - for (int x = 0; x < maskSpan.Length; x++) + for (var x = 0; x < maskSpan.Length; x++) baseColorSpan[x].FromVector4(baseColorSpan[x].ToVector4() * new Vector4(maskSpan[x].R / 255f)); } }); - - // TODO: handle other textures stored in the mask? } return BuildSharedBase(material, name) - .WithBaseColor(BuildImage(baseColor, name, "basecolor")) - .WithNormal(BuildImage(operation.Normal, name, "normal")) - .WithSpecularColor(BuildImage(specular, name, "specular")) + .WithBaseColor(BuildImage(baseColor, name, "basecolor")) + .WithNormal(BuildImage(operation.Normal, name, "normal")) + .WithSpecularColor(BuildImage(specular, name, "specular")) .WithEmissive(BuildImage(operation.Emissive, name, "emissive"), Vector3.One, 1); } @@ -95,31 +94,38 @@ public class MaterialExporter // As a future refactor, it would be neat to accept a single-channel field here, and then do composition of other stuff later. private readonly struct ProcessCharacterNormalOperation(Image normal, MtrlFile.ColorTable table) : IRowOperation { - public Image Normal { get; private init; } = normal.Clone(); - public Image BaseColor { get; private init; } = new Image(normal.Width, normal.Height); - public Image Specular { get; private init; } = new Image(normal.Width, normal.Height); - public Image Emissive { get; private init; } = new Image(normal.Width, normal.Height); + public Image Normal { get; } = normal.Clone(); + public Image BaseColor { get; } = new(normal.Width, normal.Height); + public Image Specular { get; } = new(normal.Width, normal.Height); + public Image Emissive { get; } = new(normal.Width, normal.Height); - private Buffer2D NormalBuffer => Normal.Frames.RootFrame.PixelBuffer; - private Buffer2D BaseColorBuffer => BaseColor.Frames.RootFrame.PixelBuffer; - private Buffer2D SpecularBuffer => Specular.Frames.RootFrame.PixelBuffer; - private Buffer2D EmissiveBuffer => Emissive.Frames.RootFrame.PixelBuffer; + private Buffer2D NormalBuffer + => Normal.Frames.RootFrame.PixelBuffer; + + private Buffer2D BaseColorBuffer + => BaseColor.Frames.RootFrame.PixelBuffer; + + private Buffer2D SpecularBuffer + => Specular.Frames.RootFrame.PixelBuffer; + + private Buffer2D EmissiveBuffer + => Emissive.Frames.RootFrame.PixelBuffer; public void Invoke(int y) { - var normalSpan = NormalBuffer.DangerousGetRowSpan(y); + var normalSpan = NormalBuffer.DangerousGetRowSpan(y); var baseColorSpan = BaseColorBuffer.DangerousGetRowSpan(y); - var specularSpan = SpecularBuffer.DangerousGetRowSpan(y); - var emissiveSpan = EmissiveBuffer.DangerousGetRowSpan(y); + var specularSpan = SpecularBuffer.DangerousGetRowSpan(y); + var emissiveSpan = EmissiveBuffer.DangerousGetRowSpan(y); - for (int x = 0; x < normalSpan.Length; x++) + for (var x = 0; x < normalSpan.Length; x++) { ref var normalPixel = ref normalSpan[x]; // Table row data (.a) var tableRow = GetTableRowIndices(normalPixel.A / 255f); - var prevRow = table[tableRow.Previous]; - var nextRow = table[tableRow.Next]; + var prevRow = table[tableRow.Previous]; + var nextRow = table[tableRow.Next]; // Base colour (table, .b) var lerpedDiffuse = Vector3.Lerp(prevRow.Diffuse, nextRow.Diffuse, tableRow.Weight); @@ -145,9 +151,9 @@ public class MaterialExporter private static TableRow GetTableRowIndices(float input) { // These calculations are ported from character.shpk. - var smoothed = MathF.Floor(((input * 7.5f) % 1.0f) * 2) - * (-input * 15 + MathF.Floor(input * 15 + 0.5f)) - + input * 15; + var smoothed = MathF.Floor(input * 7.5f % 1.0f * 2) + * (-input * 15 + MathF.Floor(input * 15 + 0.5f)) + + input * 15; var stepped = MathF.Floor(smoothed + 0.5f); @@ -162,9 +168,9 @@ public class MaterialExporter private ref struct TableRow { - public int Stepped; - public int Previous; - public int Next; + public int Stepped; + public int Previous; + public int Next; public float Weight; } @@ -189,50 +195,47 @@ public class MaterialExporter where TPixel1 : unmanaged, IPixel where TPixel2 : unmanaged, IPixel { - public void Invoke(int y) { - var targetSpan = target.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + var targetSpan = target.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); var multiplierSpan = multiplier.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); - for (int x = 0; x < targetSpan.Length; x++) - { + for (var x = 0; x < targetSpan.Length; x++) targetSpan[x].FromVector4(targetSpan[x].ToVector4() * multiplierSpan[x].ToVector4()); - } } } - // TODO: These are hardcoded colours - I'm not keen on supporting highly customiseable exports, but there's possibly some more sensible values to use here. - private static Vector4 _defaultHairColor = new Vector4(130, 64, 13, 255) / new Vector4(255); - private static Vector4 _defaultHighlightColor = new Vector4(77, 126, 240, 255) / new Vector4(255); + // TODO: These are hardcoded colours - I'm not keen on supporting highly customizable exports, but there's possibly some more sensible values to use here. + private static readonly Vector4 DefaultHairColor = new Vector4(130, 64, 13, 255) / new Vector4(255); + private static readonly Vector4 DefaultHighlightColor = new Vector4(77, 126, 240, 255) / new Vector4(255); /// Build a material following the semantics of hair.shpk. private static MaterialBuilder BuildHair(Material material, string name) { // Trust me bro. const uint categoryHairType = 0x24826489; - const uint valueFace = 0x6E5B8F10; + const uint valueFace = 0x6E5B8F10; var isFace = material.Mtrl.ShaderPackage.ShaderKeys - .Any(key => key.Category == categoryHairType && key.Value == valueFace); + .Any(key => key is { Category: categoryHairType, Value: valueFace }); var normal = material.Textures[TextureUsage.SamplerNormal]; - var mask = material.Textures[TextureUsage.SamplerMask]; + var mask = material.Textures[TextureUsage.SamplerMask]; mask.Mutate(context => context.Resize(normal.Width, normal.Height)); var baseColor = new Image(normal.Width, normal.Height); normal.ProcessPixelRows(mask, baseColor, (normalAccessor, maskAccessor, baseColorAccessor) => { - for (int y = 0; y < normalAccessor.Height; y++) + for (var y = 0; y < normalAccessor.Height; y++) { - var normalSpan = normalAccessor.GetRowSpan(y); - var maskSpan = maskAccessor.GetRowSpan(y); + var normalSpan = normalAccessor.GetRowSpan(y); + var maskSpan = maskAccessor.GetRowSpan(y); var baseColorSpan = baseColorAccessor.GetRowSpan(y); - for (int x = 0; x < normalSpan.Length; x++) + for (var x = 0; x < normalSpan.Length; x++) { - var color = Vector4.Lerp(_defaultHairColor, _defaultHighlightColor, maskSpan[x].A / 255f); + var color = Vector4.Lerp(DefaultHairColor, DefaultHighlightColor, maskSpan[x].A / 255f); baseColorSpan[x].FromVector4(color * new Vector4(maskSpan[x].R / 255f)); baseColorSpan[x].A = normalSpan[x].A; @@ -243,33 +246,33 @@ public class MaterialExporter return BuildSharedBase(material, name) .WithBaseColor(BuildImage(baseColor, name, "basecolor")) - .WithNormal(BuildImage(normal, name, "normal")) + .WithNormal(BuildImage(normal, name, "normal")) .WithAlpha(isFace ? AlphaMode.BLEND : AlphaMode.MASK, 0.5f); } - private static Vector4 _defaultEyeColor = new Vector4(21, 176, 172, 255) / new Vector4(255); + private static readonly Vector4 DefaultEyeColor = new Vector4(21, 176, 172, 255) / new Vector4(255); /// Build a material following the semantics of iris.shpk. - // NOTE: This is largely the same as the hair material, but is also missing a few features that would cause it to diverge. Keeping seperate for now. + // NOTE: This is largely the same as the hair material, but is also missing a few features that would cause it to diverge. Keeping separate for now. private static MaterialBuilder BuildIris(Material material, string name) { var normal = material.Textures[TextureUsage.SamplerNormal]; - var mask = material.Textures[TextureUsage.SamplerMask]; + var mask = material.Textures[TextureUsage.SamplerMask]; mask.Mutate(context => context.Resize(normal.Width, normal.Height)); var baseColor = new Image(normal.Width, normal.Height); normal.ProcessPixelRows(mask, baseColor, (normalAccessor, maskAccessor, baseColorAccessor) => { - for (int y = 0; y < normalAccessor.Height; y++) + for (var y = 0; y < normalAccessor.Height; y++) { - var normalSpan = normalAccessor.GetRowSpan(y); - var maskSpan = maskAccessor.GetRowSpan(y); + var normalSpan = normalAccessor.GetRowSpan(y); + var maskSpan = maskAccessor.GetRowSpan(y); var baseColorSpan = baseColorAccessor.GetRowSpan(y); - for (int x = 0; x < normalSpan.Length; x++) + for (var x = 0; x < normalSpan.Length; x++) { - baseColorSpan[x].FromVector4(_defaultEyeColor * new Vector4(maskSpan[x].R / 255f)); + baseColorSpan[x].FromVector4(DefaultEyeColor * new Vector4(maskSpan[x].R / 255f)); baseColorSpan[x].A = normalSpan[x].A; normalSpan[x].A = byte.MaxValue; @@ -279,7 +282,7 @@ public class MaterialExporter return BuildSharedBase(material, name) .WithBaseColor(BuildImage(baseColor, name, "basecolor")) - .WithNormal(BuildImage(normal, name, "normal")); + .WithNormal(BuildImage(normal, name, "normal")); } /// Build a material following the semantics of skin.shpk. @@ -287,7 +290,7 @@ public class MaterialExporter { // Trust me bro. const uint categorySkinType = 0x380CAED0; - const uint valueFace = 0xF5673524; + const uint valueFace = 0xF5673524; // Face is the default for the skin shader, so a lack of skin type category is also correct. var isFace = !material.Mtrl.ShaderPackage.ShaderKeys @@ -296,41 +299,37 @@ public class MaterialExporter // TODO: There's more nuance to skin than this, but this should be enough for a baseline reference. // TODO: Specular? var diffuse = material.Textures[TextureUsage.SamplerDiffuse]; - var normal = material.Textures[TextureUsage.SamplerNormal]; + var normal = material.Textures[TextureUsage.SamplerNormal]; // Create a copy of the normal that's the same size as the diffuse for purposes of copying the opacity across. var resizedNormal = normal.Clone(context => context.Resize(diffuse.Width, diffuse.Height)); diffuse.ProcessPixelRows(resizedNormal, (diffuseAccessor, normalAccessor) => { - for (int y = 0; y < diffuseAccessor.Height; y++) + for (var y = 0; y < diffuseAccessor.Height; y++) { var diffuseSpan = diffuseAccessor.GetRowSpan(y); - var normalSpan = normalAccessor.GetRowSpan(y); + var normalSpan = normalAccessor.GetRowSpan(y); - for (int x = 0; x < diffuseSpan.Length; x++) - { + for (var x = 0; x < diffuseSpan.Length; x++) diffuseSpan[x].A = normalSpan[x].B; - } } }); // Clear the blue channel out of the normal now that we're done with it. normal.ProcessPixelRows(normalAccessor => { - for (int y = 0; y < normalAccessor.Height; y++) + for (var y = 0; y < normalAccessor.Height; y++) { var normalSpan = normalAccessor.GetRowSpan(y); - for (int x = 0; x < normalSpan.Length; x++) - { + for (var x = 0; x < normalSpan.Length; x++) normalSpan[x].B = byte.MaxValue; - } } }); return BuildSharedBase(material, name) .WithBaseColor(BuildImage(diffuse, name, "basecolor")) - .WithNormal(BuildImage(normal, name, "normal")) + .WithNormal(BuildImage(normal, name, "normal")) .WithAlpha(isFace ? AlphaMode.MASK : AlphaMode.OPAQUE, 0.5f); } @@ -357,8 +356,8 @@ public class MaterialExporter private static MaterialBuilder BuildSharedBase(Material material, string name) { // TODO: Move this and potentially the other known stuff into MtrlFile? - const uint backfaceMask = 0x1; - var showBackfaces = (material.Mtrl.ShaderPackage.Flags & backfaceMask) == 0; + const uint backfaceMask = 0x1; + var showBackfaces = (material.Mtrl.ShaderPackage.Flags & backfaceMask) == 0; return new MaterialBuilder(name) .WithDoubleSide(showBackfaces); diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index c6e2d836..8c6dc31a 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -21,11 +21,9 @@ namespace Penumbra.Import.Models; using Schema2 = SharpGLTF.Schema2; using LuminaMaterial = Lumina.Models.Materials.Material; -public sealed class ModelManager(IFramework framework, ActiveCollections collections, IDataManager gameData, GamePathParser parser, TextureManager textureManager) : SingleTaskQueue, IDisposable +public sealed class ModelManager(IFramework framework, ActiveCollections collections, GamePathParser parser) : SingleTaskQueue, IDisposable { - private readonly IFramework _framework = framework; - private readonly IDataManager _gameData = gameData; - private readonly TextureManager _textureManager = textureManager; + private readonly IFramework _framework = framework; private readonly ConcurrentDictionary _tasks = new(); @@ -45,13 +43,15 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect public Task ImportGltf(string inputPath) { var action = new ImportGltfAction(inputPath); - return Enqueue(action).ContinueWith(task => + return Enqueue(action).ContinueWith(task => { - if (task.IsFaulted && task.Exception != null) + if (task is { IsFaulted: true, Exception: not null }) throw task.Exception; + return action.Out; }); } + /// Try to find the .sklb paths for a .mdl file. /// .mdl file to look up the skeletons for. /// Modified extra skeleton template parameters. @@ -69,8 +69,8 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Body, info, estManipulations)], ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Head => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Head, info, estManipulations)], - ObjectType.Equipment => [baseSkeleton], - ObjectType.Accessory => [baseSkeleton], + ObjectType.Equipment => [baseSkeleton], + ObjectType.Accessory => [baseSkeleton], ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => [baseSkeleton], ObjectType.Character when info.BodySlot is BodySlot.Hair => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Hair, info, estManipulations)], @@ -89,17 +89,17 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect // Try to find an EST entry from the manipulations provided. var (gender, race) = info.GenderRace.Split(); var modEst = estManipulations - .FirstOrNull(est => + .FirstOrNull(est => est.Gender == gender - && est.Race == race - && est.Slot == type - && est.SetId == info.PrimaryId + && est.Race == race + && est.Slot == type + && est.SetId == info.PrimaryId ); - + // Try to use an entry from provided manipulations, falling back to the current collection. var targetId = modEst?.Entry - ?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId) - ?? 0; + ?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId) + ?? 0; // If there's no entries, we can assume that there's no additional skeleton. if (targetId == 0) @@ -121,11 +121,11 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect var relativePath = rawPath.StartsWith('/') ? rawPath : '/' + Path.GetFileName(rawPath); - + // TODO: this should be a recoverable warning if (absolutePath == null) throw new Exception("Failed to resolve material path."); - + var info = parser.GetFileInfo(absolutePath); if (info.FileType is not FileType.Material) throw new Exception($"Material path {rawPath} does not conform to material conventions."); @@ -168,7 +168,12 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect return task; } - private class ExportToGltfAction(ModelManager manager, MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) + private class ExportToGltfAction( + ModelManager manager, + MdlFile mdl, + IEnumerable sklbPaths, + Func read, + string outputPath) : IAction { public void Execute(CancellationToken cancel) @@ -213,21 +218,21 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect // finicky at the best of times, and can outright cause a CTD if they // get upset. Running each conversion on its own tick seems to make // this consistently non-crashy across my testing. - Task CreateHavokTask((SklbFile Sklb, int Index) pair) => - manager._framework.RunOnTick( + Task CreateHavokTask((SklbFile Sklb, int Index) pair) + => manager._framework.RunOnTick( () => HavokConverter.HkxToXml(pair.Sklb.Skeleton), delayTicks: pair.Index, cancellationToken: cancel); } - /// Read a .mtrl and hydrate its textures. + /// Read a .mtrl and populate its textures. private MaterialExporter.Material BuildMaterial(string relativePath, CancellationToken cancel) { var path = manager.ResolveMtrlPath(relativePath); var mtrl = new MtrlFile(read(path)); - + return new MaterialExporter.Material { - Mtrl = mtrl, + Mtrl = mtrl, Textures = mtrl.ShaderPackage.Samplers.ToDictionary( sampler => (TextureUsage)sampler.SamplerId, sampler => ConvertImage(mtrl.Textures[sampler.TextureIndex], cancel) @@ -242,21 +247,15 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect var texturePath = texture.Path; if (texture.DX11) { - var lastSlashIndex = texturePath.LastIndexOf('/'); - var directory = lastSlashIndex == -1 ? texturePath : texturePath.Substring(0, lastSlashIndex); - var fileName = Path.GetFileName(texturePath); + var fileName = Path.GetFileName(texturePath); if (!fileName.StartsWith("--")) - { - texturePath = $"{directory}/--{fileName}"; - } + texturePath = $"{Path.GetDirectoryName(texturePath)}/--{fileName}"; } using var textureData = new MemoryStream(read(texturePath)); - var image = TexFileParser.Parse(textureData); - var pngImage = TextureManager.ConvertToPng(image, cancel).AsPng; - if (pngImage == null) - throw new Exception("Failed to convert texture to png."); - return pngImage; + var image = TexFileParser.Parse(textureData); + var pngImage = TextureManager.ConvertToPng(image, cancel).AsPng; + return pngImage ?? throw new Exception("Failed to convert texture to png."); } public bool Equals(IAction? other) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 43a06012..9cfe0739 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -181,9 +181,9 @@ public partial class ModEditWindow } /// Merge attribute configuration from the source onto the target. - /// + /// Model that will be updated. > /// Model to copy attribute configuration from. - public void MergeAttributes(MdlFile target, MdlFile source) + public static void MergeAttributes(MdlFile target, MdlFile source) { target.Attributes = source.Attributes; @@ -197,7 +197,7 @@ public partial class ModEditWindow target.SubMeshes[subMeshIndex].AttributeIndexMask = 0u; // Rather than comparing sub-meshes directly, we're grouping by parent mesh in an attempt - // to maintain semantic connection betwen mesh index and submesh attributes. + // to maintain semantic connection between mesh index and sub mesh attributes. if (meshIndex >= source.Meshes.Length) continue; var sourceMesh = source.Meshes[meshIndex]; @@ -214,8 +214,8 @@ public partial class ModEditWindow { IoExceptions = exception switch { null => [], - AggregateException ae => ae.Flatten().InnerExceptions.ToList(), - Exception other => [other], + AggregateException ae => [.. ae.Flatten().InnerExceptions], + _ => [exception], }; } @@ -234,7 +234,7 @@ public partial class ModEditWindow ? _edit._gameData.GetFile(path)?.Data : File.ReadAllBytes(resolvedPath.Value.ToPath()); - // TODO: some callers may not care about failures - handle exceptions seperately? + // 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?"); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 4ac789ad..ad609285 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -129,7 +129,7 @@ public partial class ModEditWindow ); } - private void DrawIoExceptions(MdlTab tab) + private static void DrawIoExceptions(MdlTab tab) { if (tab.IoExceptions.Count == 0) return; @@ -140,16 +140,15 @@ public partial class ModEditWindow var spaceAvail = ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X - 100; foreach (var (exception, index) in tab.IoExceptions.WithIndex()) { - var message = $"{exception.GetType().Name}: {exception.Message}"; - var textSize = ImGui.CalcTextSize(message).X; + using var id = ImRaii.PushId(index); + var message = $"{exception.GetType().Name}: {exception.Message}"; + var textSize = ImGui.CalcTextSize(message).X; if (textSize > spaceAvail) - message = message.Substring(0, (int)Math.Floor(message.Length * (spaceAvail / textSize))) + "..."; + message = message[..(int)Math.Floor(message.Length * (spaceAvail / textSize))] + "..."; - using (var exceptionNode = ImRaii.TreeNode($"{message}###exception{index}")) - { - if (exceptionNode) - ImGuiUtil.TextWrapped(exception.ToString()); - } + using var exceptionNode = ImRaii.TreeNode(message); + if (exceptionNode) + ImGuiUtil.TextWrapped(exception.ToString()); } }