Add shape key support

This commit is contained in:
ackwell 2024-01-05 15:32:31 +11:00
parent 4e8695e7a4
commit acaa49fec5
2 changed files with 260 additions and 48 deletions

View file

@ -4,7 +4,9 @@ using SharpGLTF.Schema2;
namespace Penumbra.Import.Models.Import;
using Writer = Action<int, List<byte>>;
using BuildFn = Func<int, byte[]>;
using HasMorphFn = Func<int, int, bool>;
using BuildMorphFn = Func<int, int, byte[]>;
using Accessors = IReadOnlyDictionary<string, Accessor>;
public class VertexAttribute
@ -12,7 +14,11 @@ public class VertexAttribute
/// <summary> XIV vertex element metadata structure. </summary>
public readonly MdlStructs.VertexElement Element;
/// <summary> Write this vertex attribute's value at the specified index to the provided byte array. </summary>
public readonly Writer Write;
public readonly BuildFn Build;
public readonly HasMorphFn HasMorph;
public readonly BuildMorphFn BuildMorph;
/// <summary> Size in bytes of a single vertex's attribute value. </summary>
public byte Size => (MdlFile.VertexType)Element.Type switch
@ -27,13 +33,32 @@ public class VertexAttribute
_ => throw new Exception($"Unhandled vertex type {(MdlFile.VertexType)Element.Type}"),
};
public VertexAttribute(MdlStructs.VertexElement element, Writer write)
public VertexAttribute(
MdlStructs.VertexElement element,
BuildFn write,
HasMorphFn? hasMorph = null,
BuildMorphFn? buildMorph = null
)
{
Element = element;
Write = write;
Build = write;
HasMorph = hasMorph ?? DefaultHasMorph;
BuildMorph = buildMorph ?? DefaultBuildMorph;
}
public static VertexAttribute Position(Accessors accessors)
// todo: this is per-shape at the moment - consider if it should do them all at once (i mean we always want to check all of them, it's mostly a semantics question on who owns the loop)
private static bool DefaultHasMorph(int morphIndex, int vertexIndex)
{
return false;
}
// xiv stores shapes as full vertex replacements, so the default value for a morph attribute is simply it's built state (rather than a delta or w/e)
private byte[] DefaultBuildMorph(int morphIndex, int vertexIndex)
{
return Build(vertexIndex);
}
public static VertexAttribute Position(Accessors accessors, IEnumerable<Accessors> morphAccessors)
{
if (!accessors.TryGetValue("POSITION", out var accessor))
throw new Exception("Meshes must contain a POSITION attribute.");
@ -47,9 +72,32 @@ public class VertexAttribute
var values = accessor.AsVector3Array();
var foo = morphAccessors
.Select(ma => ma.GetValueOrDefault("POSITION")?.AsVector3Array())
.ToArray();
return new VertexAttribute(
element,
(index, bytes) => WriteSingle3(values[index], bytes)
index => BuildSingle3(values[index]),
// TODO: at the moment this is only defined for position - is it worth setting one up for normal, too?
(morphIndex, vertexIndex) =>
{
var deltas = foo[morphIndex];
if (deltas == null) return false;
var delta = deltas[vertexIndex];
return delta != Vector3.Zero;
},
// TODO: this will _need_ to be defined for any values that appear in morphs, i.e. geom and maybe mats
(morphIndex, vertexIndex) =>
{
var value = values[vertexIndex];
var delta = foo[morphIndex]?[vertexIndex];
if (delta != null)
value += delta.Value;
return BuildSingle3(value);
}
);
}
@ -73,8 +121,7 @@ public class VertexAttribute
return new VertexAttribute(
element,
// TODO: TEMP TESTING PINNED TO BONE 0 UNTIL I SET UP BONE MAPPINGS
// (index, bytes) => WriteByteFloat4(values[index], bytes)
(index, bytes) => WriteByteFloat4(Vector4.UnitX, bytes)
index => BuildByteFloat4(Vector4.UnitX)
);
}
@ -99,8 +146,7 @@ public class VertexAttribute
return new VertexAttribute(
element,
// TODO: TEMP TESTING PINNED TO BONE 0 UNTIL I SET UP BONE MAPPINGS
// (index, bytes) => WriteUInt(values[index], bytes)
(index, bytes) => WriteUInt(Vector4.Zero, bytes)
index => BuildUInt(Vector4.Zero)
);
}
@ -120,7 +166,7 @@ public class VertexAttribute
return new VertexAttribute(
element,
(index, bytes) => WriteHalf4(new Vector4(values[index], 0), bytes)
index => BuildHalf4(new Vector4(values[index], 0))
);
}
@ -141,18 +187,18 @@ public class VertexAttribute
if (!accessors.TryGetValue("TEXCOORD_1", out var accessor2))
return new VertexAttribute(
element with { Type = (byte)MdlFile.VertexType.Half2 },
(index, bytes) => WriteHalf2(values1[index], bytes)
index => BuildHalf2(values1[index])
);
var values2 = accessor2.AsVector2Array();
return new VertexAttribute(
element with { Type = (byte)MdlFile.VertexType.Half4 },
(index, bytes) =>
index =>
{
var value1 = values1[index];
var value2 = values2[index];
WriteHalf4(new Vector4(value1.X, value1.Y, value2.X, value2.Y), bytes);
return BuildHalf4(new Vector4(value1.X, value1.Y, value2.X, value2.Y));
}
);
}
@ -173,7 +219,7 @@ public class VertexAttribute
return new VertexAttribute(
element,
(index, bytes) => WriteByteFloat4(values[index], bytes)
index => BuildByteFloat4(values[index])
);
}
@ -193,44 +239,54 @@ public class VertexAttribute
return new VertexAttribute(
element,
(index, bytes) => WriteByteFloat4(values[index], bytes)
index => BuildByteFloat4(values[index])
);
}
private static void WriteSingle3(Vector3 input, List<byte> bytes)
private static byte[] BuildSingle3(Vector3 input)
{
bytes.AddRange(BitConverter.GetBytes(input.X));
bytes.AddRange(BitConverter.GetBytes(input.Y));
bytes.AddRange(BitConverter.GetBytes(input.Z));
return [
..BitConverter.GetBytes(input.X),
..BitConverter.GetBytes(input.Y),
..BitConverter.GetBytes(input.Z),
];
}
private static void WriteUInt(Vector4 input, List<byte> bytes)
private static byte[] BuildUInt(Vector4 input)
{
bytes.Add((byte)input.X);
bytes.Add((byte)input.Y);
bytes.Add((byte)input.Z);
bytes.Add((byte)input.W);
return [
(byte)input.X,
(byte)input.Y,
(byte)input.Z,
(byte)input.W,
];
}
private static void WriteByteFloat4(Vector4 input, List<byte> bytes)
private static byte[] BuildByteFloat4(Vector4 input)
{
bytes.Add((byte)Math.Round(input.X * 255f));
bytes.Add((byte)Math.Round(input.Y * 255f));
bytes.Add((byte)Math.Round(input.Z * 255f));
bytes.Add((byte)Math.Round(input.W * 255f));
return [
(byte)Math.Round(input.X * 255f),
(byte)Math.Round(input.Y * 255f),
(byte)Math.Round(input.Z * 255f),
(byte)Math.Round(input.W * 255f),
];
}
private static void WriteHalf2(Vector2 input, List<byte> bytes)
private static byte[] BuildHalf2(Vector2 input)
{
bytes.AddRange(BitConverter.GetBytes((Half)input.X));
bytes.AddRange(BitConverter.GetBytes((Half)input.Y));
return [
..BitConverter.GetBytes((Half)input.X),
..BitConverter.GetBytes((Half)input.Y),
];
}
private static void WriteHalf4(Vector4 input, List<byte> bytes)
private static byte[] BuildHalf4(Vector4 input)
{
bytes.AddRange(BitConverter.GetBytes((Half)input.X));
bytes.AddRange(BitConverter.GetBytes((Half)input.Y));
bytes.AddRange(BitConverter.GetBytes((Half)input.Z));
bytes.AddRange(BitConverter.GetBytes((Half)input.W));
return [
..BitConverter.GetBytes((Half)input.X),
..BitConverter.GetBytes((Half)input.Y),
..BitConverter.GetBytes((Half)input.Z),
..BitConverter.GetBytes((Half)input.W),
];
}
}

