vpenades / SharpGLTF

glTF reader and writer for .NET Standard
MIT License
476 stars 75 forks source link

Armature being exported improperly #179

Closed NotNite closed 1 year ago

NotNite commented 1 year ago

Hiya,

Me and a few others are working on a project to export FINAL FANTASY XIV models to glTF. We decided to make use of SharpGLTF, and while it's been a lovely experience, we're having an issue with armatures and vertex groups.

The hyperlink in the first paragraph goes to the repository - you can check out the ModelConverter class (line ~290) on the model-export-but-broken branch (link).


For reference, model exporters already exist in this community (we're writing our own for a few reasons, glTF support being one of them) - as such, we're trying to match behavior as close as possible to the current FBX exporters. I'm using a model exported from one of them as a reference, and I'm happy to provide it if needed.

Our current code (the model-export branch) passes every bone to SceneBuilder.AddSkinnedMesh - this is not ideal as then there are hundreds of unused vertex groups on every mesh (left: other exporter, right: Xande):

My goal today was to only pass the bones that get referenced in the mesh weights to AddSkinnedMesh (on the model-export-but-broken branch). However, this seems to mess up the hierarchy of the scene, and breaks the armature.

Exporting with the previous iteration of the code (model-export), with all bones, looks like this:

But changing the code (model-export-but-broken) has the unintended side effect that the mesh is a child of... some random bone turned into an armature?, and some bones turn into regular empty nodes:

The vertex groups match the reference export, and the weights are correct, but the bones are ruined. The return value of AddSkinnedMesh (let's call it instance) reports the armature root is n_root (instance.Content.GetArmatureRoot().Name), which is the correct value.

Unfortunately, I'm at a loss on how to fix this. While exporting the model yourself would require a copy of the FFXIV game client, I'm happy to provide any model exports or breakpoint information.

Thanks for any help that can be provided!

vpenades commented 1 year ago

AddSkinnedMesh has three overloads, from what your're saying, you're using the first overload which uses a full NodeBuilder graph. That particular overload is used for simple use cases where you use all the nodes.

I think you have to use the other extensions, in particular the one that also requires an InverseBindMatrix per joint.

The thing is that you create a skeleton using NodeBuilder, and when you call AddSkinnedMesh, you pass only the joints of the skeleton used by the vertices, in the order referenced by the joint indices, so the index 0 referenced by the vertices will be the first joint you pass, and so on.

NotNite commented 1 year ago

AddSkinnedMesh has three overloads, from what your're saying, you're using the first overload which uses a full NodeBuilder graph. That particular overload is used for simple use cases where you use all the nodes.

I think you're confusing AddSkinnedMesh and AddRigidMesh? The overload I was using is AddSkinnedMesh(MESHBUILDER mesh, Matrix4x4 meshWorldMatrix, params NodeBuilder[] joints).

The thing is that you create a skeleton using NodeBuilder, and when you call AddSkinnedMesh, you pass only the joints of the skeleton used by the vertices, in the order referenced by the joint indices, so the index 0 referenced by the vertices will be the first joint you pass, and so on.

That's currently what I'm doing. The skinning is correct (vertex group & weights match), but the structure of the skeleton in the export is destroyed.

I think you have to use the other extensions, in particular the one that also requires an InverseBindMatrix per joint.

I've just tried using AddSkinnedMesh(MESHBUILDER mesh, params (NodeBuilder Joint, Matrix4x4 InverseBindMatrix)[] joints) - I don't know if I'm using it incorrectly, but it seems to change nothing.

vpenades commented 1 year ago

AddSkinnedMesh(MESHBUILDER mesh, params (NodeBuilder Joint, Matrix4x4 InverseBindMatrix)[] joints)

This one is the most "pure" overload, as it basically goes straight into glTF. the other two overloads are calling this one.

1- if the vertex data has joint indices that go from 0 to 79, you have to pass exactly 80 joints in the array. 2- The inverse bind matrix is extremely important, and if you're exporting from a game format, most probably that matrix is available somewhere. Otherwise, it's usually the inverse of the world matrix of the joint

debugging bonus: you can try to add the skeleton, and the main character mesh as a rigid mesh; when displayed in some editor, both the skeleton and the mesh should overlap. If that's not the case maybe it's because the skeleton is not being built correctly, or because missing inverse bind matrices. Or even easier: try exporting the skeleton alone, with no mesh, until you've confirmed the bones of the skeleton looks the same as the other exporters.

Keep in mind that skinning does not affect the bones in any way... the skinning is just the "glue" that binds the skeleton and the mesh

NotNite commented 1 year ago

I've made sure the skeleton lines up and the vertex data is being passed correctly. I think this may be a side effect of the fact that n_root is never referenced directly by the vertex data, so it never gets passed to AddSkinnedMesh, and thus it doesn't realize it's the root of the skeleton?

NotNite commented 1 year ago

Looking through some tools, the armature seems to be correct. I think this might just be a Blender bug, so I'm gonna close this. Whoops.

vpenades commented 1 year ago

I've made sure the skeleton lines up and the vertex data is being passed correctly. I think this may be a side effect of the fact that n_root is never referenced directly by the vertex data, so it never gets passed to AddSkinnedMesh, and thus it doesn't realize it's the root of the skeleton?

AddSkinnedMesh is smart enough to add the full skeleton to the scene whenever you just use a single node.

I always recomend using BabylonJS sandbox to preview the glTFs and ensure they're good to go.