Cleanup pass

This commit is contained in:
ackwell 2024-01-14 20:12:17 +11:00
parent 5e6ca8b22c
commit 9ff3227cf4
3 changed files with 30 additions and 16 deletions

View file

@ -21,6 +21,7 @@ public class MaterialExporter
// variant? // variant?
} }
/// <summary> Build a glTF material from a hydrated XIV model, with the provided name. </summary>
public static MaterialBuilder Export(Material material, string name) public static MaterialBuilder Export(Material material, string name)
{ {
Penumbra.Log.Debug($"Exporting material \"{name}\"."); Penumbra.Log.Debug($"Exporting material \"{name}\".");
@ -36,16 +37,18 @@ public class MaterialExporter
}; };
} }
/// <summary> Build a material following the semantics of character.shpk. </summary>
private static MaterialBuilder BuildCharacter(Material material, string name) private static MaterialBuilder BuildCharacter(Material material, string name)
{ {
// Build the textures from the color table.
var table = material.Mtrl.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 normal = material.Textures[TextureUsage.SamplerNormal];
var operation = new ProcessCharacterNormalOperation(normal, table); var operation = new ProcessCharacterNormalOperation(normal, table);
ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds(), in operation); ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds(), in operation);
// Check if full textures are provided, and merge in if available.
Image baseColor = operation.BaseColor; Image baseColor = operation.BaseColor;
if (material.Textures.TryGetValue(TextureUsage.SamplerDiffuse, out var diffuse)) if (material.Textures.TryGetValue(TextureUsage.SamplerDiffuse, out var diffuse))
{ {
@ -53,7 +56,6 @@ public class MaterialExporter
baseColor = diffuse; baseColor = diffuse;
} }
// TODO: what about the two specularmaps?
Image specular = operation.Specular; Image specular = operation.Specular;
if (material.Textures.TryGetValue(TextureUsage.SamplerSpecular, out var specularTexture)) if (material.Textures.TryGetValue(TextureUsage.SamplerSpecular, out var specularTexture))
{ {
@ -61,6 +63,7 @@ public class MaterialExporter
specular = specularTexture; specular = specularTexture;
} }
// Pull further information from the mask.
Image? occlusion = null; Image? occlusion = null;
if (material.Textures.TryGetValue(TextureUsage.SamplerMask, out var maskTexture)) if (material.Textures.TryGetValue(TextureUsage.SamplerMask, out var maskTexture))
{ {
@ -73,7 +76,7 @@ public class MaterialExporter
0f, 0f, 0f, 0f 0f, 0f, 0f, 0f
))); )));
occlusion = maskTexture; occlusion = maskTexture;
// TODO: handle other textures stored in the mask? // TODO: handle other textures stored in the mask?
} }
@ -89,7 +92,7 @@ public class MaterialExporter
return materialBuilder; 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. // 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<Rgba32> normal, MtrlFile.ColorTable table) : IRowOperation private readonly struct ProcessCharacterNormalOperation(Image<Rgba32> normal, MtrlFile.ColorTable table) : IRowOperation
{ {
@ -143,7 +146,7 @@ public class MaterialExporter
private static TableRow GetTableRowIndices(float input) private static TableRow GetTableRowIndices(float input)
{ {
// These calculations are ported from character.shpk. // 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 + MathF.Floor(input * 15 + 0.5f))
+ input * 15; + 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 _defaultHairColor = new Vector4(130, 64, 13, 255) / new Vector4(255);
private static Vector4 _defaultHighlightColor = new Vector4(77, 126, 240, 255) / new Vector4(255); private static Vector4 _defaultHighlightColor = new Vector4(77, 126, 240, 255) / new Vector4(255);
/// <summary> Build a material following the semantics of hair.shpk. </summary>
private static MaterialBuilder BuildHair(Material material, string name) private static MaterialBuilder BuildHair(Material material, string name)
{ {
// Trust me bro. // Trust me bro.
@ -241,11 +245,12 @@ public class MaterialExporter
return BuildSharedBase(material, name) return BuildSharedBase(material, name)
.WithBaseColor(BuildImage(baseColor, name, "basecolor")) .WithBaseColor(BuildImage(baseColor, name, "basecolor"))
.WithNormal(BuildImage(normal, name, "normal")) .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); private static Vector4 _defaultEyeColor = new Vector4(21, 176, 172, 255) / new Vector4(255);
/// <summary> Build a material following the semantics of iris.shpk. </summary>
// 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 seperate for now.
private static MaterialBuilder BuildIris(Material material, string name) private static MaterialBuilder BuildIris(Material material, string name)
{ {
@ -278,6 +283,7 @@ public class MaterialExporter
.WithNormal(BuildImage(normal, name, "normal")); .WithNormal(BuildImage(normal, name, "normal"));
} }
/// <summary> Build a material following the semantics of skin.shpk. </summary>
private static MaterialBuilder BuildSkin(Material material, string name) private static MaterialBuilder BuildSkin(Material material, string name)
{ {
// Trust me bro. // 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. // 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++) for (int y = 0; y < normalAccessor.Height; y++)
{ {
var normalSpan = normalAccessor.GetRowSpan(y); var normalSpan = normalAccessor.GetRowSpan(y);
@ -325,14 +332,16 @@ public class MaterialExporter
return BuildSharedBase(material, name) return BuildSharedBase(material, name)
.WithBaseColor(BuildImage(diffuse, name, "basecolor")) .WithBaseColor(BuildImage(diffuse, name, "basecolor"))
.WithNormal(BuildImage(normal, name, "normal")) .WithNormal(BuildImage(normal, name, "normal"))
.WithAlpha(isFace? AlphaMode.MASK : AlphaMode.OPAQUE, 0.5f); .WithAlpha(isFace ? AlphaMode.MASK : AlphaMode.OPAQUE, 0.5f);
} }
/// <summary> Build a material from a source with unknown semantics. </summary>
/// <remarks> Will make a loose effort to fetch common / simple textures. </remarks>
private static MaterialBuilder BuildFallback(Material material, string name) private static MaterialBuilder BuildFallback(Material material, string name)
{ {
Penumbra.Log.Warning($"Unhandled shader package: {material.Mtrl.ShaderPackage.Name}"); Penumbra.Log.Warning($"Unhandled shader package: {material.Mtrl.ShaderPackage.Name}");
var materialBuilder = BuildSharedBase(material, name) var materialBuilder = BuildSharedBase(material, name)
.WithMetallicRoughnessShader() .WithMetallicRoughnessShader()
.WithBaseColor(Vector4.One); .WithBaseColor(Vector4.One);
@ -345,6 +354,7 @@ public class MaterialExporter
return materialBuilder; return materialBuilder;
} }
/// <summary> Build a material pre-configured with settings common to all XIV materials/shaders. </summary>
private static MaterialBuilder BuildSharedBase(Material material, string name) private static MaterialBuilder BuildSharedBase(Material material, string name)
{ {
// TODO: Move this and potentially the other known stuff into MtrlFile? // TODO: Move this and potentially the other known stuff into MtrlFile?
@ -355,6 +365,7 @@ public class MaterialExporter
.WithDoubleSide(showBackfaces); .WithDoubleSide(showBackfaces);
} }
/// <summary> Convert an ImageSharp Image into an ImageBuilder for use with SharpGLTF. </summary>
private static ImageBuilder BuildImage(Image image, string materialName, string suffix) private static ImageBuilder BuildImage(Image image, string materialName, string suffix)
{ {
var name = materialName.Replace("/", "").Replace(".mtrl", "") + $"_{suffix}"; var name = materialName.Replace("/", "").Replace(".mtrl", "") + $"_{suffix}";

View file

@ -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 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 IDataManager _gameData = gameData;
private readonly TextureManager _textureManager = textureManager; private readonly TextureManager _textureManager = textureManager;
private readonly ConcurrentDictionary<IAction, (Task, CancellationTokenSource)> _tasks = new(); private readonly ConcurrentDictionary<IAction, (Task, CancellationTokenSource)> _tasks = new();
@ -163,7 +163,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
Penumbra.Log.Debug("[GLTF Export] Done."); Penumbra.Log.Debug("[GLTF Export] Done.");
} }
/// <summary> Attempt to read out the pertinent information from a .sklb. </summary> /// <summary> Attempt to read out the pertinent information from the sklb file paths provided. </summary>
private IEnumerable<XivSkeleton> BuildSkeletons(CancellationToken cancel) private IEnumerable<XivSkeleton> BuildSkeletons(CancellationToken cancel)
{ {
var havokTasks = sklbPaths var havokTasks = sklbPaths
@ -185,6 +185,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
delayTicks: pair.Index, cancellationToken: cancel); delayTicks: pair.Index, cancellationToken: cancel);
} }
/// <summary> Read a .mtrl and hydrate its textures. </summary>
private MaterialExporter.Material BuildMaterial(string relativePath, CancellationToken cancel) private MaterialExporter.Material BuildMaterial(string relativePath, CancellationToken cancel)
{ {
// TODO: this should probably be chosen in the export settings // 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) ? LuminaMaterial.ResolveRelativeMaterialPath(relativePath, variantId)
: relativePath; : 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) if (absolutePath == null)
throw new Exception("Failed to resolve material path."); throw new Exception("Failed to resolve material path.");
@ -202,7 +203,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
return new MaterialExporter.Material return new MaterialExporter.Material
{ {
Mtrl = mtrl, Mtrl = mtrl,
Textures = mtrl.ShaderPackage.Samplers.ToDictionary( Textures = mtrl.ShaderPackage.Samplers.ToDictionary(
sampler => (TextureUsage)sampler.SamplerId, sampler => (TextureUsage)sampler.SamplerId,
sampler => ConvertImage(mtrl.Textures[sampler.TextureIndex], cancel) sampler => ConvertImage(mtrl.Textures[sampler.TextureIndex], cancel)
@ -210,6 +211,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
}; };
} }
/// <summary> Read a texture referenced by a .mtrl and convert it into an ImageSharp image. </summary>
private Image<Rgba32> ConvertImage(MtrlFile.Texture texture, CancellationToken cancel) private Image<Rgba32> ConvertImage(MtrlFile.Texture texture, CancellationToken cancel)
{ {
using var textureData = new MemoryStream(read(texture.Path)); using var textureData = new MemoryStream(read(texture.Path));

View file

@ -225,15 +225,16 @@ public partial class ModEditWindow
private byte[] ReadFile(string path) private byte[] ReadFile(string path)
{ {
// TODO: if cross-collection lookups are turned off, this conversion can be skipped // 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."); 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... // 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 var bytes = resolvedPath == null
? _edit._gameData.GetFile(path)?.Data ? _edit._gameData.GetFile(path)?.Data
: File.ReadAllBytes(resolvedPath.Value.ToPath()); : File.ReadAllBytes(resolvedPath.Value.ToPath());
// TODO: some callers may not care about failures - handle exceptions seperately?
return bytes ?? throw new Exception( return bytes ?? throw new Exception(
$"Resolved path {path} could not be found. If modded, is it enabled in the current collection?"); $"Resolved path {path} could not be found. If modded, is it enabled in the current collection?");
} }