mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Make material export fallible
This commit is contained in:
parent
aa01acd76a
commit
0486d049b0
4 changed files with 54 additions and 24 deletions
|
|
@ -22,6 +22,12 @@ public class MaterialExporter
|
|||
// variant?
|
||||
}
|
||||
|
||||
/// <summary> Dependency-less material configuration, for use when no material data can be resolved. </summary>
|
||||
public static readonly MaterialBuilder Unknown = new MaterialBuilder("UNKNOWN")
|
||||
.WithMetallicRoughnessShader()
|
||||
.WithDoubleSide(true)
|
||||
.WithBaseColor(Vector4.One);
|
||||
|
||||
/// <summary> Build a glTF material from a hydrated XIV model, with the provided name. </summary>
|
||||
public static MaterialBuilder Export(Material material, string name, IoNotifier notifier)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -55,9 +55,14 @@ public class ModelExporter
|
|||
/// <summary> Build materials for each of the material slots in the .mdl. </summary>
|
||||
private static MaterialBuilder[] ConvertMaterials(MdlFile mdl, Dictionary<string, MaterialExporter.Material> rawMaterials, IoNotifier notifier)
|
||||
=> mdl.Materials
|
||||
// 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, notifier.WithContext($"Material {name}")))
|
||||
.Select(name =>
|
||||
{
|
||||
if (rawMaterials.TryGetValue(name, out var rawMaterial))
|
||||
return MaterialExporter.Export(rawMaterial, name, notifier.WithContext($"Material {name}"));
|
||||
|
||||
notifier.Warning($"Material \"{name}\" missing, using blank fallback.");
|
||||
return MaterialExporter.Unknown;
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
/// <summary> Convert XIV skeleton data into a glTF-compatible node tree, with mappings. </summary>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
|
|||
_tasks.Clear();
|
||||
}
|
||||
|
||||
public Task<IoNotifier> ExportToGltf(MdlFile mdl, IEnumerable<string> sklbPaths, Func<string, byte[]> read, string outputPath)
|
||||
public Task<IoNotifier> ExportToGltf(MdlFile mdl, IEnumerable<string> sklbPaths, Func<string, byte[]?> read, string outputPath)
|
||||
=> EnqueueWithResult(
|
||||
new ExportToGltfAction(this, mdl, sklbPaths, read, outputPath),
|
||||
action => action.Notifier
|
||||
|
|
@ -106,7 +106,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
|
|||
}
|
||||
|
||||
/// <summary> Try to resolve the absolute path to a .mtrl from the potentially-partial path provided by a model. </summary>
|
||||
private string ResolveMtrlPath(string rawPath)
|
||||
private string? ResolveMtrlPath(string rawPath, IoNotifier notifier)
|
||||
{
|
||||
// TODO: this should probably be chosen in the export settings
|
||||
var variantId = 1;
|
||||
|
|
@ -119,13 +119,18 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
|
|||
? rawPath
|
||||
: '/' + Path.GetFileName(rawPath);
|
||||
|
||||
// TODO: this should be a recoverable warning
|
||||
if (absolutePath == null)
|
||||
throw new Exception("Failed to resolve material path.");
|
||||
{
|
||||
notifier.Warning($"Material path \"{rawPath}\" could not be resolved.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var info = parser.GetFileInfo(absolutePath);
|
||||
if (info.FileType is not FileType.Material)
|
||||
throw new Exception($"Material path {rawPath} does not conform to material conventions.");
|
||||
{
|
||||
notifier.Warning($"Material path {rawPath} does not conform to material conventions.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var resolvedPath = info.ObjectType switch
|
||||
{
|
||||
|
|
@ -179,7 +184,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
|
|||
ModelManager manager,
|
||||
MdlFile mdl,
|
||||
IEnumerable<string> sklbPaths,
|
||||
Func<string, byte[]> read,
|
||||
Func<string, byte[]?> read,
|
||||
string outputPath)
|
||||
: IAction
|
||||
{
|
||||
|
|
@ -193,10 +198,10 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
|
|||
var xivSkeletons = BuildSkeletons(cancel);
|
||||
|
||||
Penumbra.Log.Debug("[GLTF Export] Reading materials...");
|
||||
var materials = mdl.Materials.ToDictionary(
|
||||
path => path,
|
||||
path => BuildMaterial(path, cancel)
|
||||
);
|
||||
var materials = mdl.Materials
|
||||
.Select(path => (path, material: BuildMaterial(path, Notifier, cancel)))
|
||||
.Where(pair => pair.material != null)
|
||||
.ToDictionary(pair => pair.path, pair => pair.material!.Value);
|
||||
|
||||
Penumbra.Log.Debug("[GLTF Export] Converting model...");
|
||||
var model = ModelExporter.Export(mdl, xivSkeletons, materials, Notifier);
|
||||
|
|
@ -215,7 +220,9 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
|
|||
private IEnumerable<XivSkeleton> BuildSkeletons(CancellationToken cancel)
|
||||
{
|
||||
var havokTasks = sklbPaths
|
||||
.Select(path => new SklbFile(read(path)))
|
||||
.Select(path => read(path) ?? throw new Exception(
|
||||
$"Resolved skeleton \"{path}\" could not be read. Ensure EST metadata is configured, and/or relevant mods are enabled in the current collection."))
|
||||
.Select(bytes => new SklbFile(bytes))
|
||||
.WithIndex()
|
||||
.Select(CreateHavokTask)
|
||||
.ToArray();
|
||||
|
|
@ -234,10 +241,15 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
|
|||
}
|
||||
|
||||
/// <summary> Read a .mtrl and populate its textures. </summary>
|
||||
private MaterialExporter.Material BuildMaterial(string relativePath, CancellationToken cancel)
|
||||
private MaterialExporter.Material? BuildMaterial(string relativePath, IoNotifier notifier, CancellationToken cancel)
|
||||
{
|
||||
var path = manager.ResolveMtrlPath(relativePath);
|
||||
var mtrl = new MtrlFile(read(path));
|
||||
var path = manager.ResolveMtrlPath(relativePath, notifier);
|
||||
if (path == null)
|
||||
return null;
|
||||
var bytes = read(path);
|
||||
if (bytes == null)
|
||||
return null;
|
||||
var mtrl = new MtrlFile(bytes);
|
||||
|
||||
return new MaterialExporter.Material
|
||||
{
|
||||
|
|
@ -254,12 +266,23 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
|
|||
{
|
||||
// Work out the texture's path - the DX11 material flag controls a file name prefix.
|
||||
GamePaths.Tex.HandleDx11Path(texture, out var texturePath);
|
||||
using var textureData = new MemoryStream(read(texturePath));
|
||||
var bytes = read(texturePath);
|
||||
if (bytes == null)
|
||||
return CreateDummyImage();
|
||||
|
||||
using var textureData = new MemoryStream(bytes);
|
||||
var image = TexFileParser.Parse(textureData);
|
||||
var pngImage = TextureManager.ConvertToPng(image, cancel).AsPng;
|
||||
return pngImage ?? throw new Exception("Failed to convert texture to png.");
|
||||
}
|
||||
|
||||
private Image<Rgba32> CreateDummyImage()
|
||||
{
|
||||
var image = new Image<Rgba32>(1, 1);
|
||||
image[0, 0] = Color.White;
|
||||
return image;
|
||||
}
|
||||
|
||||
public bool Equals(IAction? other)
|
||||
{
|
||||
if (other is not ExportToGltfAction rhs)
|
||||
|
|
|
|||
|
|
@ -260,7 +260,7 @@ public partial class ModEditWindow
|
|||
/// <summary> Read a file from the active collection or game. </summary>
|
||||
/// <param name="path"> Game path to the file to load. </param>
|
||||
// TODO: Also look up files within the current mod regardless of mod state?
|
||||
private byte[] ReadFile(string path)
|
||||
private byte[]? ReadFile(string path)
|
||||
{
|
||||
// TODO: if cross-collection lookups are turned off, this conversion can be skipped
|
||||
if (!Utf8GamePath.FromString(path, out var utf8Path, true))
|
||||
|
|
@ -269,13 +269,9 @@ public partial class ModEditWindow
|
|||
var resolvedPath = _edit._activeCollections.Current.ResolvePath(utf8Path) ?? new FullPath(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.IsRooted
|
||||
return resolvedPath.IsRooted
|
||||
? File.ReadAllBytes(resolvedPath.FullName)
|
||||
: _edit._gameData.GetFile(resolvedPath.InternalName.ToString())?.Data;
|
||||
|
||||
// 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?");
|
||||
}
|
||||
|
||||
/// <summary> Remove the material given by the index. </summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue