Fix character*.shpk exports

This commit is contained in:
ackwell 2024-09-01 21:32:31 +10:00 committed by Ottermandias
parent 4719f413b6
commit 8fa0875ec6

View file

@ -36,12 +36,13 @@ public class MaterialExporter
return material.Mtrl.ShaderPackage.Name switch return material.Mtrl.ShaderPackage.Name switch
{ {
// NOTE: this isn't particularly precise to game behavior (it has some fade around high opacity), but good enough for now. // NOTE: this isn't particularly precise to game behavior (it has some fade around high opacity), but good enough for now.
"character.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f), "character.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f),
"characterglass.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.BLEND), "characterlegacy.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f),
"hair.shpk" => BuildHair(material, name), "characterglass.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.BLEND),
"iris.shpk" => BuildIris(material, name), "hair.shpk" => BuildHair(material, name),
"skin.shpk" => BuildSkin(material, name), "iris.shpk" => BuildIris(material, name),
_ => BuildFallback(material, name, notifier), "skin.shpk" => BuildSkin(material, name),
_ => BuildFallback(material, name, notifier),
}; };
} }
@ -49,70 +50,65 @@ public class MaterialExporter
private static MaterialBuilder BuildCharacter(Material material, string name) private static MaterialBuilder BuildCharacter(Material material, string name)
{ {
// Build the textures from the color table. // Build the textures from the color table.
var table = new LegacyColorTable(material.Mtrl.Table!); var table = new ColorTable(material.Mtrl.Table!);
var indexTexture = material.Textures[(TextureUsage)1449103320];
var indexOperation = new ProcessCharacterIndexOperation(indexTexture, table);
ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, indexTexture.Bounds, in indexOperation);
var normal = material.Textures[TextureUsage.SamplerNormal]; var normalTexture = material.Textures[TextureUsage.SamplerNormal];
var normalOperation = new ProcessCharacterNormalOperation(normalTexture);
ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normalTexture.Bounds, in normalOperation);
var operation = new ProcessCharacterNormalOperation(normal, table); // Merge in opacity from the normal.
ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds, in operation); var baseColor = indexOperation.BaseColor;
MultiplyOperation.Execute(baseColor, normalOperation.BaseColorOpacity);
// Check if full textures are provided, and merge in if available. // Check if a full diffuse is provided, and merge in if available.
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, indexOperation.BaseColor);
baseColor = diffuse; baseColor = diffuse;
} }
Image specular = operation.Specular; var specular = indexOperation.Specular;
if (material.Textures.TryGetValue(TextureUsage.SamplerSpecular, out var specularTexture)) if (material.Textures.TryGetValue(TextureUsage.SamplerSpecular, out var specularTexture))
{ {
MultiplyOperation.Execute(specularTexture, operation.Specular); MultiplyOperation.Execute(specularTexture, indexOperation.Specular);
specular = specularTexture; specular = specularTexture;
} }
// Pull further information from the mask. // Pull further information from the mask.
if (material.Textures.TryGetValue(TextureUsage.SamplerMask, out var maskTexture)) if (material.Textures.TryGetValue(TextureUsage.SamplerMask, out var maskTexture))
{ {
// Extract the red channel for "ambient occlusion". var maskOperation = new ProcessCharacterMaskOperation(maskTexture);
maskTexture.Mutate(context => context.Resize(baseColor.Width, baseColor.Height)); ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, maskTexture.Bounds, in maskOperation);
maskTexture.ProcessPixelRows(baseColor, (maskAccessor, baseColorAccessor) =>
{
for (var y = 0; y < maskAccessor.Height; y++)
{
var maskSpan = maskAccessor.GetRowSpan(y);
var baseColorSpan = baseColorAccessor.GetRowSpan(y);
for (var x = 0; x < maskSpan.Length; x++) // TODO: consider using the occusion gltf material property.
baseColorSpan[x].FromVector4(baseColorSpan[x].ToVector4() * new Vector4(maskSpan[x].R / 255f)); MultiplyOperation.Execute(baseColor, maskOperation.Occlusion);
}
}); // Similar to base color's alpha, this is a pretty wasteful operation for a single channel.
// TODO: handle other textures stored in the mask? MultiplyOperation.Execute(specular, maskOperation.SpecularFactor);
} }
// Specular extension puts colour on RGB and factor on A. We're already packing like that, so we can reuse the texture. // Specular extension puts colour on RGB and factor on A. We're already packing like that, so we can reuse the texture.
var specularImage = BuildImage(specular, name, "specular"); var specularImage = BuildImage(specular, name, "specular");
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(normalOperation.Normal, name, "normal"))
.WithEmissive(BuildImage(operation.Emissive, name, "emissive"), Vector3.One, 1) .WithEmissive(BuildImage(indexOperation.Emissive, name, "emissive"), Vector3.One, 1)
.WithSpecularFactor(specularImage, 1) .WithSpecularFactor(specularImage, 1)
.WithSpecularColor(specularImage); .WithSpecularColor(specularImage);
} }
// TODO: It feels a little silly to request the entire normal here when extracting the normal only needs some of the components. private readonly struct ProcessCharacterIndexOperation(Image<Rgba32> index, ColorTable table) : IRowOperation
// As a future refactor, it would be neat to accept a single-channel field here, and then do composition of other stuff later.
// TODO(Dawntrail): Use the dedicated index (_id) map, that is not embedded in the normal map's alpha channel anymore.
private readonly struct ProcessCharacterNormalOperation(Image<Rgba32> normal, LegacyColorTable table) : IRowOperation
{ {
public Image<Rgba32> Normal { get; } = normal.Clone(); public Image<Rgba32> BaseColor { get; } = new(index.Width, index.Height);
public Image<Rgba32> BaseColor { get; } = new(normal.Width, normal.Height); public Image<Rgba32> Specular { get; } = new(index.Width, index.Height);
public Image<Rgba32> Specular { get; } = new(normal.Width, normal.Height); public Image<Rgb24> Emissive { get; } = new(index.Width, index.Height);
public Image<Rgb24> Emissive { get; } = new(normal.Width, normal.Height);
private Buffer2D<Rgba32> NormalBuffer private Buffer2D<Rgba32> IndexBuffer
=> Normal.Frames.RootFrame.PixelBuffer; => index.Frames.RootFrame.PixelBuffer;
private Buffer2D<Rgba32> BaseColorBuffer private Buffer2D<Rgba32> BaseColorBuffer
=> BaseColor.Frames.RootFrame.PixelBuffer; => BaseColor.Frames.RootFrame.PixelBuffer;
@ -125,66 +121,96 @@ public class MaterialExporter
public void Invoke(int y) public void Invoke(int y)
{ {
var normalSpan = NormalBuffer.DangerousGetRowSpan(y); var indexSpan = IndexBuffer.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 (var x = 0; x < indexSpan.Length; x++)
{
ref var indexPixel = ref indexSpan[x];
// Calculate and fetch the color table rows being used for this pixel.
var tablePair = (int) Math.Round(indexPixel.R / 17f);
var rowBlend = 1.0f - indexPixel.G / 255f;
var prevRow = table[tablePair * 2];
var nextRow = table[Math.Min(tablePair * 2 + 1, ColorTable.NumRows)];
// Lerp between table row values to fetch final pixel values for each subtexture.
var lerpedDiffuse = Vector3.Lerp((Vector3)prevRow.DiffuseColor, (Vector3)nextRow.DiffuseColor, rowBlend);
baseColorSpan[x].FromVector4(new Vector4(lerpedDiffuse, 1));
var lerpedSpecularColor = Vector3.Lerp((Vector3)prevRow.SpecularColor, (Vector3)nextRow.SpecularColor, rowBlend);
specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, 1));
var lerpedEmissive = Vector3.Lerp((Vector3)prevRow.EmissiveColor, (Vector3)nextRow.EmissiveColor, rowBlend);
emissiveSpan[x].FromVector4(new Vector4(lerpedEmissive, 1));
}
}
}
private readonly struct ProcessCharacterNormalOperation(Image<Rgba32> normal) : IRowOperation
{
// TODO: Consider omitting the alpha channel here.
public Image<Rgba32> Normal { get; } = normal.Clone();
// TODO: We only really need the alpha here, however using A8 will result in the multiply later zeroing out the RGB channels.
public Image<Rgba32> BaseColorOpacity { get; } = new(normal.Width, normal.Height);
private Buffer2D<Rgba32> NormalBuffer
=> Normal.Frames.RootFrame.PixelBuffer;
private Buffer2D<Rgba32> BaseColorOpacityBuffer
=> BaseColorOpacity.Frames.RootFrame.PixelBuffer;
public void Invoke(int y)
{
var normalSpan = NormalBuffer.DangerousGetRowSpan(y);
var baseColorOpacitySpan = BaseColorOpacityBuffer.DangerousGetRowSpan(y);
for (var 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) baseColorOpacitySpan[x].FromVector4(Vector4.One);
var tableRow = GetTableRowIndices(normalPixel.A / 255f); baseColorOpacitySpan[x].A = normalPixel.B;
var prevRow = table[tableRow.Previous];
var nextRow = table[tableRow.Next];
// Base colour (table, .b)
var lerpedDiffuse = Vector3.Lerp((Vector3)prevRow.DiffuseColor, (Vector3)nextRow.DiffuseColor, tableRow.Weight);
baseColorSpan[x].FromVector4(new Vector4(lerpedDiffuse, 1));
baseColorSpan[x].A = normalPixel.B;
// Specular (table)
var lerpedSpecularColor = Vector3.Lerp((Vector3)prevRow.SpecularColor, (Vector3)nextRow.SpecularColor, tableRow.Weight);
var lerpedSpecularFactor = float.Lerp((float)prevRow.SpecularMask, (float)nextRow.SpecularMask, tableRow.Weight);
specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, lerpedSpecularFactor));
// Emissive (table)
var lerpedEmissive = Vector3.Lerp((Vector3)prevRow.EmissiveColor, (Vector3)nextRow.EmissiveColor, tableRow.Weight);
emissiveSpan[x].FromVector4(new Vector4(lerpedEmissive, 1));
// Normal (.rg)
// TODO: we don't actually need alpha at all for normal, but _not_ using the existing rgba texture means I'll need a new one, with a new accessor. Think about it.
normalPixel.B = byte.MaxValue; normalPixel.B = byte.MaxValue;
normalPixel.A = byte.MaxValue; normalPixel.A = byte.MaxValue;
} }
} }
} }
private static TableRow GetTableRowIndices(float input) private readonly struct ProcessCharacterMaskOperation(Image<Rgba32> mask) : IRowOperation
{ {
// These calculations are ported from character.shpk. public Image<Rgba32> Occlusion { get; } = new(mask.Width, mask.Height);
var smoothed = MathF.Floor(input * 7.5f % 1.0f * 2) public Image<Rgba32> SpecularFactor { get; } = new(mask.Width, mask.Height);
* (-input * 15 + MathF.Floor(input * 15 + 0.5f))
+ input * 15;
var stepped = MathF.Floor(smoothed + 0.5f); private Buffer2D<Rgba32> MaskBuffer
=> mask.Frames.RootFrame.PixelBuffer;
return new TableRow private Buffer2D<Rgba32> OcclusionBuffer
=> Occlusion.Frames.RootFrame.PixelBuffer;
private Buffer2D<Rgba32> SpecularFactorBuffer
=> SpecularFactor.Frames.RootFrame.PixelBuffer;
public void Invoke(int y)
{ {
Stepped = (int)stepped, var maskSpan = MaskBuffer.DangerousGetRowSpan(y);
Previous = (int)MathF.Floor(smoothed), var occlusionSpan = OcclusionBuffer.DangerousGetRowSpan(y);
Next = (int)MathF.Ceiling(smoothed), var specularFactorSpan = SpecularFactorBuffer.DangerousGetRowSpan(y);
Weight = smoothed % 1,
};
}
private ref struct TableRow for (var x = 0; x < maskSpan.Length; x++)
{ {
public int Stepped; ref var maskPixel = ref maskSpan[x];
public int Previous;
public int Next; occlusionSpan[x].FromL8(new L8(maskPixel.B));
public float Weight;
specularFactorSpan[x].FromVector4(Vector4.One);
specularFactorSpan[x].A = maskPixel.R;
}
}
} }
private readonly struct MultiplyOperation private readonly struct MultiplyOperation