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