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