View file

@ -206,6 +206,9 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
var submeshes = new List<MdlStructs.SubmeshStruct>();
var vertexBuffer = new List<byte>();
var indices = new List<byte>();
var shapeData = new Dictionary<string, List<MdlStructs.ShapeMeshStruct>>();
var shapeValues = new List<MdlStructs.ShapeValueStruct>();
foreach (var submeshnodes in nodes)
{
@ -214,6 +217,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
var subOffset = submeshes.Count;
var vertOffset = vertexBuffer.Count;
var idxOffset = indices.Count;
var shapeValueOffset = shapeValues.Count;
var (
vertexDeclaration,
@ -221,16 +225,18 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
xivMesh,
xivSubmeshes,
meshVertexBuffer,
meshIndices
meshIndices,
meshShapeData // fasdfasd
) = MeshThing(submeshnodes);
vertexDeclarations.Add(vertexDeclaration);
boneTables.Add(boneTable);
var meshStartIndex = (uint)(xivMesh.StartIndex + idxOffset / sizeof(ushort));
meshes.Add(xivMesh with {
SubMeshIndex = (ushort)(xivMesh.SubMeshIndex + subOffset),
// TODO: should probably define a type for index type hey.
BoneTableIndex = (ushort)(xivMesh.BoneTableIndex + boneTableOffset),
StartIndex = (uint)(xivMesh.StartIndex + idxOffset / sizeof(ushort)),
StartIndex = meshStartIndex,
VertexBufferOffset = xivMesh.VertexBufferOffset
.Select(offset => (uint)(offset + vertOffset))
.ToArray(),
@ -243,6 +249,39 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
});
vertexBuffer.AddRange(meshVertexBuffer);
indices.AddRange(meshIndices.SelectMany(index => BitConverter.GetBytes((ushort)index)));
foreach (var (key, (shapeMesh, meshShapeValues)) in meshShapeData)
{
List<MdlStructs.ShapeMeshStruct> keyshapedata;
if (!shapeData.TryGetValue(key, out keyshapedata))
{
keyshapedata = new();
shapeData.Add(key, keyshapedata);
}
keyshapedata.Add(shapeMesh with {
MeshIndexOffset = meshStartIndex,
ShapeValueOffset = (uint)shapeValueOffset,
});
shapeValues.AddRange(meshShapeValues);
}
}
var shapes = new List<MdlFile.Shape>();
var shapeMeshes = new List<MdlStructs.ShapeMeshStruct>();
foreach (var (name, sms) in shapeData)
{
var smOff = shapeMeshes.Count;
shapeMeshes.AddRange(sms);
shapes.Add(new MdlFile.Shape()
{
ShapeName = name,
// TODO: THESE IS PER LOD
ShapeMeshStartIndex = [(ushort)smOff, 0, 0],
ShapeMeshCount = [(ushort)sms.Count, 0, 0],
});
}
var mdl = new MdlFile()
@ -292,6 +331,10 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
// game clearly doesn't rely on this, but the "correct" values are a listing of the bones used by each submesh
SubMeshBoneMap = [0],
Shapes = shapes.ToArray(),
ShapeMeshes = shapeMeshes.ToArray(),
ShapeValues = shapeValues.ToArray(),
Lods = [new MdlStructs.LodStruct()
{
MeshIndex = 0,
@ -323,7 +366,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
MdlStructs.MeshStruct,
IEnumerable<MdlStructs.SubmeshStruct>,
IEnumerable<byte>,
IEnumerable<ushort>
IEnumerable<ushort>,
IDictionary<string, (MdlStructs.ShapeMeshStruct, List<MdlStructs.ShapeValueStruct>)>
) MeshThing(IEnumerable<Node> nodes)
{
var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() { VertexElements = Array.Empty<MdlStructs.VertexElement>()};
@ -336,6 +380,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
var indices = new List<ushort>();
var strides = new byte[] {0, 0, 0};
var submeshes = new List<MdlStructs.SubmeshStruct>();
var morphData = new Dictionary<string, List<MdlStructs.ShapeValueStruct>>();
// TODO: check that attrs/elems/strides match - we should be generating per-mesh stuff for sanity's sake, but we need to make sure they match if there's >1 node mesh in a mesh.
foreach (var node in nodes)
@ -343,7 +388,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
var vertOff = vertexCount;
var idxOff = indexCount;
var (vertDecl, newStrides, submesh, vertCount, vertStreams, idxCount, idxs) = NodeMeshThing(node);
var (vertDecl, newStrides, submesh, vertCount, vertStreams, idxCount, idxs, subMorphData) = NodeMeshThing(node);
vertexDeclaration = vertDecl; // TODO: CHECK EQUAL AFTER FIRST
strides = newStrides; // ALSO CHECK EQUAL
vertexCount += vertCount;
@ -356,6 +401,25 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
IndexOffset = submesh.IndexOffset + idxOff
// TODO: bone stuff probably
});
// TODO: HANDLE MORPHS, NEED TO ADJUST EVERY VALUE'S INDEX OFFSETS
foreach (var (key, shapeValues) in subMorphData)
{
List<MdlStructs.ShapeValueStruct> valueList;
if (!morphData.TryGetValue(key, out valueList))
{
valueList = new();
morphData.Add(key, valueList);
}
valueList.AddRange(
shapeValues
.Select(value => value with {
// but this is actually an index index
BaseIndicesIndex = (ushort)(value.BaseIndicesIndex + idxOff),
// this is a vert idx
ReplacingVertexIndex = (ushort)(value.ReplacingVertexIndex + vertOff),
})
);
}
}
// one of these per skinned mesh.
@ -390,13 +454,30 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
VertexStreamCount = (byte)(vertexDeclaration.VertexElements.Select(element => element.Stream).Max() + 1)
};
// TODO: can probably get away with flattening the values and blindly setting offsets in parent - mesh matters above, but the values are already Dealt With at this point
var shapeData = morphData.ToDictionary(
(pair) => pair.Key,
pair => (
new MdlStructs.ShapeMeshStruct()
{
// TODO: this needs to be adjusted by the parent
MeshIndexOffset = 0,
ShapeValueCount = (uint)pair.Value.Count,
// TODO: Also update by parent
ShapeValueOffset = 0,
},
pair.Value
)
);
return (
vertexDeclaration,
boneTable,
xivMesh,
submeshes,
streams[0].Concat(streams[1]).Concat(streams[2]),
indices
indices,
shapeData
);
}
@ -408,7 +489,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
ushort,
IEnumerable<byte>[],
uint,
IEnumerable<ushort>
IEnumerable<ushort>,
IDictionary<string, List<MdlStructs.ShapeValueStruct>>
) NodeMeshThing(Node node)
{
// BoneTable (mesh.btidx = 255 means unskinned)
@ -423,9 +505,22 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
var primitive = mesh.Primitives[0];
var accessors = primitive.VertexAccessors;
// var foo = primitive.GetMorphTargetAccessors(0);
// var bar = foo["POSITION"];
// var baz = bar.AsVector3Array();
var morphAccessors = Enumerable.Range(0, primitive.MorphTargetsCount)
// todo: map by name, probably? or do that later (probably later)
.Select(index => primitive.GetMorphTargetAccessors(index));
// TODO: name
var morphChangedVerts = Enumerable.Range(0, primitive.MorphTargetsCount)
.Select(_ => new List<int>())
.ToArray();
var rawAttributes = new[] {
VertexAttribute.Position(accessors),
VertexAttribute.Position(accessors, morphAccessors),
VertexAttribute.BlendWeight(accessors),
VertexAttribute.BlendIndex(accessors),
VertexAttribute.Normal(accessors),
@ -440,9 +535,12 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
{
if (attribute == null) continue;
var element = attribute.Element;
// recreating this here really sucks - add a "withstream" or something.
attributes.Add(new VertexAttribute(
element with {Offset = offsets[element.Stream]},
attribute.Write
attribute.Build,
attribute.HasMorph,
attribute.BuildMorph
));
offsets[element.Stream] += attribute.Size;
}
@ -460,7 +558,18 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
{
foreach (var attribute in attributes)
{
attribute.Write(vertexIndex, streams[attribute.Element.Stream]);
streams[attribute.Element.Stream].AddRange(attribute.Build(vertexIndex));
}
// this is a meme but idk maybe it's the best approach? it's not like the attr array is ever long
foreach (var (list, morphIndex) in morphChangedVerts.WithIndex())
{
var hasMorph = attributes.Aggregate(false, (cur, attr) => cur || attr.HasMorph(morphIndex, vertexIndex));
// Penumbra.Log.Information($"eh? {vertexIndex} {morphIndex}: {hasMorph}");
if (hasMorph)
{
list.Add(vertexIndex);
}
}
}
@ -471,6 +580,52 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
// .ToArray();
var indices = primitive.GetIndices().Select(idx => (ushort)idx).ToArray();
// BLAH
// foreach (var (list, morphIndex) in morphChangedVerts.WithIndex())
// {
// Penumbra.Log.Information($"morph {morphIndex}: {string.Join(",", list)}");
// }
// TODO BUILD THE MORPH VERTS
// (source, target)
var morphmappingstuff = new List<MdlStructs.ShapeValueStruct>[morphChangedVerts.Length];
foreach (var (list, morphIndex) in morphChangedVerts.WithIndex())
{
var morphmaplist = morphmappingstuff[morphIndex] = new();
foreach (var vertIdx in list)
{
foreach (var attribute in attributes)
{
streams[attribute.Element.Stream].AddRange(attribute.BuildMorph(morphIndex, vertIdx));
}
var fuck = indices.WithIndex()
.Where(pair => pair.Value == vertIdx)
.Select(pair => pair.Index);
foreach (var something in fuck)
{
morphmaplist.Add(new MdlStructs.ShapeValueStruct(){
BaseIndicesIndex = (ushort)something,
ReplacingVertexIndex = (ushort)vertexCount,
});
}
vertexCount++;
}
}
// TODO: HANDLE THIS BEING MISSING - probably warn or something, it's not the end of the world
var morphData = new Dictionary<string, List<MdlStructs.ShapeValueStruct>>();
if (morphmappingstuff.Length > 0)
{
var morphnames = mesh.Extras.GetNode("targetNames").Deserialize<List<string>>();
morphData = morphmappingstuff
.Zip(morphnames)
.ToDictionary(
(pair) => pair.Second,
(pair) => pair.First
);
}
// one of these per mesh
var vertexDeclaration = new MdlStructs.VertexDeclarationStruct()
{
@ -521,7 +676,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
(ushort)vertexCount,
streams,
(uint)indices.Length,
indices
indices,
morphData
);
}