mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-15 13:14:17 +01:00
Spike material export workflow
This commit is contained in:
parent
be588e2fa3
commit
c6642c4fa3
3 changed files with 149 additions and 15 deletions
81
Penumbra/Import/Models/Export/MaterialExporter.cs
Normal file
81
Penumbra/Import/Models/Export/MaterialExporter.cs
Normal file
|
|
@ -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<Rgba32> 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<Rgba32> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,10 +23,10 @@ public class ModelExporter
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Export a model in preparation for usage in a glTF file. If provided, skeleton will be used to skin the resulting meshes where appropriate. </summary>
|
/// <summary> Export a model in preparation for usage in a glTF file. If provided, skeleton will be used to skin the resulting meshes where appropriate. </summary>
|
||||||
public static Model Export(MdlFile mdl, IEnumerable<XivSkeleton>? xivSkeleton)
|
public static Model Export(MdlFile mdl, IEnumerable<XivSkeleton>? xivSkeleton, Dictionary<string, MaterialExporter.Material> rawMaterials)
|
||||||
{
|
{
|
||||||
var gltfSkeleton = xivSkeleton != null ? ConvertSkeleton(xivSkeleton) : null;
|
var gltfSkeleton = xivSkeleton != null ? ConvertSkeleton(xivSkeleton) : null;
|
||||||
var materials = ConvertMaterials(mdl);
|
var materials = ConvertMaterials(mdl, rawMaterials);
|
||||||
var meshes = ConvertMeshes(mdl, materials, gltfSkeleton);
|
var meshes = ConvertMeshes(mdl, materials, gltfSkeleton);
|
||||||
return new Model(meshes, gltfSkeleton);
|
return new Model(meshes, gltfSkeleton);
|
||||||
}
|
}
|
||||||
|
|
@ -51,16 +51,12 @@ public class ModelExporter
|
||||||
return meshes;
|
return meshes;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Compose textures for use with these materials
|
/// <summary> Build materials for each of the material slots in the .mdl. </summary>
|
||||||
/// <summary> Build placeholder materials for each of the material slots in the .mdl. </summary>
|
private static MaterialBuilder[] ConvertMaterials(MdlFile mdl, Dictionary<string, MaterialExporter.Material> rawMaterials)
|
||||||
private static MaterialBuilder[] ConvertMaterials(MdlFile mdl)
|
|
||||||
=> mdl.Materials
|
=> mdl.Materials
|
||||||
.Select(name =>
|
// TODO: material generation should be fallible, which means this lookup should be a tryget, with a fallback.
|
||||||
new MaterialBuilder(name)
|
// fallback can likely be a static on the material exporter.
|
||||||
.WithMetallicRoughnessShader()
|
.Select(name => MaterialExporter.Export(rawMaterials[name], name))
|
||||||
.WithDoubleSide(true)
|
|
||||||
.WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, Vector4.One)
|
|
||||||
)
|
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
/// <summary> Convert XIV skeleton data into a glTF-compatible node tree, with mappings. </summary>
|
/// <summary> Convert XIV skeleton data into a glTF-compatible node tree, with mappings. </summary>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
|
using Lumina.Data.Parsing;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using OtterGui.Tasks;
|
using OtterGui.Tasks;
|
||||||
using Penumbra.Collections.Manager;
|
using Penumbra.Collections.Manager;
|
||||||
|
|
@ -9,15 +10,22 @@ using Penumbra.GameData.Files;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
using Penumbra.Import.Models.Export;
|
using Penumbra.Import.Models.Export;
|
||||||
using Penumbra.Import.Models.Import;
|
using Penumbra.Import.Models.Import;
|
||||||
|
using Penumbra.Import.Textures;
|
||||||
using Penumbra.Meta.Manipulations;
|
using Penumbra.Meta.Manipulations;
|
||||||
using SharpGLTF.Scenes;
|
using SharpGLTF.Scenes;
|
||||||
using SharpGLTF.Schema2;
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
|
||||||
namespace Penumbra.Import.Models;
|
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 IFramework _framework = framework;
|
||||||
|
private readonly IDataManager _gameData = gameData;
|
||||||
|
private readonly TextureManager _textureManager = textureManager;
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<IAction, (Task, CancellationTokenSource)> _tasks = new();
|
private readonly ConcurrentDictionary<IAction, (Task, CancellationTokenSource)> _tasks = new();
|
||||||
|
|
||||||
|
|
@ -132,11 +140,18 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
|
||||||
public void Execute(CancellationToken cancel)
|
public void Execute(CancellationToken cancel)
|
||||||
{
|
{
|
||||||
Penumbra.Log.Debug($"[GLTF Export] Exporting model to {outputPath}...");
|
Penumbra.Log.Debug($"[GLTF Export] Exporting model to {outputPath}...");
|
||||||
|
|
||||||
Penumbra.Log.Debug("[GLTF Export] Reading skeletons...");
|
Penumbra.Log.Debug("[GLTF Export] Reading skeletons...");
|
||||||
var xivSkeletons = BuildSkeletons(cancel);
|
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...");
|
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...");
|
Penumbra.Log.Debug("[GLTF Export] Building scene...");
|
||||||
var scene = new SceneBuilder();
|
var scene = new SceneBuilder();
|
||||||
|
|
@ -169,6 +184,48 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
|
||||||
delayTicks: pair.Index, cancellationToken: cancel);
|
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<Rgba32> 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)
|
public bool Equals(IAction? other)
|
||||||
{
|
{
|
||||||
if (other is not ExportToGltfAction rhs)
|
if (other is not ExportToGltfAction rhs)
|
||||||
|
|
@ -185,7 +242,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
|
||||||
|
|
||||||
public void Execute(CancellationToken cancel)
|
public void Execute(CancellationToken cancel)
|
||||||
{
|
{
|
||||||
var model = ModelRoot.Load(inputPath);
|
var model = Schema2.ModelRoot.Load(inputPath);
|
||||||
|
|
||||||
Out = ModelImporter.Import(model);
|
Out = ModelImporter.Import(model);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue