mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Initial pass on skinned mesh output
This commit is contained in:
parent
695c18439d
commit
727fa3c183
2 changed files with 98 additions and 19 deletions
|
|
@ -10,9 +10,9 @@ namespace Penumbra.Import.Modules;
|
|||
|
||||
public sealed class MeshConverter
|
||||
{
|
||||
public static IMeshBuilder<MaterialBuilder> ToGltf(MdlFile mdl, byte lod, ushort meshIndex)
|
||||
public static IMeshBuilder<MaterialBuilder> ToGltf(MdlFile mdl, byte lod, ushort meshIndex, Dictionary<string, int>? boneNameMap)
|
||||
{
|
||||
var self = new MeshConverter(mdl, lod, meshIndex);
|
||||
var self = new MeshConverter(mdl, lod, meshIndex, boneNameMap);
|
||||
return self.BuildMesh();
|
||||
}
|
||||
|
||||
|
|
@ -23,21 +23,49 @@ public sealed class MeshConverter
|
|||
private readonly ushort _meshIndex;
|
||||
private MdlStructs.MeshStruct Mesh => _mdl.Meshes[_meshIndex];
|
||||
|
||||
private readonly Type _geometryType;
|
||||
private readonly Dictionary<ushort, int>? _boneIndexMap;
|
||||
|
||||
private MeshConverter(MdlFile mdl, byte lod, ushort meshIndex)
|
||||
private readonly Type _geometryType;
|
||||
private readonly Type _skinningType;
|
||||
|
||||
private MeshConverter(MdlFile mdl, byte lod, ushort meshIndex, Dictionary<string, int>? boneNameMap)
|
||||
{
|
||||
_mdl = mdl;
|
||||
_lod = lod;
|
||||
_meshIndex = meshIndex;
|
||||
|
||||
if (boneNameMap != null)
|
||||
_boneIndexMap = BuildBoneIndexMap(boneNameMap);
|
||||
|
||||
var usages = _mdl.VertexDeclarations[_meshIndex].VertexElements
|
||||
.Select(element => (MdlFile.VertexUsage)element.Usage)
|
||||
.ToImmutableHashSet();
|
||||
|
||||
_geometryType = GetGeometryType(usages);
|
||||
_skinningType = GetSkinningType(usages);
|
||||
}
|
||||
|
||||
private Dictionary<ushort, int> BuildBoneIndexMap(Dictionary<string, int> boneNameMap)
|
||||
{
|
||||
// todo: BoneTableIndex of 255 means null? if so, it should probably feed into the attributes we assign...
|
||||
var xivBoneTable = _mdl.BoneTables[Mesh.BoneTableIndex];
|
||||
|
||||
var indexMap = new Dictionary<ushort, int>();
|
||||
|
||||
foreach (var xivBoneIndex in xivBoneTable.BoneIndex.Take(xivBoneTable.BoneCount))
|
||||
{
|
||||
var boneName = _mdl.Bones[xivBoneIndex];
|
||||
if (!boneNameMap.TryGetValue(boneName, out var gltfBoneIndex))
|
||||
// TODO: handle - i think this is a hard failure, it means that a bone name in the model doesn't exist in the armature.
|
||||
throw new Exception($"looking for {boneName} in {string.Join(", ", boneNameMap.Keys)}");
|
||||
|
||||
indexMap.Add(xivBoneIndex, gltfBoneIndex);
|
||||
}
|
||||
|
||||
return indexMap;
|
||||
}
|
||||
|
||||
// TODO: consider a struct return type
|
||||
private IMeshBuilder<MaterialBuilder> BuildMesh()
|
||||
{
|
||||
var indices = BuildIndices();
|
||||
|
|
@ -47,7 +75,7 @@ public sealed class MeshConverter
|
|||
typeof(MaterialBuilder),
|
||||
_geometryType,
|
||||
typeof(VertexEmpty),
|
||||
typeof(VertexEmpty)
|
||||
_skinningType
|
||||
);
|
||||
var meshBuilder = (IMeshBuilder<MaterialBuilder>)Activator.CreateInstance(meshBuilderType, $"mesh{_meshIndex}")!;
|
||||
|
||||
|
|
@ -81,7 +109,7 @@ public sealed class MeshConverter
|
|||
private IReadOnlyList<IVertexBuilder> BuildVertices()
|
||||
{
|
||||
var vertexBuilderType = typeof(VertexBuilder<,,>)
|
||||
.MakeGenericType(_geometryType, typeof(VertexEmpty), typeof(VertexEmpty));
|
||||
.MakeGenericType(_geometryType, typeof(VertexEmpty), _skinningType);
|
||||
|
||||
// NOTE: This assumes that buffer streams are tightly packed, which has proven safe across tested files. If this assumption is broken, seeks will need to be moved into the vertex element loop.
|
||||
var streams = new BinaryReader[MaximumMeshBufferStreams];
|
||||
|
|
@ -107,8 +135,9 @@ public sealed class MeshConverter
|
|||
attributes[usage] = ReadVertexAttribute(streams[element.Stream], element);
|
||||
|
||||
var vertexGeometry = BuildVertexGeometry(attributes);
|
||||
var vertexSkinning = BuildVertexSkinning(attributes);
|
||||
|
||||
var vertexBuilder = (IVertexBuilder)Activator.CreateInstance(vertexBuilderType, vertexGeometry, new VertexEmpty(), new VertexEmpty())!;
|
||||
var vertexBuilder = (IVertexBuilder)Activator.CreateInstance(vertexBuilderType, vertexGeometry, new VertexEmpty(), vertexSkinning)!;
|
||||
vertices.Add(vertexBuilder);
|
||||
}
|
||||
|
||||
|
|
@ -167,6 +196,40 @@ public sealed class MeshConverter
|
|||
throw new Exception($"Unknown geometry type {_geometryType}.");
|
||||
}
|
||||
|
||||
private Type GetSkinningType(IReadOnlySet<MdlFile.VertexUsage> usages)
|
||||
{
|
||||
// TODO: possibly need to check only index - weight might be missing?
|
||||
if (usages.Contains(MdlFile.VertexUsage.BlendWeights) && usages.Contains(MdlFile.VertexUsage.BlendIndices))
|
||||
return typeof(VertexJoints4);
|
||||
|
||||
return typeof(VertexEmpty);
|
||||
}
|
||||
|
||||
private IVertexSkinning BuildVertexSkinning(IReadOnlyDictionary<MdlFile.VertexUsage, object> attributes)
|
||||
{
|
||||
if (_skinningType == typeof(VertexEmpty))
|
||||
return new VertexEmpty();
|
||||
|
||||
if (_skinningType == typeof(VertexJoints4))
|
||||
{
|
||||
// todo: this shouldn't happen... right? better approach?
|
||||
if (_boneIndexMap == null)
|
||||
throw new Exception("cannot build skinned vertex without index mapping");
|
||||
|
||||
var indices = ToByteArray(attributes[MdlFile.VertexUsage.BlendIndices]);
|
||||
var weights = ToVector4(attributes[MdlFile.VertexUsage.BlendWeights]);
|
||||
|
||||
// todo: if this throws on the bone index map, the mod is broken, as it contains weights for bones that do not exist.
|
||||
// i've not seen any of these that even tt can understand
|
||||
var bindings = Enumerable.Range(0, 4)
|
||||
.Select(index => (_boneIndexMap[indices[index]], weights[index]))
|
||||
.ToArray();
|
||||
return new VertexJoints4(bindings);
|
||||
}
|
||||
|
||||
throw new Exception($"Unknown skinning type {_skinningType}");
|
||||
}
|
||||
|
||||
// Some tangent W values that should be -1 are stored as 0.
|
||||
private Vector4 FixTangentVector(Vector4 tangent)
|
||||
=> tangent with { W = tangent.W == 1 ? 1 : -1 };
|
||||
|
|
@ -188,4 +251,11 @@ public sealed class MeshConverter
|
|||
Vector4 v4 => v4,
|
||||
_ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}")
|
||||
};
|
||||
|
||||
private byte[] ToByteArray(object data)
|
||||
=> data switch
|
||||
{
|
||||
byte[] value => value,
|
||||
_ => throw new ArgumentOutOfRangeException($"Invalid byte[] input {data}")
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,20 +75,24 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
|
|||
{
|
||||
var scene = new SceneBuilder();
|
||||
|
||||
var skeletonRoot = BuildSkeleton(cancel);
|
||||
if (skeletonRoot != null)
|
||||
scene.AddNode(skeletonRoot);
|
||||
var skeleton = BuildSkeleton(cancel);
|
||||
if (skeleton != null)
|
||||
scene.AddNode(skeleton.Value.Root);
|
||||
|
||||
// TODO: group by LoD in output tree
|
||||
for (byte lodIndex = 0; lodIndex < _mdl.LodCount; lodIndex++)
|
||||
{
|
||||
var lod = _mdl.Lods[lodIndex];
|
||||
|
||||
// TODO: consider other types?
|
||||
// TODO: consider other types of mesh?
|
||||
for (ushort meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++)
|
||||
{
|
||||
var meshBuilder = MeshConverter.ToGltf(_mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset));
|
||||
scene.AddRigidMesh(meshBuilder, Matrix4x4.Identity);
|
||||
var meshBuilder = MeshConverter.ToGltf(_mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset), skeleton?.Names);
|
||||
// TODO: use a value from the mesh converter for this check, rather than assuming that it has joints
|
||||
if (skeleton == null)
|
||||
scene.AddRigidMesh(meshBuilder, Matrix4x4.Identity);
|
||||
else
|
||||
scene.AddSkinnedMesh(meshBuilder, Matrix4x4.Identity, skeleton?.Joints);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +101,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
|
|||
}
|
||||
|
||||
// TODO: this should be moved to a seperate model converter or something
|
||||
private NodeBuilder? BuildSkeleton(CancellationToken cancel)
|
||||
private (NodeBuilder Root, NodeBuilder[] Joints, Dictionary<string, int> Names)? BuildSkeleton(CancellationToken cancel)
|
||||
{
|
||||
if (_sklb == null)
|
||||
return null;
|
||||
|
|
@ -114,15 +118,17 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
|
|||
|
||||
// this is (less) atrocious
|
||||
NodeBuilder? root = null;
|
||||
var boneMap = new Dictionary<string, NodeBuilder>();
|
||||
var names = new Dictionary<string, int>();
|
||||
var joints = new List<NodeBuilder>();
|
||||
for (var boneIndex = 0; boneIndex < skeleton.Bones.Length; boneIndex++)
|
||||
{
|
||||
var bone = skeleton.Bones[boneIndex];
|
||||
|
||||
if (boneMap.ContainsKey(bone.Name)) continue;
|
||||
if (names.ContainsKey(bone.Name)) continue;
|
||||
|
||||
var node = new NodeBuilder(bone.Name);
|
||||
boneMap[bone.Name] = node;
|
||||
names[bone.Name] = joints.Count;
|
||||
joints.Add(node);
|
||||
|
||||
node.SetLocalTransform(new AffineTransform(
|
||||
bone.Transform.Scale,
|
||||
|
|
@ -136,11 +142,14 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
|
|||
continue;
|
||||
}
|
||||
|
||||
var parent = boneMap[skeleton.Bones[bone.ParentIndex].Name];
|
||||
var parent = joints[names[skeleton.Bones[bone.ParentIndex].Name]];
|
||||
parent.AddNode(node);
|
||||
}
|
||||
|
||||
return root;
|
||||
if (root == null)
|
||||
return null;
|
||||
|
||||
return (root, joints.ToArray(), names);
|
||||
}
|
||||
|
||||
public bool Equals(IAction? other)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue