FBX Export: reconstruct full skeleton for any FBX deformers.
parent
9287adb735
commit
249f1844ae
|
@ -1463,46 +1463,67 @@ void FBXExporter::WriteObjects ()
|
||||||
// one sticky point is that the number of vertices may not match,
|
// one sticky point is that the number of vertices may not match,
|
||||||
// because assimp splits vertices by normal, uv, etc.
|
// because assimp splits vertices by normal, uv, etc.
|
||||||
|
|
||||||
// first we should mark all the skeleton nodes,
|
// first we should mark the skeleton for each mesh.
|
||||||
// so that they can be treated as LimbNode in stead of Mesh or Null.
|
// the skeleton must include not only the aiBones,
|
||||||
// at the same time we can build up a map of bone nodes.
|
// but also all their parent nodes.
|
||||||
|
// anything that affects the position of any bone node must be included.
|
||||||
|
std::vector<std::set<const aiNode*>> skeleton_by_mesh(mScene->mNumMeshes);
|
||||||
|
// at the same time we can build a list of all the skeleton nodes,
|
||||||
|
// which will be used later to mark them as type "limbNode".
|
||||||
std::unordered_set<const aiNode*> limbnodes;
|
std::unordered_set<const aiNode*> limbnodes;
|
||||||
|
// and a map of nodes by bone name, as finding them is annoying.
|
||||||
std::map<std::string,aiNode*> node_by_bone;
|
std::map<std::string,aiNode*> node_by_bone;
|
||||||
for (size_t mi = 0; mi < mScene->mNumMeshes; ++mi) {
|
for (size_t mi = 0; mi < mScene->mNumMeshes; ++mi) {
|
||||||
const aiMesh* m = mScene->mMeshes[mi];
|
const aiMesh* m = mScene->mMeshes[mi];
|
||||||
|
std::set<const aiNode*> skeleton;
|
||||||
for (size_t bi =0; bi < m->mNumBones; ++bi) {
|
for (size_t bi =0; bi < m->mNumBones; ++bi) {
|
||||||
const aiBone* b = m->mBones[bi];
|
const aiBone* b = m->mBones[bi];
|
||||||
const std::string name(b->mName.C_Str());
|
const std::string name(b->mName.C_Str());
|
||||||
if (node_by_bone.count(name) > 0) {
|
auto elem = node_by_bone.find(name);
|
||||||
// already processed, skip
|
aiNode* n;
|
||||||
continue;
|
if (elem != node_by_bone.end()) {
|
||||||
|
n = elem->second;
|
||||||
|
} else {
|
||||||
|
n = mScene->mRootNode->FindNode(b->mName);
|
||||||
|
if (!n) {
|
||||||
|
// this should never happen
|
||||||
|
std::stringstream err;
|
||||||
|
err << "Failed to find node for bone: \"" << name << "\"";
|
||||||
|
throw DeadlyExportError(err.str());
|
||||||
|
}
|
||||||
|
node_by_bone[name] = n;
|
||||||
|
limbnodes.insert(n);
|
||||||
}
|
}
|
||||||
aiNode* n = mScene->mRootNode->FindNode(b->mName);
|
skeleton.insert(n);
|
||||||
if (!n) {
|
|
||||||
// this should never happen
|
|
||||||
std::stringstream err;
|
|
||||||
err << "Failed to find node for bone: \"" << name << "\"";
|
|
||||||
throw DeadlyExportError(err.str());
|
|
||||||
}
|
|
||||||
node_by_bone[name] = n;
|
|
||||||
limbnodes.insert(n);
|
|
||||||
if (n == mScene->mRootNode) { continue; }
|
|
||||||
// mark all parent nodes as skeleton as well,
|
// mark all parent nodes as skeleton as well,
|
||||||
// up until we find the root node,
|
// up until we find the root node,
|
||||||
// or else the node containing the mesh,
|
// or else the node containing the mesh,
|
||||||
// or else the parent of a node containig the mesh.
|
// or else the parent of a node containig the mesh.
|
||||||
for (
|
for (
|
||||||
const aiNode* parent = n->mParent;
|
const aiNode* parent = n->mParent;
|
||||||
parent != mScene->mRootNode;
|
parent && parent != mScene->mRootNode;
|
||||||
parent = parent->mParent
|
parent = parent->mParent
|
||||||
) {
|
) {
|
||||||
|
// if we've already done this node we can skip it all
|
||||||
|
if (skeleton.count(parent)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// ignore fbx transform nodes as these will be collapsed later
|
||||||
|
// TODO: cache this by aiNode*
|
||||||
|
const std::string node_name(parent->mName.C_Str());
|
||||||
|
if (node_name.find(MAGIC_NODE_TAG) != std::string::npos) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// otherwise check if this is the root of the skeleton
|
||||||
bool end = false;
|
bool end = false;
|
||||||
|
// is the mesh part of this node?
|
||||||
for (size_t i = 0; i < parent->mNumMeshes; ++i) {
|
for (size_t i = 0; i < parent->mNumMeshes; ++i) {
|
||||||
if (parent->mMeshes[i] == mi) {
|
if (parent->mMeshes[i] == mi) {
|
||||||
end = true;
|
end = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// is the mesh in one of the children of this node?
|
||||||
for (size_t j = 0; j < parent->mNumChildren; ++j) {
|
for (size_t j = 0; j < parent->mNumChildren; ++j) {
|
||||||
aiNode* child = parent->mChildren[j];
|
aiNode* child = parent->mChildren[j];
|
||||||
for (size_t i = 0; i < child->mNumMeshes; ++i) {
|
for (size_t i = 0; i < child->mNumMeshes; ++i) {
|
||||||
|
@ -1513,27 +1534,23 @@ void FBXExporter::WriteObjects ()
|
||||||
}
|
}
|
||||||
if (end) { break; }
|
if (end) { break; }
|
||||||
}
|
}
|
||||||
if (end) { break; }
|
|
||||||
limbnodes.insert(parent);
|
limbnodes.insert(parent);
|
||||||
|
skeleton.insert(parent);
|
||||||
|
// if it was the skeleton root we can finish here
|
||||||
|
if (end) { break; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
skeleton_by_mesh[mi] = skeleton;
|
||||||
}
|
}
|
||||||
|
|
||||||
// we'll need the uids for the bone nodes, so generate them now
|
// we'll need the uids for the bone nodes, so generate them now
|
||||||
std::map<std::string,int64_t> bone_uids;
|
for (size_t i = 0; i < mScene->mNumMeshes; ++i) {
|
||||||
for (auto &bone : limbnodes) {
|
auto &s = skeleton_by_mesh[i];
|
||||||
std::string bone_name(bone->mName.C_Str());
|
for (const aiNode* n : s) {
|
||||||
aiNode* bone_node = mScene->mRootNode->FindNode(bone->mName);
|
auto elem = node_uids.find(n);
|
||||||
if (!bone_node) {
|
if (elem == node_uids.end()) {
|
||||||
throw DeadlyExportError("Couldn't find node for bone" + bone_name);
|
node_uids[n] = generate_uid();
|
||||||
}
|
}
|
||||||
auto elem = node_uids.find(bone_node);
|
|
||||||
if (elem == node_uids.end()) {
|
|
||||||
int64_t uid = generate_uid();
|
|
||||||
node_uids[bone_node] = uid;
|
|
||||||
bone_uids[bone_name] = uid;
|
|
||||||
} else {
|
|
||||||
bone_uids[bone_name] = elem->second;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1585,6 +1602,9 @@ void FBXExporter::WriteObjects ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO, FIXME: this won't work if anything is not in the bind pose.
|
||||||
|
// for now if such a situation is detected, we throw an exception.
|
||||||
|
|
||||||
// first get this mesh's position in world space,
|
// first get this mesh's position in world space,
|
||||||
// as we'll need it for each subdeformer.
|
// as we'll need it for each subdeformer.
|
||||||
//
|
//
|
||||||
|
@ -1597,12 +1617,23 @@ void FBXExporter::WriteObjects ()
|
||||||
// but there's no guarantee that the bone is in the bindpose,
|
// but there's no guarantee that the bone is in the bindpose,
|
||||||
// so this would be even less reliable.
|
// so this would be even less reliable.
|
||||||
aiNode* mesh_node = get_node_for_mesh(mi, mScene->mRootNode);
|
aiNode* mesh_node = get_node_for_mesh(mi, mScene->mRootNode);
|
||||||
aiMatrix4x4 mesh_node_xform = get_world_transform(mesh_node, mScene);
|
aiMatrix4x4 mesh_xform = get_world_transform(mesh_node, mScene);
|
||||||
|
|
||||||
// now make a subdeformer for each bone
|
// now make a subdeformer for each bone in the skeleton
|
||||||
for (size_t bi =0; bi < m->mNumBones; ++bi) {
|
const std::set<const aiNode*> &skeleton = skeleton_by_mesh[mi];
|
||||||
const aiBone* b = m->mBones[bi];
|
for (const aiNode* bone_node : skeleton) {
|
||||||
const std::string name(b->mName.C_Str());
|
// if there's a bone for this node, find it
|
||||||
|
const aiBone* b = nullptr;
|
||||||
|
for (size_t bi = 0; bi < m->mNumBones; ++bi) {
|
||||||
|
// TODO: this probably should index by something else
|
||||||
|
const std::string name(m->mBones[bi]->mName.C_Str());
|
||||||
|
if (node_by_bone[name] == bone_node) {
|
||||||
|
b = m->mBones[bi];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// start the subdeformer node
|
||||||
const int64_t subdeformer_uid = generate_uid();
|
const int64_t subdeformer_uid = generate_uid();
|
||||||
FBX::Node sdnode("Deformer");
|
FBX::Node sdnode("Deformer");
|
||||||
sdnode.AddProperties(
|
sdnode.AddProperties(
|
||||||
|
@ -1611,43 +1642,57 @@ void FBXExporter::WriteObjects ()
|
||||||
sdnode.AddChild("Version", int32_t(100));
|
sdnode.AddChild("Version", int32_t(100));
|
||||||
sdnode.AddChild("UserData", "", "");
|
sdnode.AddChild("UserData", "", "");
|
||||||
|
|
||||||
// get indices and weights
|
// add indices and weights, if any
|
||||||
std::vector<int32_t> subdef_indices;
|
if (b) {
|
||||||
std::vector<double> subdef_weights;
|
std::vector<int32_t> subdef_indices;
|
||||||
int32_t last_index = -1;
|
std::vector<double> subdef_weights;
|
||||||
for (size_t wi = 0; wi < b->mNumWeights; ++wi) {
|
int32_t last_index = -1;
|
||||||
int32_t vi = vertex_indices[b->mWeights[wi].mVertexId];
|
for (size_t wi = 0; wi < b->mNumWeights; ++wi) {
|
||||||
if (vi == last_index) {
|
int32_t vi = vertex_indices[b->mWeights[wi].mVertexId];
|
||||||
// only for vertices we exported to fbx
|
if (vi == last_index) {
|
||||||
// TODO, FIXME: this assumes identically-located vertices
|
// only for vertices we exported to fbx
|
||||||
// will always deform in the same way.
|
// TODO, FIXME: this assumes identically-located vertices
|
||||||
// as assimp doesn't store a separate list of "positions",
|
// will always deform in the same way.
|
||||||
// there's not much that can be done about this
|
// as assimp doesn't store a separate list of "positions",
|
||||||
// other than assuming that identical position means
|
// there's not much that can be done about this
|
||||||
// identical vertex.
|
// other than assuming that identical position means
|
||||||
continue;
|
// identical vertex.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
subdef_indices.push_back(vi);
|
||||||
|
subdef_weights.push_back(b->mWeights[wi].mWeight);
|
||||||
|
last_index = vi;
|
||||||
}
|
}
|
||||||
subdef_indices.push_back(vi);
|
// yes, "indexes"
|
||||||
subdef_weights.push_back(b->mWeights[wi].mWeight);
|
sdnode.AddChild("Indexes", subdef_indices);
|
||||||
last_index = vi;
|
sdnode.AddChild("Weights", subdef_weights);
|
||||||
}
|
}
|
||||||
// yes, "indexes"
|
|
||||||
sdnode.AddChild("Indexes", subdef_indices);
|
// transform is the transform of the mesh, but in bone space.
|
||||||
sdnode.AddChild("Weights", subdef_weights);
|
// To get it we take the inverse of the world-space bone transform,
|
||||||
// transform is the transform of the mesh, but in bone space...
|
// and multiply by the world-space transform of the mesh.
|
||||||
// which is exactly what assimp's mOffsetMatrix is,
|
aiMatrix4x4 bone_xform = get_world_transform(bone_node, mScene);
|
||||||
// no matter what the assimp docs may say.
|
aiMatrix4x4 inverse_bone_xform = bone_xform;
|
||||||
aiMatrix4x4 tr = b->mOffsetMatrix;
|
inverse_bone_xform.Inverse();
|
||||||
|
aiMatrix4x4 tr = inverse_bone_xform * mesh_xform;
|
||||||
sdnode.AddChild("Transform", tr);
|
sdnode.AddChild("Transform", tr);
|
||||||
|
|
||||||
|
// this should match assimp's mOffsetMatrix.
|
||||||
|
// if it doesn't then we have a problem.
|
||||||
|
// as assimp doesn't store a mOffsetMatrix for bones with 0 weight
|
||||||
|
// we have no way of reconstructing that information.
|
||||||
|
const float epsilon = 1e-5; // some error is to be expected
|
||||||
|
if (b && ! tr.Equal(b->mOffsetMatrix, epsilon)) {
|
||||||
|
std::stringstream err;
|
||||||
|
err << "transform matrix for bone \"" << b->mName.C_Str();
|
||||||
|
err << "\" does not match mOffsetMatrix!";
|
||||||
|
err << " Bones *must* be in the bind pose to export.";
|
||||||
|
throw DeadlyExportError(err.str());
|
||||||
|
}
|
||||||
|
|
||||||
// transformlink should be the position of the bone in world space,
|
// transformlink should be the position of the bone in world space,
|
||||||
// in the bind pose.
|
// which we just calculated.
|
||||||
// For now let's use the inverse of mOffsetMatrix,
|
sdnode.AddChild("TransformLink", bone_xform);
|
||||||
// and the (assumedly static) mesh position in world space.
|
|
||||||
// TODO: find a better way of doing this? there aren't many options
|
|
||||||
tr = b->mOffsetMatrix;
|
|
||||||
tr.Inverse();
|
|
||||||
tr *= mesh_node_xform;
|
|
||||||
sdnode.AddChild("TransformLink", tr);
|
|
||||||
|
|
||||||
// done
|
// done
|
||||||
sdnode.Dump(outstream);
|
sdnode.Dump(outstream);
|
||||||
|
@ -1659,7 +1704,7 @@ void FBXExporter::WriteObjects ()
|
||||||
|
|
||||||
// we also need to connect the limb node to the subdeformer.
|
// we also need to connect the limb node to the subdeformer.
|
||||||
c = FBX::Node("C");
|
c = FBX::Node("C");
|
||||||
c.AddProperties("OO", bone_uids[name], subdeformer_uid);
|
c.AddProperties("OO", node_uids[bone_node], subdeformer_uid);
|
||||||
connections.push_back(c); // TODO: emplace_back
|
connections.push_back(c); // TODO: emplace_back
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1753,7 +1798,7 @@ void FBXExporter::WriteObjects ()
|
||||||
// write nodes (i.e. model heirarchy)
|
// write nodes (i.e. model heirarchy)
|
||||||
// start at root node
|
// start at root node
|
||||||
WriteModelNodes(
|
WriteModelNodes(
|
||||||
outstream, mScene->mRootNode, 0, bone_uids
|
outstream, mScene->mRootNode, 0, limbnodes
|
||||||
);
|
);
|
||||||
|
|
||||||
object_node.End(outstream, true);
|
object_node.End(outstream, true);
|
||||||
|
@ -1864,17 +1909,17 @@ void FBXExporter::WriteModelNodes(
|
||||||
StreamWriterLE& s,
|
StreamWriterLE& s,
|
||||||
const aiNode* node,
|
const aiNode* node,
|
||||||
int64_t parent_uid,
|
int64_t parent_uid,
|
||||||
const std::map<std::string,int64_t>& bone_uids
|
const std::unordered_set<const aiNode*>& limbnodes
|
||||||
) {
|
) {
|
||||||
std::vector<std::pair<std::string,aiVector3D>> chain;
|
std::vector<std::pair<std::string,aiVector3D>> chain;
|
||||||
WriteModelNodes(s, node, parent_uid, bone_uids, chain);
|
WriteModelNodes(s, node, parent_uid, limbnodes, chain);
|
||||||
}
|
}
|
||||||
|
|
||||||
void FBXExporter::WriteModelNodes(
|
void FBXExporter::WriteModelNodes(
|
||||||
StreamWriterLE& outstream,
|
StreamWriterLE& outstream,
|
||||||
const aiNode* node,
|
const aiNode* node,
|
||||||
int64_t parent_uid,
|
int64_t parent_uid,
|
||||||
const std::map<std::string,int64_t>& bone_uids,
|
const std::unordered_set<const aiNode*>& limbnodes,
|
||||||
std::vector<std::pair<std::string,aiVector3D>>& transform_chain
|
std::vector<std::pair<std::string,aiVector3D>>& transform_chain
|
||||||
) {
|
) {
|
||||||
// first collapse any expanded transformation chains created by FBX import.
|
// first collapse any expanded transformation chains created by FBX import.
|
||||||
|
@ -1924,7 +1969,7 @@ void FBXExporter::WriteModelNodes(
|
||||||
}
|
}
|
||||||
// now just continue to the next node
|
// now just continue to the next node
|
||||||
WriteModelNodes(
|
WriteModelNodes(
|
||||||
outstream, next_node, parent_uid, bone_uids, transform_chain
|
outstream, next_node, parent_uid, limbnodes, transform_chain
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1962,7 +2007,7 @@ void FBXExporter::WriteModelNodes(
|
||||||
connections.push_back(c);
|
connections.push_back(c);
|
||||||
// write model node
|
// write model node
|
||||||
WriteModelNode(outstream, node, node_uid, "Mesh", transform_chain);
|
WriteModelNode(outstream, node, node_uid, "Mesh", transform_chain);
|
||||||
} else if (bone_uids.count(node_name)) {
|
} else if (limbnodes.count(node)) {
|
||||||
WriteModelNode(outstream, node, node_uid, "LimbNode", transform_chain);
|
WriteModelNode(outstream, node, node_uid, "LimbNode", transform_chain);
|
||||||
// we also need to write a nodeattribute to mark it as a skeleton
|
// we also need to write a nodeattribute to mark it as a skeleton
|
||||||
int64_t node_attribute_uid = generate_uid();
|
int64_t node_attribute_uid = generate_uid();
|
||||||
|
@ -2021,7 +2066,7 @@ void FBXExporter::WriteModelNodes(
|
||||||
// now recurse into children
|
// now recurse into children
|
||||||
for (size_t i = 0; i < node->mNumChildren; ++i) {
|
for (size_t i = 0; i < node->mNumChildren; ++i) {
|
||||||
WriteModelNodes(
|
WriteModelNodes(
|
||||||
outstream, node->mChildren[i], node_uid, bone_uids
|
outstream, node->mChildren[i], node_uid, limbnodes
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <unordered_set>
|
||||||
#include <memory> // shared_ptr
|
#include <memory> // shared_ptr
|
||||||
#include <sstream> // stringstream
|
#include <sstream> // stringstream
|
||||||
|
|
||||||
|
@ -129,13 +130,13 @@ namespace Assimp
|
||||||
Assimp::StreamWriterLE& s,
|
Assimp::StreamWriterLE& s,
|
||||||
const aiNode* node,
|
const aiNode* node,
|
||||||
int64_t parent_uid,
|
int64_t parent_uid,
|
||||||
const std::map<std::string,int64_t>& bone_uids
|
const std::unordered_set<const aiNode*>& limbnodes
|
||||||
);
|
);
|
||||||
void WriteModelNodes( // usually don't call this directly
|
void WriteModelNodes( // usually don't call this directly
|
||||||
StreamWriterLE& s,
|
StreamWriterLE& s,
|
||||||
const aiNode* node,
|
const aiNode* node,
|
||||||
int64_t parent_uid,
|
int64_t parent_uid,
|
||||||
const std::map<std::string,int64_t>& bone_uids,
|
const std::unordered_set<const aiNode*>& limbnodes,
|
||||||
std::vector<std::pair<std::string,aiVector3D>>& transform_chain
|
std::vector<std::pair<std::string,aiVector3D>>& transform_chain
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue