Minor cleanup.

This commit is contained in:
Ottermandias 2024-01-14 13:35:46 +01:00
parent 7c83e30e9f
commit 3b9d841014
4 changed files with 115 additions and 118 deletions

View file

@ -17,6 +17,7 @@ public class MaterialExporter
public struct Material public struct Material
{ {
public MtrlFile Mtrl; public MtrlFile Mtrl;
public Dictionary<TextureUsage, Image<Rgba32>> Textures; public Dictionary<TextureUsage, Image<Rgba32>> Textures;
// variant? // variant?
} }
@ -49,7 +50,7 @@ public class MaterialExporter
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. // Check if full textures are provided, and merge in if available.
Image<Rgba32> baseColor = operation.BaseColor; var baseColor = operation.BaseColor;
if (material.Textures.TryGetValue(TextureUsage.SamplerDiffuse, out var diffuse)) if (material.Textures.TryGetValue(TextureUsage.SamplerDiffuse, out var diffuse))
{ {
MultiplyOperation.Execute(diffuse, operation.BaseColor); MultiplyOperation.Execute(diffuse, operation.BaseColor);
@ -70,24 +71,22 @@ public class MaterialExporter
maskTexture.Mutate(context => context.Resize(baseColor.Width, baseColor.Height)); maskTexture.Mutate(context => context.Resize(baseColor.Width, baseColor.Height));
maskTexture.ProcessPixelRows(baseColor, (maskAccessor, baseColorAccessor) => maskTexture.ProcessPixelRows(baseColor, (maskAccessor, baseColorAccessor) =>
{ {
for (int y = 0; y < maskAccessor.Height; y++) for (var y = 0; y < maskAccessor.Height; y++)
{ {
var maskSpan = maskAccessor.GetRowSpan(y); var maskSpan = maskAccessor.GetRowSpan(y);
var baseColorSpan = baseColorAccessor.GetRowSpan(y); var baseColorSpan = baseColorAccessor.GetRowSpan(y);
for (int x = 0; x < maskSpan.Length; x++) for (var x = 0; x < maskSpan.Length; x++)
baseColorSpan[x].FromVector4(baseColorSpan[x].ToVector4() * new Vector4(maskSpan[x].R / 255f)); baseColorSpan[x].FromVector4(baseColorSpan[x].ToVector4() * new Vector4(maskSpan[x].R / 255f));
} }
}); });
// TODO: handle other textures stored in the mask? // TODO: handle other textures stored in the mask?
} }
return BuildSharedBase(material, name) return BuildSharedBase(material, name)
.WithBaseColor(BuildImage(baseColor, name, "basecolor")) .WithBaseColor(BuildImage(baseColor, name, "basecolor"))
.WithNormal(BuildImage(operation.Normal, name, "normal")) .WithNormal(BuildImage(operation.Normal, name, "normal"))
.WithSpecularColor(BuildImage(specular, name, "specular")) .WithSpecularColor(BuildImage(specular, name, "specular"))
.WithEmissive(BuildImage(operation.Emissive, name, "emissive"), Vector3.One, 1); .WithEmissive(BuildImage(operation.Emissive, name, "emissive"), Vector3.One, 1);
} }
@ -95,31 +94,38 @@ public class MaterialExporter
// 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
{ {
public Image<Rgba32> Normal { get; private init; } = normal.Clone(); public Image<Rgba32> Normal { get; } = normal.Clone();
public Image<Rgba32> BaseColor { get; private init; } = new Image<Rgba32>(normal.Width, normal.Height); public Image<Rgba32> BaseColor { get; } = new(normal.Width, normal.Height);
public Image<Rgb24> Specular { get; private init; } = new Image<Rgb24>(normal.Width, normal.Height); public Image<Rgb24> Specular { get; } = new(normal.Width, normal.Height);
public Image<Rgb24> Emissive { get; private init; } = new Image<Rgb24>(normal.Width, normal.Height); public Image<Rgb24> Emissive { get; } = new(normal.Width, normal.Height);
private Buffer2D<Rgba32> NormalBuffer => Normal.Frames.RootFrame.PixelBuffer; private Buffer2D<Rgba32> NormalBuffer
private Buffer2D<Rgba32> BaseColorBuffer => BaseColor.Frames.RootFrame.PixelBuffer; => Normal.Frames.RootFrame.PixelBuffer;
private Buffer2D<Rgb24> SpecularBuffer => Specular.Frames.RootFrame.PixelBuffer;
private Buffer2D<Rgb24> EmissiveBuffer => Emissive.Frames.RootFrame.PixelBuffer; private Buffer2D<Rgba32> BaseColorBuffer
=> BaseColor.Frames.RootFrame.PixelBuffer;
private Buffer2D<Rgb24> SpecularBuffer
=> Specular.Frames.RootFrame.PixelBuffer;
private Buffer2D<Rgb24> EmissiveBuffer
=> Emissive.Frames.RootFrame.PixelBuffer;
public void Invoke(int y) public void Invoke(int y)
{ {
var normalSpan = NormalBuffer.DangerousGetRowSpan(y); var normalSpan = NormalBuffer.DangerousGetRowSpan(y);
var baseColorSpan = BaseColorBuffer.DangerousGetRowSpan(y); var baseColorSpan = BaseColorBuffer.DangerousGetRowSpan(y);
var specularSpan = SpecularBuffer.DangerousGetRowSpan(y); var specularSpan = SpecularBuffer.DangerousGetRowSpan(y);
var emissiveSpan = EmissiveBuffer.DangerousGetRowSpan(y); var emissiveSpan = EmissiveBuffer.DangerousGetRowSpan(y);
for (int x = 0; x < normalSpan.Length; x++) for (var x = 0; x < normalSpan.Length; x++)
{ {
ref var normalPixel = ref normalSpan[x]; ref var normalPixel = ref normalSpan[x];
// Table row data (.a) // Table row data (.a)
var tableRow = GetTableRowIndices(normalPixel.A / 255f); var tableRow = GetTableRowIndices(normalPixel.A / 255f);
var prevRow = table[tableRow.Previous]; var prevRow = table[tableRow.Previous];
var nextRow = table[tableRow.Next]; var nextRow = table[tableRow.Next];
// Base colour (table, .b) // Base colour (table, .b)
var lerpedDiffuse = Vector3.Lerp(prevRow.Diffuse, nextRow.Diffuse, tableRow.Weight); var lerpedDiffuse = Vector3.Lerp(prevRow.Diffuse, nextRow.Diffuse, tableRow.Weight);
@ -145,9 +151,9 @@ 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;
var stepped = MathF.Floor(smoothed + 0.5f); var stepped = MathF.Floor(smoothed + 0.5f);
@ -162,9 +168,9 @@ public class MaterialExporter
private ref struct TableRow private ref struct TableRow
{ {
public int Stepped; public int Stepped;
public int Previous; public int Previous;
public int Next; public int Next;
public float Weight; public float Weight;
} }
@ -189,50 +195,47 @@ public class MaterialExporter
where TPixel1 : unmanaged, IPixel<TPixel1> where TPixel1 : unmanaged, IPixel<TPixel1>
where TPixel2 : unmanaged, IPixel<TPixel2> where TPixel2 : unmanaged, IPixel<TPixel2>
{ {
public void Invoke(int y) public void Invoke(int y)
{ {
var targetSpan = target.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); var targetSpan = target.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
var multiplierSpan = multiplier.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); var multiplierSpan = multiplier.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
for (int x = 0; x < targetSpan.Length; x++) for (var x = 0; x < targetSpan.Length; x++)
{
targetSpan[x].FromVector4(targetSpan[x].ToVector4() * multiplierSpan[x].ToVector4()); targetSpan[x].FromVector4(targetSpan[x].ToVector4() * multiplierSpan[x].ToVector4());
}
} }
} }
// TODO: These are hardcoded colours - I'm not keen on supporting highly customiseable exports, but there's possibly some more sensible values to use here. // TODO: These are hardcoded colours - I'm not keen on supporting highly customizable exports, but there's possibly some more sensible values to use here.
private static Vector4 _defaultHairColor = new Vector4(130, 64, 13, 255) / new Vector4(255); private static readonly 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 readonly Vector4 DefaultHighlightColor = new Vector4(77, 126, 240, 255) / new Vector4(255);
/// <summary> Build a material following the semantics of hair.shpk. </summary> /// <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.
const uint categoryHairType = 0x24826489; const uint categoryHairType = 0x24826489;
const uint valueFace = 0x6E5B8F10; const uint valueFace = 0x6E5B8F10;
var isFace = material.Mtrl.ShaderPackage.ShaderKeys var isFace = material.Mtrl.ShaderPackage.ShaderKeys
.Any(key => key.Category == categoryHairType && key.Value == valueFace); .Any(key => key is { Category: categoryHairType, Value: valueFace });
var normal = material.Textures[TextureUsage.SamplerNormal]; var normal = material.Textures[TextureUsage.SamplerNormal];
var mask = material.Textures[TextureUsage.SamplerMask]; var mask = material.Textures[TextureUsage.SamplerMask];
mask.Mutate(context => context.Resize(normal.Width, normal.Height)); mask.Mutate(context => context.Resize(normal.Width, normal.Height));
var baseColor = new Image<Rgba32>(normal.Width, normal.Height); var baseColor = new Image<Rgba32>(normal.Width, normal.Height);
normal.ProcessPixelRows(mask, baseColor, (normalAccessor, maskAccessor, baseColorAccessor) => normal.ProcessPixelRows(mask, baseColor, (normalAccessor, maskAccessor, baseColorAccessor) =>
{ {
for (int y = 0; y < normalAccessor.Height; y++) for (var y = 0; y < normalAccessor.Height; y++)
{ {
var normalSpan = normalAccessor.GetRowSpan(y); var normalSpan = normalAccessor.GetRowSpan(y);
var maskSpan = maskAccessor.GetRowSpan(y); var maskSpan = maskAccessor.GetRowSpan(y);
var baseColorSpan = baseColorAccessor.GetRowSpan(y); var baseColorSpan = baseColorAccessor.GetRowSpan(y);
for (int x = 0; x < normalSpan.Length; x++) for (var x = 0; x < normalSpan.Length; x++)
{ {
var color = Vector4.Lerp(_defaultHairColor, _defaultHighlightColor, maskSpan[x].A / 255f); var color = Vector4.Lerp(DefaultHairColor, DefaultHighlightColor, maskSpan[x].A / 255f);
baseColorSpan[x].FromVector4(color * new Vector4(maskSpan[x].R / 255f)); baseColorSpan[x].FromVector4(color * new Vector4(maskSpan[x].R / 255f));
baseColorSpan[x].A = normalSpan[x].A; baseColorSpan[x].A = normalSpan[x].A;
@ -243,33 +246,33 @@ 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 readonly Vector4 DefaultEyeColor = new Vector4(21, 176, 172, 255) / new Vector4(255);
/// <summary> Build a material following the semantics of iris.shpk. </summary> /// <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 separate for now.
private static MaterialBuilder BuildIris(Material material, string name) private static MaterialBuilder BuildIris(Material material, string name)
{ {
var normal = material.Textures[TextureUsage.SamplerNormal]; var normal = material.Textures[TextureUsage.SamplerNormal];
var mask = material.Textures[TextureUsage.SamplerMask]; var mask = material.Textures[TextureUsage.SamplerMask];
mask.Mutate(context => context.Resize(normal.Width, normal.Height)); mask.Mutate(context => context.Resize(normal.Width, normal.Height));
var baseColor = new Image<Rgba32>(normal.Width, normal.Height); var baseColor = new Image<Rgba32>(normal.Width, normal.Height);
normal.ProcessPixelRows(mask, baseColor, (normalAccessor, maskAccessor, baseColorAccessor) => normal.ProcessPixelRows(mask, baseColor, (normalAccessor, maskAccessor, baseColorAccessor) =>
{ {
for (int y = 0; y < normalAccessor.Height; y++) for (var y = 0; y < normalAccessor.Height; y++)
{ {
var normalSpan = normalAccessor.GetRowSpan(y); var normalSpan = normalAccessor.GetRowSpan(y);
var maskSpan = maskAccessor.GetRowSpan(y); var maskSpan = maskAccessor.GetRowSpan(y);
var baseColorSpan = baseColorAccessor.GetRowSpan(y); var baseColorSpan = baseColorAccessor.GetRowSpan(y);
for (int x = 0; x < normalSpan.Length; x++) for (var x = 0; x < normalSpan.Length; x++)
{ {
baseColorSpan[x].FromVector4(_defaultEyeColor * new Vector4(maskSpan[x].R / 255f)); baseColorSpan[x].FromVector4(DefaultEyeColor * new Vector4(maskSpan[x].R / 255f));
baseColorSpan[x].A = normalSpan[x].A; baseColorSpan[x].A = normalSpan[x].A;
normalSpan[x].A = byte.MaxValue; normalSpan[x].A = byte.MaxValue;
@ -279,7 +282,7 @@ 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"));
} }
/// <summary> Build a material following the semantics of skin.shpk. </summary> /// <summary> Build a material following the semantics of skin.shpk. </summary>
@ -287,7 +290,7 @@ public class MaterialExporter
{ {
// Trust me bro. // Trust me bro.
const uint categorySkinType = 0x380CAED0; const uint categorySkinType = 0x380CAED0;
const uint valueFace = 0xF5673524; const uint valueFace = 0xF5673524;
// Face is the default for the skin shader, so a lack of skin type category is also correct. // Face is the default for the skin shader, so a lack of skin type category is also correct.
var isFace = !material.Mtrl.ShaderPackage.ShaderKeys var isFace = !material.Mtrl.ShaderPackage.ShaderKeys
@ -296,41 +299,37 @@ public class MaterialExporter
// TODO: There's more nuance to skin than this, but this should be enough for a baseline reference. // TODO: There's more nuance to skin than this, but this should be enough for a baseline reference.
// TODO: Specular? // TODO: Specular?
var diffuse = material.Textures[TextureUsage.SamplerDiffuse]; var diffuse = material.Textures[TextureUsage.SamplerDiffuse];
var normal = material.Textures[TextureUsage.SamplerNormal]; var normal = material.Textures[TextureUsage.SamplerNormal];
// Create a copy of the normal that's the same size as the diffuse for purposes of copying the opacity across. // Create a copy of the normal that's the same size as the diffuse for purposes of copying the opacity across.
var resizedNormal = normal.Clone(context => context.Resize(diffuse.Width, diffuse.Height)); var resizedNormal = normal.Clone(context => context.Resize(diffuse.Width, diffuse.Height));
diffuse.ProcessPixelRows(resizedNormal, (diffuseAccessor, normalAccessor) => diffuse.ProcessPixelRows(resizedNormal, (diffuseAccessor, normalAccessor) =>
{ {
for (int y = 0; y < diffuseAccessor.Height; y++) for (var y = 0; y < diffuseAccessor.Height; y++)
{ {
var diffuseSpan = diffuseAccessor.GetRowSpan(y); var diffuseSpan = diffuseAccessor.GetRowSpan(y);
var normalSpan = normalAccessor.GetRowSpan(y); var normalSpan = normalAccessor.GetRowSpan(y);
for (int x = 0; x < diffuseSpan.Length; x++) for (var x = 0; x < diffuseSpan.Length; x++)
{
diffuseSpan[x].A = normalSpan[x].B; diffuseSpan[x].A = normalSpan[x].B;
}
} }
}); });
// 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 (var y = 0; y < normalAccessor.Height; y++)
{ {
var normalSpan = normalAccessor.GetRowSpan(y); var normalSpan = normalAccessor.GetRowSpan(y);
for (int x = 0; x < normalSpan.Length; x++) for (var x = 0; x < normalSpan.Length; x++)
{
normalSpan[x].B = byte.MaxValue; normalSpan[x].B = byte.MaxValue;
}
} }
}); });
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);
} }
@ -357,8 +356,8 @@ public class MaterialExporter
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?
const uint backfaceMask = 0x1; const uint backfaceMask = 0x1;
var showBackfaces = (material.Mtrl.ShaderPackage.Flags & backfaceMask) == 0; var showBackfaces = (material.Mtrl.ShaderPackage.Flags & backfaceMask) == 0;
return new MaterialBuilder(name) return new MaterialBuilder(name)
.WithDoubleSide(showBackfaces); .WithDoubleSide(showBackfaces);

View file

@ -21,11 +21,9 @@ namespace Penumbra.Import.Models;
using Schema2 = SharpGLTF.Schema2; using Schema2 = SharpGLTF.Schema2;
using LuminaMaterial = Lumina.Models.Materials.Material; 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, GamePathParser parser) : 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();
@ -47,11 +45,13 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
var action = new ImportGltfAction(inputPath); var action = new ImportGltfAction(inputPath);
return Enqueue(action).ContinueWith(task => return Enqueue(action).ContinueWith(task =>
{ {
if (task.IsFaulted && task.Exception != null) if (task is { IsFaulted: true, Exception: not null })
throw task.Exception; throw task.Exception;
return action.Out; return action.Out;
}); });
} }
/// <summary> Try to find the .sklb paths for a .mdl file. </summary> /// <summary> Try to find the .sklb paths for a .mdl file. </summary>
/// <param name="mdlPath"> .mdl file to look up the skeletons for. </param> /// <param name="mdlPath"> .mdl file to look up the skeletons for. </param>
/// <param name="estManipulations"> Modified extra skeleton template parameters. </param> /// <param name="estManipulations"> Modified extra skeleton template parameters. </param>
@ -69,8 +69,8 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
=> [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Body, info, estManipulations)], => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Body, info, estManipulations)],
ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Head ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Head
=> [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Head, info, estManipulations)], => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Head, info, estManipulations)],
ObjectType.Equipment => [baseSkeleton], ObjectType.Equipment => [baseSkeleton],
ObjectType.Accessory => [baseSkeleton], ObjectType.Accessory => [baseSkeleton],
ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => [baseSkeleton], ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => [baseSkeleton],
ObjectType.Character when info.BodySlot is BodySlot.Hair ObjectType.Character when info.BodySlot is BodySlot.Hair
=> [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Hair, info, estManipulations)], => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Hair, info, estManipulations)],
@ -91,15 +91,15 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
var modEst = estManipulations var modEst = estManipulations
.FirstOrNull(est => .FirstOrNull(est =>
est.Gender == gender est.Gender == gender
&& est.Race == race && est.Race == race
&& est.Slot == type && est.Slot == type
&& est.SetId == info.PrimaryId && est.SetId == info.PrimaryId
); );
// Try to use an entry from provided manipulations, falling back to the current collection. // Try to use an entry from provided manipulations, falling back to the current collection.
var targetId = modEst?.Entry var targetId = modEst?.Entry
?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId) ?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId)
?? 0; ?? 0;
// If there's no entries, we can assume that there's no additional skeleton. // If there's no entries, we can assume that there's no additional skeleton.
if (targetId == 0) if (targetId == 0)
@ -168,7 +168,12 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
return task; return task;
} }
private class ExportToGltfAction(ModelManager manager, MdlFile mdl, IEnumerable<string> sklbPaths, Func<string, byte[]> read, string outputPath) private class ExportToGltfAction(
ModelManager manager,
MdlFile mdl,
IEnumerable<string> sklbPaths,
Func<string, byte[]> read,
string outputPath)
: IAction : IAction
{ {
public void Execute(CancellationToken cancel) public void Execute(CancellationToken cancel)
@ -213,13 +218,13 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
// finicky at the best of times, and can outright cause a CTD if they // finicky at the best of times, and can outright cause a CTD if they
// get upset. Running each conversion on its own tick seems to make // get upset. Running each conversion on its own tick seems to make
// this consistently non-crashy across my testing. // this consistently non-crashy across my testing.
Task<string> CreateHavokTask((SklbFile Sklb, int Index) pair) => Task<string> CreateHavokTask((SklbFile Sklb, int Index) pair)
manager._framework.RunOnTick( => manager._framework.RunOnTick(
() => HavokConverter.HkxToXml(pair.Sklb.Skeleton), () => HavokConverter.HkxToXml(pair.Sklb.Skeleton),
delayTicks: pair.Index, cancellationToken: cancel); delayTicks: pair.Index, cancellationToken: cancel);
} }
/// <summary> Read a .mtrl and hydrate its textures. </summary> /// <summary> Read a .mtrl and populate its textures. </summary>
private MaterialExporter.Material BuildMaterial(string relativePath, CancellationToken cancel) private MaterialExporter.Material BuildMaterial(string relativePath, CancellationToken cancel)
{ {
var path = manager.ResolveMtrlPath(relativePath); var path = manager.ResolveMtrlPath(relativePath);
@ -227,7 +232,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)
@ -242,21 +247,15 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
var texturePath = texture.Path; var texturePath = texture.Path;
if (texture.DX11) if (texture.DX11)
{ {
var lastSlashIndex = texturePath.LastIndexOf('/'); var fileName = Path.GetFileName(texturePath);
var directory = lastSlashIndex == -1 ? texturePath : texturePath.Substring(0, lastSlashIndex);
var fileName = Path.GetFileName(texturePath);
if (!fileName.StartsWith("--")) if (!fileName.StartsWith("--"))
{ texturePath = $"{Path.GetDirectoryName(texturePath)}/--{fileName}";
texturePath = $"{directory}/--{fileName}";
}
} }
using var textureData = new MemoryStream(read(texturePath)); using var textureData = new MemoryStream(read(texturePath));
var image = TexFileParser.Parse(textureData); var image = TexFileParser.Parse(textureData);
var pngImage = TextureManager.ConvertToPng(image, cancel).AsPng; var pngImage = TextureManager.ConvertToPng(image, cancel).AsPng;
if (pngImage == null) return pngImage ?? throw new Exception("Failed to convert texture to png.");
throw new Exception("Failed to convert texture to png.");
return pngImage;
} }
public bool Equals(IAction? other) public bool Equals(IAction? other)

View file

@ -181,9 +181,9 @@ public partial class ModEditWindow
} }
/// <summary> Merge attribute configuration from the source onto the target. </summary> /// <summary> Merge attribute configuration from the source onto the target. </summary>
/// <param name="target" Model that will be update. ></param> /// <param name="target"> Model that will be updated. ></param>
/// <param name="source"> Model to copy attribute configuration from. </param> /// <param name="source"> Model to copy attribute configuration from. </param>
public void MergeAttributes(MdlFile target, MdlFile source) public static void MergeAttributes(MdlFile target, MdlFile source)
{ {
target.Attributes = source.Attributes; target.Attributes = source.Attributes;
@ -197,7 +197,7 @@ public partial class ModEditWindow
target.SubMeshes[subMeshIndex].AttributeIndexMask = 0u; target.SubMeshes[subMeshIndex].AttributeIndexMask = 0u;
// Rather than comparing sub-meshes directly, we're grouping by parent mesh in an attempt // Rather than comparing sub-meshes directly, we're grouping by parent mesh in an attempt
// to maintain semantic connection betwen mesh index and submesh attributes. // to maintain semantic connection between mesh index and sub mesh attributes.
if (meshIndex >= source.Meshes.Length) if (meshIndex >= source.Meshes.Length)
continue; continue;
var sourceMesh = source.Meshes[meshIndex]; var sourceMesh = source.Meshes[meshIndex];
@ -214,8 +214,8 @@ public partial class ModEditWindow
{ {
IoExceptions = exception switch { IoExceptions = exception switch {
null => [], null => [],
AggregateException ae => ae.Flatten().InnerExceptions.ToList(), AggregateException ae => [.. ae.Flatten().InnerExceptions],
Exception other => [other], _ => [exception],
}; };
} }
@ -234,7 +234,7 @@ public partial class ModEditWindow
? _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? // TODO: some callers may not care about failures - handle exceptions separately?
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?");
} }

View file

@ -129,7 +129,7 @@ public partial class ModEditWindow
); );
} }
private void DrawIoExceptions(MdlTab tab) private static void DrawIoExceptions(MdlTab tab)
{ {
if (tab.IoExceptions.Count == 0) if (tab.IoExceptions.Count == 0)
return; return;
@ -140,16 +140,15 @@ public partial class ModEditWindow
var spaceAvail = ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X - 100; var spaceAvail = ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X - 100;
foreach (var (exception, index) in tab.IoExceptions.WithIndex()) foreach (var (exception, index) in tab.IoExceptions.WithIndex())
{ {
var message = $"{exception.GetType().Name}: {exception.Message}"; using var id = ImRaii.PushId(index);
var textSize = ImGui.CalcTextSize(message).X; var message = $"{exception.GetType().Name}: {exception.Message}";
var textSize = ImGui.CalcTextSize(message).X;
if (textSize > spaceAvail) if (textSize > spaceAvail)
message = message.Substring(0, (int)Math.Floor(message.Length * (spaceAvail / textSize))) + "..."; message = message[..(int)Math.Floor(message.Length * (spaceAvail / textSize))] + "...";
using (var exceptionNode = ImRaii.TreeNode($"{message}###exception{index}")) using var exceptionNode = ImRaii.TreeNode(message);
{ if (exceptionNode)
if (exceptionNode) ImGuiUtil.TextWrapped(exception.ToString());
ImGuiUtil.TextWrapped(exception.ToString());
}
} }
} }