Resolve bind poses from FBX clusters instead of FBX poses

Turns out that the information in FBX Pose objects is relatively often broken.
Using skin cluster bind poses seems more reliable, but cannot capture the bind pose of the root bone.
This commit is contained in:
bqqbarbhg 2024-04-23 02:56:54 +03:00
parent 11d3768132
commit 095569011c

View file

@ -86,6 +86,17 @@ static Quaternion _as_quaternion(const ufbx_quat &p_quat) {
return Quaternion(real_t(p_quat.x), real_t(p_quat.y), real_t(p_quat.z), real_t(p_quat.w));
}
static Transform3D _as_transform(const ufbx_transform &p_xform) {
Transform3D result;
result.origin = FBXDocument::_as_vec3(p_xform.translation);
result.basis.set_quaternion_scale(_as_quaternion(p_xform.rotation), FBXDocument::_as_vec3(p_xform.scale));
return result;
}
static real_t _relative_error(const Vector3 &p_a, const Vector3 &p_b) {
return p_a.distance_to(p_b) / MAX(p_a.length(), p_b.length());
}
static Color _material_color(const ufbx_material_map &p_map) {
if (p_map.value_components == 1) {
float r = float(p_map.value_real);
@ -196,6 +207,16 @@ static uint32_t _decode_vertex_index(const Vector3 &p_vertex) {
return uint32_t(p_vertex.x) | uint32_t(p_vertex.y) << 16;
}
static ufbx_skin_deformer *_find_skin_deformer(ufbx_skin_cluster *p_cluster) {
for (const ufbx_connection &conn : p_cluster->element.connections_src) {
ufbx_skin_deformer *deformer = ufbx_as_skin_deformer(conn.dst);
if (deformer) {
return deformer;
}
}
return nullptr;
}
struct ThreadPoolFBX {
struct Group {
ufbx_thread_pool_context ctx = {};
@ -333,23 +354,67 @@ Error FBXDocument::_parse_nodes(Ref<FBXState> p_state) {
}
{
node->transform.origin = _as_vec3(fbx_node->local_transform.translation);
node->transform.basis.set_quaternion_scale(_as_quaternion(fbx_node->local_transform.rotation), _as_vec3(fbx_node->local_transform.scale));
node->transform = _as_transform(fbx_node->local_transform);
if (fbx_node->bind_pose) {
ufbx_bone_pose *pose = ufbx_get_bone_pose(fbx_node->bind_pose, fbx_node);
ufbx_transform rest_transform = ufbx_matrix_to_transform(&pose->bone_to_parent);
bool found_rest_xform = false;
bool bad_rest_xform = false;
Transform3D candidate_rest_xform;
Vector3 rest_position = _as_vec3(rest_transform.translation);
Quaternion rest_rotation = _as_quaternion(rest_transform.rotation);
Vector3 rest_scale = _as_vec3(rest_transform.scale);
Transform3D godot_rest_xform;
godot_rest_xform.basis.set_quaternion_scale(rest_rotation, rest_scale);
godot_rest_xform.origin = rest_position;
node->set_additional_data("GODOT_rest_transform", godot_rest_xform);
} else {
node->set_additional_data("GODOT_rest_transform", node->transform);
if (fbx_node->parent) {
// Attempt to resolve a rest pose for bones: This uses internal FBX connections to find
// all skin clusters connected to the bone.
for (const ufbx_connection &child_conn : fbx_node->element.connections_src) {
ufbx_skin_cluster *child_cluster = ufbx_as_skin_cluster(child_conn.dst);
if (!child_cluster)
continue;
ufbx_skin_deformer *child_deformer = _find_skin_deformer(child_cluster);
if (!child_deformer)
continue;
// Found a skin cluster: Now iterate through all the skin clusters of the parent and
// try to find one that used by the same deformer.
for (const ufbx_connection &parent_conn : fbx_node->parent->element.connections_src) {
ufbx_skin_cluster *parent_cluster = ufbx_as_skin_cluster(parent_conn.dst);
if (!parent_cluster)
continue;
ufbx_skin_deformer *parent_deformer = _find_skin_deformer(parent_cluster);
if (parent_deformer != child_deformer)
continue;
// Success: Found two skin clusters from the same deformer, now we can resolve the
// local bind pose from the difference between the two world-space bind poses.
ufbx_matrix child_to_world = child_cluster->bind_to_world;
ufbx_matrix world_to_parent = ufbx_matrix_invert(&parent_cluster->bind_to_world);
ufbx_matrix child_to_parent = ufbx_matrix_mul(&world_to_parent, &child_to_world);
Transform3D xform = _as_transform(ufbx_matrix_to_transform(&child_to_parent));
if (!found_rest_xform) {
// Found the first bind pose for the node, assume that this one is good
found_rest_xform = true;
candidate_rest_xform = xform;
} else if (!bad_rest_xform) {
// Found another: Let's hope it's similar to the previous one, if not warn and
// use the initial pose, which is used by default if rest pose is not found.
real_t error = 0.0f;
error += _relative_error(candidate_rest_xform.origin, xform.origin);
for (int i = 0; i < 3; i++) {
error += _relative_error(candidate_rest_xform.basis.rows[i], xform.basis.rows[i]);
}
const real_t max_error = 0.01f;
if (error >= max_error) {
WARN_PRINT(vformat("FBX: Node '%s' has multiple bind poses, using initial pose as rest pose.", node->get_name()));
bad_rest_xform = true;
}
}
}
}
}
Transform3D godot_rest_xform = node->transform;
if (found_rest_xform && !bad_rest_xform) {
godot_rest_xform = candidate_rest_xform;
}
node->set_additional_data("GODOT_rest_transform", godot_rest_xform);
}
for (const ufbx_node *child : fbx_node->children) {