mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Fix character*.shpk exports
This commit is contained in:
parent
4719f413b6
commit
8fa0875ec6
1 changed files with 107 additions and 81 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue