From c6642c4fa30eaee0f10dc1cbd208101d76b33630 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 13 Jan 2024 11:32:26 +1100 Subject: [PATCH] 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); }