Fix #138795: FBX importer was not handling armatures with non-bone nodes
File under #138795 showed several issues, which, while investigating them, led to also fixing some other issues. - FBX files can contain non-bone nodes in between actual bone nodes ("fake bones" as they used to be called in Python importer). Handling this case was missing in the new importer. - Due to above, some armatures had what appeared like multiple "root bones" inside them, which led to crashes while importing animations. - Meshes with multiple armature modifiers (multiple skin deformers in FBX) were not handled correctly, see https://projects.blender.org/blender/blender-addons/issues/45171 for when the same issue was fixed in the Python importer. Extended test coverage to encompass the above. Pull Request: https://projects.blender.org/blender/blender/pulls/138992
This commit is contained in:
committed by
Aras Pranckevicius
parent
cef7cb4534
commit
de5d0cfdc5
@@ -155,6 +155,7 @@ void FbxImportContext::import_cameras()
|
||||
}
|
||||
node_matrix_to_obj(node, obj, this->mapping);
|
||||
this->mapping.el_to_object.add(&node->element, obj);
|
||||
this->mapping.imported_objects.add(obj);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +207,7 @@ void FbxImportContext::import_lights()
|
||||
}
|
||||
node_matrix_to_obj(node, obj, this->mapping);
|
||||
this->mapping.el_to_object.add(&node->element, obj);
|
||||
this->mapping.imported_objects.add(obj);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +221,9 @@ void FbxImportContext::import_empties()
|
||||
/* Create empties for fbx nodes. */
|
||||
for (const ufbx_node *node : this->fbx.nodes) {
|
||||
/* Ignore root, bones and nodes for which we have created objects already. */
|
||||
if (node->is_root || node->bone || this->mapping.el_to_object.contains(&node->element)) {
|
||||
if (node->is_root || this->mapping.node_is_blender_bone.contains(node) ||
|
||||
this->mapping.el_to_object.contains(&node->element))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Object *obj = BKE_object_add_only_object(this->bmain, OB_EMPTY, get_fbx_name(node->name));
|
||||
@@ -232,6 +236,7 @@ void FbxImportContext::import_empties()
|
||||
}
|
||||
node_matrix_to_obj(node, obj, this->mapping);
|
||||
this->mapping.el_to_object.add(&node->element, obj);
|
||||
this->mapping.imported_objects.add(obj);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,11 +258,6 @@ void FbxImportContext::setup_hierarchy()
|
||||
if (node == nullptr) {
|
||||
continue;
|
||||
}
|
||||
if (node->bone != nullptr && !node->bone->is_root) {
|
||||
/* If this node is for a non-root bone, do not try to setup object parenting
|
||||
* for it (the object for bone bones is whole armature). */
|
||||
continue;
|
||||
}
|
||||
if (node->parent) {
|
||||
Object *obj_par = this->mapping.el_to_object.lookup_default(&node->parent->element, nullptr);
|
||||
if (!ELEM(obj_par, nullptr, item.value)) {
|
||||
@@ -390,14 +390,14 @@ void importer_main(Main *bmain, Scene *scene, ViewLayer *view_layer, const FBXIm
|
||||
ufbx_free_scene(fbx);
|
||||
|
||||
/* Add objects to collection. */
|
||||
for (Object *obj : ctx.mapping.el_to_object.values()) {
|
||||
for (Object *obj : ctx.mapping.imported_objects) {
|
||||
BKE_collection_object_add(bmain, lc->collection, obj);
|
||||
}
|
||||
|
||||
/* Select objects, sync layers etc. */
|
||||
BKE_view_layer_base_deselect_all(scene, view_layer);
|
||||
BKE_view_layer_synced_ensure(scene, view_layer);
|
||||
for (Object *obj : ctx.mapping.el_to_object.values()) {
|
||||
for (Object *obj : ctx.mapping.imported_objects) {
|
||||
Base *base = BKE_view_layer_base_find(view_layer, obj);
|
||||
BKE_view_layer_base_select_and_set_active(view_layer, base);
|
||||
|
||||
|
||||
@@ -129,8 +129,15 @@ static Vector<ElementAnimations> gather_animated_properties(const FbxElementMapp
|
||||
}
|
||||
}
|
||||
else {
|
||||
/* Animating Object property. */
|
||||
Object *obj = mapping.el_to_object.lookup_default(fprop.element, nullptr);
|
||||
/* Animating Bone/Armature/Object property. */
|
||||
const ufbx_node *fnode = ufbx_as_node(fprop.element);
|
||||
Object *obj = nullptr;
|
||||
if (fnode) {
|
||||
obj = mapping.bone_to_armature.lookup_default(fnode, nullptr);
|
||||
}
|
||||
if (obj == nullptr) {
|
||||
obj = mapping.el_to_object.lookup_default(fprop.element, nullptr);
|
||||
}
|
||||
if (obj == nullptr) {
|
||||
continue;
|
||||
}
|
||||
@@ -199,11 +206,10 @@ static void create_transform_curve_desc(const FbxElementMapping &mapping,
|
||||
{
|
||||
/* For animated bones, prepend bone path to animation curve path. */
|
||||
std::string rna_prefix;
|
||||
bool is_bone = false;
|
||||
std::string group_name_str = get_fbx_name(anim.fbx_elem->name);
|
||||
const ufbx_node *fnode = ufbx_as_node(anim.fbx_elem);
|
||||
if (fnode != nullptr && fnode->bone != nullptr && !fnode->bone->is_root) {
|
||||
is_bone = true;
|
||||
const bool is_bone = mapping.node_is_blender_bone.contains(fnode);
|
||||
if (is_bone) {
|
||||
group_name_str = mapping.node_to_name.lookup_default(fnode, "");
|
||||
rna_prefix = std::string("pose.bones[\"") + group_name_str + "\"].";
|
||||
}
|
||||
@@ -252,12 +258,10 @@ static void create_transform_curve_data(const FbxElementMapping &mapping,
|
||||
const float anim_offset,
|
||||
FCurve **curves)
|
||||
{
|
||||
bool is_bone = false;
|
||||
const ufbx_node *fnode = ufbx_as_node(anim.fbx_elem);
|
||||
ufbx_matrix bone_xform = ufbx_identity_matrix;
|
||||
if (fnode != nullptr && fnode->bone != nullptr && !fnode->bone->is_root) {
|
||||
is_bone = true;
|
||||
|
||||
const bool is_bone = mapping.node_is_blender_bone.contains(fnode);
|
||||
if (is_bone) {
|
||||
/* Bone transform curves need to be transformed to the bind transform
|
||||
* in joint-local space:
|
||||
* - Calculate local space bind matrix: inv(parent_bind) * bind
|
||||
@@ -273,10 +277,8 @@ static void create_transform_curve_data(const FbxElementMapping &mapping,
|
||||
}
|
||||
}
|
||||
|
||||
bool found = false;
|
||||
bone_xform = mapping.calc_local_bind_matrix(fnode, world_to_arm, found);
|
||||
bone_xform = mapping.calc_local_bind_matrix(fnode, world_to_arm);
|
||||
bone_xform = ufbx_matrix_invert(&bone_xform);
|
||||
BLI_assert_msg(found, "fbx: did not find bind matrix for bone curve");
|
||||
}
|
||||
|
||||
int rot_channels = 3;
|
||||
@@ -338,6 +340,7 @@ static void create_transform_curve_data(const FbxElementMapping &mapping,
|
||||
int64_t scale_index = rot_index + rot_channels;
|
||||
int64_t tot_curves = scale_index + 3;
|
||||
for (int64_t i = 0; i < tot_curves; i++) {
|
||||
BLI_assert_msg(curves[i], "fbx: animation curve was not created successfully");
|
||||
BKE_fcurve_bezt_resize(curves[i], sorted_key_times.size());
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ struct ArmatureImportContext {
|
||||
Object *create_armature_for_node(const ufbx_node *node);
|
||||
void create_armature_bones(const ufbx_node *node,
|
||||
Object *arm_obj,
|
||||
Set<const ufbx_node *> &arm_bones,
|
||||
const Set<const ufbx_node *> &bone_nodes,
|
||||
EditBone *parent_bone,
|
||||
const ufbx_matrix &parent_mtx,
|
||||
const ufbx_matrix &world_to_arm,
|
||||
@@ -51,25 +51,20 @@ struct ArmatureImportContext {
|
||||
|
||||
Object *ArmatureImportContext::create_armature_for_node(const ufbx_node *node)
|
||||
{
|
||||
Object *obj = nullptr;
|
||||
if (node != nullptr) {
|
||||
obj = this->mapping.el_to_object.lookup_default(&node->element, nullptr);
|
||||
if (obj != nullptr && obj->type == OB_ARMATURE) {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
BLI_assert_msg(node != nullptr, "fbx: node for armature creation should not be null");
|
||||
|
||||
const char *arm_name = node ? get_fbx_name(node->name, "Armature") : "Armature";
|
||||
const char *obj_name = node ? get_fbx_name(node->name, "Armature") : "Armature";
|
||||
const char *arm_name = get_fbx_name(node->name, "Armature");
|
||||
const char *obj_name = get_fbx_name(node->name, "Armature");
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file, "create ARMATURE %s\n", arm_name);
|
||||
#endif
|
||||
|
||||
bArmature *arm = BKE_armature_add(&this->bmain, arm_name);
|
||||
obj = BKE_object_add_only_object(&this->bmain, OB_ARMATURE, obj_name);
|
||||
Object *obj = BKE_object_add_only_object(&this->bmain, OB_ARMATURE, obj_name);
|
||||
obj->dtx |= OB_DRAW_IN_FRONT;
|
||||
obj->data = arm;
|
||||
if (node != nullptr) {
|
||||
this->mapping.imported_objects.add(obj);
|
||||
if (!node->is_root) {
|
||||
this->mapping.el_to_object.add(&node->element, obj);
|
||||
if (this->params.use_custom_props) {
|
||||
read_custom_properties(node->props, obj->id, this->params.props_enum_as_string);
|
||||
@@ -104,55 +99,39 @@ Object *ArmatureImportContext::create_armature_for_node(const ufbx_node *node)
|
||||
|
||||
void ArmatureImportContext::create_armature_bones(const ufbx_node *node,
|
||||
Object *arm_obj,
|
||||
Set<const ufbx_node *> &arm_bones,
|
||||
const Set<const ufbx_node *> &bone_nodes,
|
||||
EditBone *parent_bone,
|
||||
const ufbx_matrix &parent_mtx,
|
||||
const ufbx_matrix &world_to_arm,
|
||||
const float parent_bone_size)
|
||||
{
|
||||
BLI_assert(node != nullptr && !node->is_root);
|
||||
bArmature *arm = static_cast<bArmature *>(arm_obj->data);
|
||||
|
||||
arm_bones.add(node);
|
||||
/* For all bone nodes, record the whole armature as the owning object. */
|
||||
this->mapping.el_to_object.add(&node->element, arm_obj);
|
||||
|
||||
ufbx_matrix bone_mtx = ufbx_identity_matrix;
|
||||
EditBone *bone = nullptr;
|
||||
if (node->bone == nullptr || node->bone->is_root) {
|
||||
/* For root bone, the armature object itself is created, so it will not have the EditBone. */
|
||||
this->mapping.node_to_name.add(node, BKE_id_name(arm_obj->id));
|
||||
/* Create an EditBone. */
|
||||
EditBone *bone = ED_armature_ebone_add(arm, get_fbx_name(node->name, "Bone"));
|
||||
this->mapping.node_to_name.add(node, bone->name);
|
||||
this->mapping.node_is_blender_bone.add(node);
|
||||
this->mapping.bone_to_armature.add(node, arm_obj);
|
||||
bone->flag |= BONE_SELECTED;
|
||||
bone->parent = parent_bone;
|
||||
if (node->inherit_mode == UFBX_INHERIT_MODE_IGNORE_PARENT_SCALE) {
|
||||
bone->inherit_scale_mode = BONE_INHERIT_SCALE_NONE;
|
||||
}
|
||||
else {
|
||||
/* For regular non-root bones, create an EditBone. */
|
||||
bone = ED_armature_ebone_add(arm, get_fbx_name(node->name, "Bone"));
|
||||
this->mapping.node_to_name.add(node, bone->name);
|
||||
bone->flag |= BONE_SELECTED;
|
||||
bone->parent = parent_bone;
|
||||
if (node->inherit_mode == UFBX_INHERIT_MODE_IGNORE_PARENT_SCALE) {
|
||||
bone->inherit_scale_mode = BONE_INHERIT_SCALE_NONE;
|
||||
}
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file,
|
||||
"create BONE %s (parent %s) parent_mtx:\n",
|
||||
node->name.data,
|
||||
parent_bone ? parent_bone->name : "");
|
||||
print_matrix(parent_mtx);
|
||||
fprintf(g_debug_file,
|
||||
"create BONE %s (parent %s) parent_mtx:\n",
|
||||
node->name.data,
|
||||
parent_bone ? parent_bone->name : "");
|
||||
print_matrix(parent_mtx);
|
||||
#endif
|
||||
|
||||
const ufbx_matrix *bind_mtx = this->mapping.bone_to_bind_matrix.lookup_ptr(node);
|
||||
BLI_assert_msg(bind_mtx, "fbx: did not find bind matrix for bone");
|
||||
if (bind_mtx != nullptr) {
|
||||
bone_mtx = *bind_mtx;
|
||||
}
|
||||
}
|
||||
|
||||
ufbx_matrix bone_mtx = this->mapping.get_node_bind_matrix(node);
|
||||
bone_mtx = ufbx_matrix_mul(&world_to_arm, &bone_mtx);
|
||||
bone_mtx.cols[0] = ufbx_vec3_normalize(bone_mtx.cols[0]);
|
||||
bone_mtx.cols[1] = ufbx_vec3_normalize(bone_mtx.cols[1]);
|
||||
bone_mtx.cols[2] = ufbx_vec3_normalize(bone_mtx.cols[2]);
|
||||
|
||||
this->mapping.bone_to_armature.add(node, arm_obj);
|
||||
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file, " bone_mtx:\n");
|
||||
print_matrix(bone_mtx);
|
||||
@@ -162,19 +141,16 @@ void ArmatureImportContext::create_armature_bones(const ufbx_node *node,
|
||||
float bone_size = 0.0f;
|
||||
int child_bone_count = 0;
|
||||
for (const ufbx_node *fchild : node->children) {
|
||||
if (fchild->attrib_type != UFBX_ELEMENT_BONE) {
|
||||
if (!bone_nodes.contains(fchild)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Estimate child position from local transform, but if the child
|
||||
* is skinned/posed then use the posed transform instead. */
|
||||
ufbx_vec3 pos = fchild->local_transform.translation;
|
||||
if (this->mapping.bone_has_pose_or_skin_matrix.contains(fchild)) {
|
||||
bool found;
|
||||
ufbx_matrix local_mtx = this->mapping.calc_local_bind_matrix(fchild, world_to_arm, found);
|
||||
if (found) {
|
||||
pos = local_mtx.cols[3];
|
||||
}
|
||||
if (this->mapping.bone_to_bind_matrix.contains(fchild)) {
|
||||
ufbx_matrix local_mtx = this->mapping.calc_local_bind_matrix(fchild, world_to_arm);
|
||||
pos = local_mtx.cols[3];
|
||||
}
|
||||
bone_size += math::length(float3(pos.x, pos.y, pos.z));
|
||||
child_bone_count++;
|
||||
@@ -187,7 +163,7 @@ void ArmatureImportContext::create_armature_bones(const ufbx_node *node,
|
||||
bone_size = parent_bone_size;
|
||||
/* If we do not have actual pose/skin matrix for this bone, apply local transform onto parent
|
||||
* matrix. */
|
||||
if (!this->mapping.bone_has_pose_or_skin_matrix.contains(node)) {
|
||||
if (!this->mapping.bone_to_bind_matrix.contains(node)) {
|
||||
ufbx_matrix offset_mtx = ufbx_transform_to_matrix(&node->local_transform);
|
||||
bone_mtx = ufbx_matrix_mul(&parent_mtx, &offset_mtx);
|
||||
bone_mtx.cols[0] = ufbx_vec3_normalize(bone_mtx.cols[0]);
|
||||
@@ -204,34 +180,30 @@ void ArmatureImportContext::create_armature_bones(const ufbx_node *node,
|
||||
bone_size = math::max(bone_size, 0.01f);
|
||||
this->mapping.bone_to_length.add(node, bone_size);
|
||||
|
||||
if (bone != nullptr) {
|
||||
bone->tail[0] = 0.0f;
|
||||
bone->tail[1] = bone_size;
|
||||
bone->tail[2] = 0.0f;
|
||||
/* Set bone matrix. */
|
||||
float bone_matrix[4][4];
|
||||
matrix_to_m44(bone_mtx, bone_matrix);
|
||||
ED_armature_ebone_from_mat4(bone, bone_matrix);
|
||||
}
|
||||
bone->tail[0] = 0.0f;
|
||||
bone->tail[1] = bone_size;
|
||||
bone->tail[2] = 0.0f;
|
||||
/* Set bone matrix. */
|
||||
float bone_matrix[4][4];
|
||||
matrix_to_m44(bone_mtx, bone_matrix);
|
||||
ED_armature_ebone_from_mat4(bone, bone_matrix);
|
||||
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
if (bone != nullptr) {
|
||||
fprintf(g_debug_file,
|
||||
" length %.3f head (%.3f %.3f %.3f) tail (%.3f %.3f %.3f)\n",
|
||||
adjf(bone_size),
|
||||
adjf(bone->head[0]),
|
||||
adjf(bone->head[1]),
|
||||
adjf(bone->head[2]),
|
||||
adjf(bone->tail[0]),
|
||||
adjf(bone->tail[1]),
|
||||
adjf(bone->tail[2]));
|
||||
}
|
||||
fprintf(g_debug_file,
|
||||
" length %.3f head (%.3f %.3f %.3f) tail (%.3f %.3f %.3f)\n",
|
||||
adjf(bone_size),
|
||||
adjf(bone->head[0]),
|
||||
adjf(bone->head[1]),
|
||||
adjf(bone->head[2]),
|
||||
adjf(bone->tail[0]),
|
||||
adjf(bone->tail[1]),
|
||||
adjf(bone->tail[2]));
|
||||
#endif
|
||||
|
||||
/* Mark bone as connected to parent if head approximately in the same place as parent tail, in
|
||||
* both rest pose and current pose. */
|
||||
if (parent_bone != nullptr) {
|
||||
float3 self_head_rest = bone ? float3(bone->head) : float3(0);
|
||||
float3 self_head_rest(bone->head);
|
||||
float3 par_tail_rest(parent_bone->tail);
|
||||
const float connect_dist = 1.0e-4f;
|
||||
const float connect_dist_sq = connect_dist * connect_dist;
|
||||
@@ -248,7 +220,7 @@ void ArmatureImportContext::create_armature_bones(const ufbx_node *node,
|
||||
float3 par_tail_cur(par_tail_cur_u.x, par_tail_cur_u.y, par_tail_cur_u.z);
|
||||
float dist_sq_cur = math::distance_squared(self_head_cur, par_tail_cur);
|
||||
|
||||
if (dist_sq_cur < connect_dist_sq && bone != nullptr) {
|
||||
if (dist_sq_cur < connect_dist_sq) {
|
||||
/* Connected in both cases. */
|
||||
bone->flag |= BONE_CONNECTED;
|
||||
}
|
||||
@@ -257,7 +229,7 @@ void ArmatureImportContext::create_armature_bones(const ufbx_node *node,
|
||||
|
||||
/* Recurse into child bones. */
|
||||
for (const ufbx_node *fchild : node->children) {
|
||||
if (fchild->attrib_type != UFBX_ELEMENT_BONE) {
|
||||
if (!bone_nodes.contains(fchild)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -267,66 +239,106 @@ void ArmatureImportContext::create_armature_bones(const ufbx_node *node,
|
||||
!mapping.bone_is_skinned.contains(fchild))
|
||||
{
|
||||
skip_child = true;
|
||||
/* We are skipping this bone, but still record as it would be belonging to
|
||||
* our armature -- so that later code does not try to create an empty for it. */
|
||||
this->mapping.el_to_object.add(&fchild->element, arm_obj);
|
||||
/* We are skipping this bone, but still record it --
|
||||
* so that later code does not try to create an empty for it. */
|
||||
this->mapping.node_is_blender_bone.add(fchild);
|
||||
}
|
||||
}
|
||||
|
||||
if (!skip_child) {
|
||||
create_armature_bones(fchild, arm_obj, arm_bones, bone, bone_mtx, world_to_arm, bone_size);
|
||||
create_armature_bones(fchild, arm_obj, bone_nodes, bone, bone_mtx, world_to_arm, bone_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ArmatureImportContext::find_armatures(const ufbx_node *node)
|
||||
/* Need to create armature if we are root bone, or any child is a non-root bone. */
|
||||
static bool need_create_armature_for_node(const ufbx_node *node)
|
||||
{
|
||||
/* Need to create armature if we are root bone, or any child is a non-root bone. */
|
||||
bool needs_arm = false;
|
||||
if (node->bone && node->bone->is_root) {
|
||||
return true;
|
||||
}
|
||||
for (const ufbx_node *fchild : node->children) {
|
||||
if (fchild->bone && !fchild->bone->is_root) {
|
||||
needs_arm = true;
|
||||
break;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (node->bone && node->bone->is_root) {
|
||||
needs_arm = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Create armature if needed. */
|
||||
static void find_bones(const ufbx_node *node, Set<const ufbx_node *> &r_bones)
|
||||
{
|
||||
if (node->bone != nullptr) {
|
||||
r_bones.add(node);
|
||||
}
|
||||
for (const ufbx_node *child : node->children) {
|
||||
find_bones(child, r_bones);
|
||||
}
|
||||
}
|
||||
|
||||
static void find_fake_bones(const ufbx_node *root_node,
|
||||
const Set<const ufbx_node *> &bones,
|
||||
Set<const ufbx_node *> &r_fake_bones)
|
||||
{
|
||||
for (const ufbx_node *bone_node : bones) {
|
||||
const ufbx_node *node = bone_node->parent;
|
||||
while (node != nullptr && node != root_node) {
|
||||
if (node->bone == nullptr) {
|
||||
r_fake_bones.add(node);
|
||||
}
|
||||
node = node->parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Set<const ufbx_node *> find_all_bones(const ufbx_node *root_node)
|
||||
{
|
||||
/* Find regular FBX bones nodes anywhere under our root armature node. */
|
||||
Set<const ufbx_node *> bones;
|
||||
find_bones(root_node, bones);
|
||||
|
||||
/* There might be non-bone nodes in between, e.g. FBX structure being like:
|
||||
* BoneA -> MeshB -> BoneC -> MeshD. Blender Armature can only contain
|
||||
* bones, so in this case "MeshB" has to have a bone created for it as well.
|
||||
* "Fake bones" are any non-bone FBX nodes in between root armature node
|
||||
* and the actual bone node. */
|
||||
Set<const ufbx_node *> fake_bones;
|
||||
find_fake_bones(root_node, bones, fake_bones);
|
||||
for (const ufbx_node *b : fake_bones) {
|
||||
bones.add(b);
|
||||
}
|
||||
return bones;
|
||||
}
|
||||
|
||||
void ArmatureImportContext::find_armatures(const ufbx_node *node)
|
||||
{
|
||||
const bool needs_arm = need_create_armature_for_node(node);
|
||||
if (needs_arm) {
|
||||
Object *arm_obj = nullptr;
|
||||
if ((node->bone && node->bone->is_root) || (node->attrib_type == UFBX_ELEMENT_EMPTY)) {
|
||||
arm_obj = this->create_armature_for_node(node);
|
||||
}
|
||||
else {
|
||||
arm_obj = this->create_armature_for_node(nullptr);
|
||||
}
|
||||
|
||||
/* Create bones in edit mode. */
|
||||
/* Create armature. */
|
||||
Object *arm_obj = this->create_armature_for_node(node);
|
||||
ufbx_matrix world_to_arm = this->mapping.armature_world_to_arm_pose_matrix.lookup_default(
|
||||
arm_obj, ufbx_identity_matrix);
|
||||
|
||||
Set<const ufbx_node *> arm_bones;
|
||||
Set<const ufbx_node *> bone_nodes = find_all_bones(node);
|
||||
|
||||
/* Create bones in edit mode. */
|
||||
bArmature *arm = static_cast<bArmature *>(arm_obj->data);
|
||||
ED_armature_to_edit(arm);
|
||||
if (node->bone && node->bone->is_root) {
|
||||
create_armature_bones(
|
||||
node, arm_obj, arm_bones, nullptr, ufbx_identity_matrix, world_to_arm, 1.0f);
|
||||
}
|
||||
else {
|
||||
for (const ufbx_node *fchild : node->children) {
|
||||
if (fchild->bone) {
|
||||
create_armature_bones(
|
||||
fchild, arm_obj, arm_bones, nullptr, ufbx_identity_matrix, world_to_arm, 1.0f);
|
||||
}
|
||||
this->mapping.node_to_name.add(node, BKE_id_name(arm_obj->id));
|
||||
for (const ufbx_node *fchild : node->children) {
|
||||
if (bone_nodes.contains(fchild)) {
|
||||
create_armature_bones(
|
||||
fchild, arm_obj, bone_nodes, nullptr, ufbx_identity_matrix, world_to_arm, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
ED_armature_from_edit(&this->bmain, arm);
|
||||
ED_armature_edit_free(arm);
|
||||
|
||||
/* Setup pose on the object, and custom properties on the pose bones. */
|
||||
for (const ufbx_node *fbone : arm_bones) {
|
||||
/* Setup pose on the object, and custom properties on the bone pose channels. */
|
||||
for (const ufbx_node *fbone : bone_nodes) {
|
||||
if (!this->mapping.node_is_blender_bone.contains(fbone)) {
|
||||
continue; /* Blender bone was not created for it (e.g. root bone in some cases). */
|
||||
}
|
||||
bPoseChannel *pchan = BKE_pose_channel_find_name(
|
||||
arm_obj->pose, this->mapping.node_to_name.lookup_default(fbone, "").c_str());
|
||||
if (pchan == nullptr) {
|
||||
@@ -336,35 +348,31 @@ void ArmatureImportContext::find_armatures(const ufbx_node *node)
|
||||
|
||||
/* For bones that have rest/bind information, put their current transform into
|
||||
* the current pose. */
|
||||
if (this->mapping.bone_has_pose_or_skin_matrix.contains(fbone)) {
|
||||
bool found;
|
||||
ufbx_matrix bind_local_mtx = this->mapping.calc_local_bind_matrix(
|
||||
fbone, world_to_arm, found);
|
||||
if (found) {
|
||||
ufbx_matrix bind_local_mtx_inv = ufbx_matrix_invert(&bind_local_mtx);
|
||||
ufbx_transform xform = fbone->local_transform;
|
||||
if (fbone->node_depth <= 1) {
|
||||
ufbx_matrix matrix = ufbx_matrix_mul(&world_to_arm, &fbone->node_to_world);
|
||||
xform = ufbx_matrix_to_transform(&matrix);
|
||||
}
|
||||
ufbx_matrix pose_mtx = calc_bone_pose_matrix(xform, *fbone, bind_local_mtx_inv);
|
||||
if (this->mapping.bone_to_bind_matrix.contains(fbone)) {
|
||||
ufbx_matrix bind_local_mtx = this->mapping.calc_local_bind_matrix(fbone, world_to_arm);
|
||||
ufbx_matrix bind_local_mtx_inv = ufbx_matrix_invert(&bind_local_mtx);
|
||||
ufbx_transform xform = fbone->local_transform;
|
||||
if (fbone->node_depth <= 1) {
|
||||
ufbx_matrix matrix = ufbx_matrix_mul(&world_to_arm, &fbone->node_to_world);
|
||||
xform = ufbx_matrix_to_transform(&matrix);
|
||||
}
|
||||
ufbx_matrix pose_mtx = calc_bone_pose_matrix(xform, *fbone, bind_local_mtx_inv);
|
||||
|
||||
float pchan_matrix[4][4];
|
||||
matrix_to_m44(pose_mtx, pchan_matrix);
|
||||
BKE_pchan_apply_mat4(pchan, pchan_matrix, false);
|
||||
float pchan_matrix[4][4];
|
||||
matrix_to_m44(pose_mtx, pchan_matrix);
|
||||
BKE_pchan_apply_mat4(pchan, pchan_matrix, false);
|
||||
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file, "set POSE matrix of %s matrix_basis:\n", fbone->name.data);
|
||||
print_matrix(pose_mtx);
|
||||
fprintf(g_debug_file, "set POSE matrix of %s matrix_basis:\n", fbone->name.data);
|
||||
print_matrix(pose_mtx);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Recurse into non-bone or root-bone children. */
|
||||
/* Recurse into children that have not been turned into bones yet. */
|
||||
for (const ufbx_node *fchild : node->children) {
|
||||
if (fchild->bone == nullptr || fchild->bone->is_root) {
|
||||
if (!this->mapping.node_is_blender_bone.contains(fchild)) {
|
||||
this->find_armatures(fchild);
|
||||
}
|
||||
}
|
||||
@@ -384,7 +392,6 @@ void ArmatureImportContext::calc_bone_bind_matrices()
|
||||
for (const ufbx_bone_pose &bone_pose : fpose->bone_poses) {
|
||||
const ufbx_matrix &bind_matrix = bone_pose.bone_to_world;
|
||||
this->mapping.bone_to_bind_matrix.add_overwrite(bone_pose.bone_node, bind_matrix);
|
||||
this->mapping.bone_has_pose_or_skin_matrix.add(bone_pose.bone_node);
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file, "bone POSE matrix %s\n", bone_pose.bone_node->name.data);
|
||||
print_matrix(bind_matrix);
|
||||
@@ -396,23 +403,10 @@ void ArmatureImportContext::calc_bone_bind_matrices()
|
||||
for (const ufbx_skin_cluster *fbone : fskin->clusters) {
|
||||
const ufbx_matrix &bind_matrix = fbone->bind_to_world;
|
||||
this->mapping.bone_to_bind_matrix.add_overwrite(fbone->bone_node, bind_matrix);
|
||||
this->mapping.bone_has_pose_or_skin_matrix.add(fbone->bone_node);
|
||||
this->mapping.bone_is_skinned.add(fbone->bone_node);
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file, "bone SKIN matrix %s\n", fbone->bone_node->name.data);
|
||||
print_matrix(bind_matrix);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
for (const ufbx_bone *fbone : this->fbx.bones) {
|
||||
if (fbone->instances.count != 0) {
|
||||
const ufbx_node *bone_node = fbone->instances[0];
|
||||
const ufbx_matrix &bind_matrix = bone_node->node_to_world;
|
||||
this->mapping.bone_to_bind_matrix.add(bone_node, bind_matrix);
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file, "bone NODE matrix %s\n", bone_node->name.data);
|
||||
print_matrix(bind_matrix);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -425,11 +419,6 @@ void import_armatures(Main &bmain,
|
||||
{
|
||||
ArmatureImportContext context(bmain, fbx, params, mapping);
|
||||
context.calc_bone_bind_matrices();
|
||||
|
||||
/* Create blender armatures at:
|
||||
* - "Root" bones,
|
||||
* - Bones with an empty parent,
|
||||
* - For bones without a parent or a non-empty parent, create an armature above them. */
|
||||
context.find_armatures(fbx.root_node);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "BLI_ordered_edge.hh"
|
||||
#include "BLI_string.h"
|
||||
#include "BLI_task.hh"
|
||||
#include "BLI_vector_set.hh"
|
||||
|
||||
#include "BLT_translation.hh"
|
||||
|
||||
@@ -36,15 +37,10 @@ namespace blender::io::fbx {
|
||||
|
||||
static constexpr const char *temp_custom_normals_name = "fbx_temp_custom_normals";
|
||||
|
||||
static const ufbx_skin_deformer *get_skin_from_mesh(const ufbx_mesh *mesh)
|
||||
static bool is_skin_deformer_usable(const ufbx_mesh *mesh, const ufbx_skin_deformer *skin)
|
||||
{
|
||||
if (mesh->skin_deformers.count > 0) {
|
||||
const ufbx_skin_deformer *skin = mesh->skin_deformers[0];
|
||||
if (skin != nullptr && mesh->num_vertices > 0 && skin->vertices.count == mesh->num_vertices) {
|
||||
return skin;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
return mesh != nullptr && skin != nullptr && skin->clusters.count > 0 &&
|
||||
mesh->num_vertices > 0 && skin->vertices.count == mesh->num_vertices;
|
||||
}
|
||||
|
||||
static void import_vertex_positions(const ufbx_mesh *fmesh, Mesh *mesh)
|
||||
@@ -275,34 +271,65 @@ static bool import_normals_into_temp_attribute(const ufbx_mesh *fmesh,
|
||||
return true;
|
||||
}
|
||||
|
||||
static void import_skin_vertex_groups(const ufbx_mesh *fmesh,
|
||||
const ufbx_skin_deformer *skin,
|
||||
static VectorSet<std::string> get_skin_bone_name_set(const FbxElementMapping &mapping,
|
||||
const ufbx_mesh *fmesh)
|
||||
{
|
||||
VectorSet<std::string> name_set;
|
||||
for (const ufbx_skin_deformer *skin : fmesh->skin_deformers) {
|
||||
if (!is_skin_deformer_usable(fmesh, skin)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const ufbx_skin_cluster *cluster : skin->clusters) {
|
||||
if (cluster->num_weights == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string bone_name = mapping.node_to_name.lookup_default(cluster->bone_node, "");
|
||||
name_set.add(bone_name);
|
||||
}
|
||||
}
|
||||
return name_set;
|
||||
}
|
||||
|
||||
static void import_skin_vertex_groups(const FbxElementMapping &mapping,
|
||||
const ufbx_mesh *fmesh,
|
||||
Mesh *mesh)
|
||||
{
|
||||
/* We need to build mapping from cluster indices to non-empty
|
||||
* cluster indices. */
|
||||
Vector<int> skin_cluster_to_nonempty_cluster_index(skin->clusters.count, -1);
|
||||
int cluster_counter = 0;
|
||||
for (int i = 0; i < skin->clusters.count; i++) {
|
||||
if (skin->clusters[i]->num_weights != 0) {
|
||||
skin_cluster_to_nonempty_cluster_index[i] = cluster_counter;
|
||||
cluster_counter++;
|
||||
}
|
||||
if (fmesh->skin_deformers.count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* A single mesh can be skinned by several armatures, so we need to build bone (vertex group)
|
||||
* name set, taking all skin deformers into account. */
|
||||
VectorSet<std::string> bone_set = get_skin_bone_name_set(mapping, fmesh);
|
||||
if (bone_set.is_empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
MutableSpan<MDeformVert> dverts = mesh->deform_verts_for_write();
|
||||
for (int i = 0; i < fmesh->num_vertices; i++) {
|
||||
const ufbx_skin_vertex &fvertex = skin->vertices[i];
|
||||
int num_weights = fvertex.num_weights;
|
||||
if (num_weights > 0) {
|
||||
dverts[i].dw = MEM_malloc_arrayN<MDeformWeight>(num_weights, __func__);
|
||||
dverts[i].totweight = num_weights;
|
||||
for (int j = 0; j < num_weights; j++) {
|
||||
const ufbx_skin_weight &fweight = skin->weights[fvertex.weight_begin + j];
|
||||
const int bone_index = skin_cluster_to_nonempty_cluster_index[fweight.cluster_index];
|
||||
const bool valid = bone_index >= 0;
|
||||
dverts[i].dw[j].def_nr = valid ? bone_index : 0;
|
||||
dverts[i].dw[j].weight = valid ? fweight.weight : 0.0f;
|
||||
|
||||
for (const ufbx_skin_deformer *skin : fmesh->skin_deformers) {
|
||||
if (!is_skin_deformer_usable(fmesh, skin)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const ufbx_skin_cluster *cluster : skin->clusters) {
|
||||
if (cluster->num_weights == 0) {
|
||||
continue;
|
||||
}
|
||||
std::string bone_name = mapping.node_to_name.lookup_default(cluster->bone_node, "");
|
||||
const int group_index = bone_set.index_of_try(bone_name);
|
||||
if (group_index < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int i = 0; i < cluster->num_weights; i++) {
|
||||
const int vertex = cluster->vertices[i];
|
||||
if (vertex < dverts.size()) {
|
||||
MDeformWeight *dw = BKE_defvert_ensure_index(&dverts[vertex], group_index);
|
||||
dw->weight = cluster->weights[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -445,10 +472,7 @@ void import_meshes(Main &bmain,
|
||||
* data as custom normals after the validation. */
|
||||
has_custom_normals = import_normals_into_temp_attribute(fmesh, mesh, attributes);
|
||||
}
|
||||
const ufbx_skin_deformer *skin = get_skin_from_mesh(fmesh);
|
||||
if (skin != nullptr) {
|
||||
import_skin_vertex_groups(fmesh, skin, mesh);
|
||||
}
|
||||
import_skin_vertex_groups(mapping, fmesh, mesh);
|
||||
|
||||
/* Validate if needed. */
|
||||
if (params.validate_meshes) {
|
||||
@@ -481,7 +505,6 @@ void import_meshes(Main &bmain,
|
||||
}
|
||||
const ufbx_mesh *fmesh = fbx.meshes[index];
|
||||
BLI_assert(fmesh != nullptr);
|
||||
const ufbx_skin_deformer *skin = get_skin_from_mesh(fmesh);
|
||||
|
||||
Mesh *mesh_main = static_cast<Mesh *>(
|
||||
BKE_object_obdata_add_from_type(&bmain, OB_MESH, get_fbx_name(fmesh->name, "Mesh")));
|
||||
@@ -509,50 +532,61 @@ void import_meshes(Main &bmain,
|
||||
bool matrix_already_set = false;
|
||||
|
||||
/* Skinned mesh. */
|
||||
if (skin != nullptr && skin->clusters.count > 0) {
|
||||
Object *parent_to_arm = nullptr;
|
||||
if (fmesh->skin_deformers.count > 0) {
|
||||
/* Add vertex groups to the object. */
|
||||
for (const ufbx_skin_cluster *fcluster : skin->clusters) {
|
||||
if (fcluster->num_weights == 0) { /* Do not add groups for empty clusters. */
|
||||
continue;
|
||||
}
|
||||
if (parent_to_arm == nullptr) {
|
||||
parent_to_arm = mapping.bone_to_armature.lookup_default(fcluster->bone_node, nullptr);
|
||||
}
|
||||
std::string bone_name = mapping.node_to_name.lookup_default(fcluster->bone_node, "");
|
||||
BKE_object_defgroup_add_name(obj, bone_name.c_str());
|
||||
VectorSet<std::string> bone_set = get_skin_bone_name_set(mapping, fmesh);
|
||||
for (const std::string &name : bone_set) {
|
||||
BKE_object_defgroup_add_name(obj, name.c_str());
|
||||
}
|
||||
|
||||
/* Add armature modifier. */
|
||||
if (parent_to_arm) {
|
||||
ModifierData *md = BKE_modifier_new(eModifierType_Armature);
|
||||
STRNCPY(md->name, BKE_id_name(parent_to_arm->id));
|
||||
BLI_addtail(&obj->modifiers, md);
|
||||
BKE_modifiers_persistent_uid_init(*obj, *md);
|
||||
ArmatureModifierData *ad = reinterpret_cast<ArmatureModifierData *>(md);
|
||||
ad->object = parent_to_arm;
|
||||
obj->parent = parent_to_arm;
|
||||
/* Add armature modifiers for each skin deformer. */
|
||||
for (const ufbx_skin_deformer *skin : fmesh->skin_deformers) {
|
||||
if (!is_skin_deformer_usable(fmesh, skin)) {
|
||||
continue;
|
||||
}
|
||||
Object *arm_obj = nullptr;
|
||||
for (const ufbx_skin_cluster *cluster : skin->clusters) {
|
||||
if (cluster->num_weights == 0) {
|
||||
continue;
|
||||
}
|
||||
arm_obj = mapping.bone_to_armature.lookup_default(cluster->bone_node, nullptr);
|
||||
if (arm_obj != nullptr) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* Add armature modifier. */
|
||||
if (arm_obj != nullptr) {
|
||||
ModifierData *md = BKE_modifier_new(eModifierType_Armature);
|
||||
STRNCPY(md->name, BKE_id_name(arm_obj->id));
|
||||
BLI_addtail(&obj->modifiers, md);
|
||||
BKE_modifiers_persistent_uid_init(*obj, *md);
|
||||
ArmatureModifierData *ad = reinterpret_cast<ArmatureModifierData *>(md);
|
||||
ad->object = arm_obj;
|
||||
|
||||
/* We are setting mesh parent to the armature, so set the matrix that is
|
||||
* armature-local. Note that the matrix needs to be relative to the FBX
|
||||
* node matrix (not the root bone pose matrix). */
|
||||
ufbx_matrix world_to_arm = mapping.armature_world_to_arm_node_matrix.lookup_default(
|
||||
parent_to_arm, ufbx_identity_matrix);
|
||||
ufbx_matrix world_to_arm_pose = mapping.armature_world_to_arm_pose_matrix.lookup_default(
|
||||
parent_to_arm, ufbx_identity_matrix);
|
||||
if (!matrix_already_set) {
|
||||
matrix_already_set = true;
|
||||
obj->parent = arm_obj;
|
||||
|
||||
ufbx_matrix mtx = ufbx_matrix_mul(&world_to_arm, &node->geometry_to_world);
|
||||
ufbx_matrix_to_obj(mtx, obj);
|
||||
matrix_already_set = true;
|
||||
/* We are setting mesh parent to the armature, so set the matrix that is
|
||||
* armature-local. Note that the matrix needs to be relative to the FBX
|
||||
* node matrix (not the root bone pose matrix). */
|
||||
ufbx_matrix world_to_arm = mapping.armature_world_to_arm_node_matrix.lookup_default(
|
||||
arm_obj, ufbx_identity_matrix);
|
||||
ufbx_matrix world_to_arm_pose = mapping.armature_world_to_arm_pose_matrix
|
||||
.lookup_default(arm_obj, ufbx_identity_matrix);
|
||||
|
||||
/* Setup parent inverse matrix of the mesh, to account for the mesh possibly being in
|
||||
* different bind pose than what the node is at. */
|
||||
ufbx_matrix mtx_inv = ufbx_matrix_invert(&mtx);
|
||||
ufbx_matrix mtx_world = mapping.bone_to_bind_matrix.lookup_default(
|
||||
node, node->geometry_to_world);
|
||||
ufbx_matrix mtx_parent_inverse = ufbx_matrix_mul(&mtx_world, &mtx_inv);
|
||||
mtx_parent_inverse = ufbx_matrix_mul(&world_to_arm_pose, &mtx_parent_inverse);
|
||||
matrix_to_m44(mtx_parent_inverse, obj->parentinv);
|
||||
ufbx_matrix mtx = ufbx_matrix_mul(&world_to_arm, &node->geometry_to_world);
|
||||
ufbx_matrix_to_obj(mtx, obj);
|
||||
|
||||
/* Setup parent inverse matrix of the mesh, to account for the mesh possibly being in
|
||||
* different bind pose than what the node is at. */
|
||||
ufbx_matrix mtx_inv = ufbx_matrix_invert(&mtx);
|
||||
ufbx_matrix mtx_world = mapping.get_node_bind_matrix(node);
|
||||
ufbx_matrix mtx_parent_inverse = ufbx_matrix_mul(&mtx_world, &mtx_inv);
|
||||
mtx_parent_inverse = ufbx_matrix_mul(&world_to_arm_pose, &mtx_parent_inverse);
|
||||
matrix_to_m44(mtx_parent_inverse, obj->parentinv);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,6 +644,7 @@ void import_meshes(Main &bmain,
|
||||
node_matrix_to_obj(node, obj, mapping);
|
||||
}
|
||||
mapping.el_to_object.add(&node->element, obj);
|
||||
mapping.imported_objects.add(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,12 +86,20 @@ void node_matrix_to_obj(const ufbx_node *node, Object *obj, const FbxElementMapp
|
||||
/* Handle case of an object parented to a bone: need to set
|
||||
* bone as parent, and make transform be at the end of the bone. */
|
||||
const ufbx_node *parbone = node->parent;
|
||||
if (obj->parent == nullptr && parbone && parbone->bone) {
|
||||
if (obj->parent == nullptr && parbone && mapping.node_is_blender_bone.contains(parbone)) {
|
||||
Object *arm = mapping.bone_to_armature.lookup_default(parbone, nullptr);
|
||||
if (arm != nullptr) {
|
||||
ufbx_matrix offset_mtx = ufbx_identity_matrix;
|
||||
offset_mtx.cols[3].y = -mapping.bone_to_length.lookup_default(parbone, 0.0);
|
||||
mtx = ufbx_matrix_mul(&offset_mtx, &mtx);
|
||||
if (mapping.node_is_blender_bone.contains(node)) {
|
||||
/* The node itself is a "fake bone", in which case parent it to the matching
|
||||
* fake bone, and matrix is just what puts transform at the bone tail. */
|
||||
parbone = node;
|
||||
mtx = offset_mtx;
|
||||
}
|
||||
else {
|
||||
mtx = ufbx_matrix_mul(&offset_mtx, &mtx);
|
||||
}
|
||||
|
||||
obj->parent = arm;
|
||||
obj->partype = PARBONE;
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace blender::io::fbx {
|
||||
const char *get_fbx_name(const ufbx_string &name, const char *def = "Untitled");
|
||||
|
||||
struct FbxElementMapping {
|
||||
Set<Object *> imported_objects;
|
||||
Map<const ufbx_element *, Object *> el_to_object;
|
||||
Map<const ufbx_element *, Key *> el_to_shape_key;
|
||||
Map<const ufbx_material *, Material *> mat_to_material;
|
||||
@@ -35,40 +36,34 @@ struct FbxElementMapping {
|
||||
Map<const Object *, ufbx_matrix> armature_world_to_arm_pose_matrix;
|
||||
Map<const Object *, ufbx_matrix> armature_world_to_arm_node_matrix;
|
||||
|
||||
/* Which FBX bone nodes got turned into actual armature bones (not all of them
|
||||
* always are; in some cases root bone is the armature object itself). */
|
||||
Set<const ufbx_node *> node_is_blender_bone;
|
||||
|
||||
/* Mapping of ufbx node to object name used within blender. If names are too long
|
||||
* or duplicate, they might not match what was in FBX file. */
|
||||
Map<const ufbx_node *, std::string> node_to_name;
|
||||
/* Bone node to "bind matrix", i.e. matrix that transforms from bone (in skin bind pose) local
|
||||
* space to world space. */
|
||||
* space to world space. This records bone pose or skin cluster bind matrix (skin cluster taking
|
||||
* precedence if it exists). */
|
||||
Map<const ufbx_node *, ufbx_matrix> bone_to_bind_matrix;
|
||||
Map<const ufbx_node *, ufbx_real> bone_to_length;
|
||||
/* Which bones actually have pose or skin cluster bind matrices in the FBX file (the others
|
||||
* would just use their world transform). */
|
||||
Set<const ufbx_node *> bone_has_pose_or_skin_matrix;
|
||||
Set<const ufbx_node *> bone_is_skinned;
|
||||
ufbx_matrix global_conv_matrix;
|
||||
|
||||
//@TODO: these could be precalculated once
|
||||
ufbx_matrix calc_local_bind_matrix(const ufbx_node *bone_node,
|
||||
const ufbx_matrix &world_to_arm,
|
||||
bool &r_found) const
|
||||
ufbx_matrix get_node_bind_matrix(const ufbx_node *node) const
|
||||
{
|
||||
r_found = false;
|
||||
const ufbx_matrix *bind_mtx = this->bone_to_bind_matrix.lookup_ptr(bone_node);
|
||||
if (bind_mtx == nullptr) {
|
||||
return ufbx_identity_matrix;
|
||||
}
|
||||
r_found = true;
|
||||
ufbx_matrix res = *bind_mtx;
|
||||
|
||||
const ufbx_matrix *parent_mtx = nullptr;
|
||||
if (bone_node->parent != nullptr) {
|
||||
parent_mtx = this->bone_to_bind_matrix.lookup_ptr(bone_node->parent);
|
||||
}
|
||||
return this->bone_to_bind_matrix.lookup_default(node, node->geometry_to_world);
|
||||
}
|
||||
|
||||
ufbx_matrix calc_local_bind_matrix(const ufbx_node *bone_node,
|
||||
const ufbx_matrix &world_to_arm) const
|
||||
{
|
||||
ufbx_matrix res = this->get_node_bind_matrix(bone_node);
|
||||
ufbx_matrix parent_inv_mtx;
|
||||
if (parent_mtx) {
|
||||
parent_inv_mtx = ufbx_matrix_invert(parent_mtx);
|
||||
if (bone_node->parent != nullptr && !bone_node->parent->is_root) {
|
||||
ufbx_matrix parent_mtx = this->get_node_bind_matrix(bone_node->parent);
|
||||
parent_inv_mtx = ufbx_matrix_invert(&parent_mtx);
|
||||
}
|
||||
else {
|
||||
parent_inv_mtx = world_to_arm;
|
||||
|
||||
BIN
tests/files/io_tests/fbx/issue45171_double_armature.fbx
(Stored with Git LFS)
Normal file
BIN
tests/files/io_tests/fbx/issue45171_double_armature.fbx
(Stored with Git LFS)
Normal file
Binary file not shown.
@@ -826,7 +826,7 @@
|
||||
0.635 0.405 -0.658 5.409
|
||||
0.535 -0.845 -0.003 7.713
|
||||
-0.557 -0.350 -0.753 -5.440
|
||||
- bone '(64)attach#1C0#0' h:(0.000, 0.000, 9.000) t:(0.000, 3.025, 9.000) radius h:0.100 t:0.050
|
||||
- bone '(64)attach#1C0#0' h:(0.000, 0.000, 9.000) t:(0.000, 1.000, 9.000) radius h:0.100 t:0.050
|
||||
1.000 0.000 0.000 0.000
|
||||
0.000 1.000 0.000 0.000
|
||||
0.000 0.000 1.000 9.000
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
==== Meshes: 1
|
||||
- Mesh 'Cubes' vtx:16 face:12 loop:48 edge:24
|
||||
- 1 3 2 0 3 ... 12 13 15 11 9
|
||||
- 1/3 2/3 0/2 0/1 3/7 ... 13/15 12/13 12/14 9/13 8/12
|
||||
- attr 'position' FLOAT_VECTOR POINT
|
||||
- (-0.484, -0.484, -0.484)
|
||||
- (-0.484, -0.484, 0.484)
|
||||
- (-0.484, 0.484, -0.484)
|
||||
...
|
||||
- (-0.446, -1.275, 1.551)
|
||||
- (-0.446, -0.307, 0.583)
|
||||
- (-0.446, -0.307, 1.551)
|
||||
- attr 'sharp_edge' BOOLEAN EDGE
|
||||
- 1 1 1 1 1 ... 1 1 1 1 1
|
||||
- attr 'custom_normal' INT16_2D CORNER
|
||||
- (0, 0)
|
||||
- (0, 0)
|
||||
- (0, 0)
|
||||
...
|
||||
- (0, 0)
|
||||
- (0, 0)
|
||||
- (0, 0)
|
||||
- vertex groups:
|
||||
- 1=1.000
|
||||
- 1=1.000
|
||||
- 1=1.000
|
||||
- 1=1.000
|
||||
- 1=1.000
|
||||
|
||||
==== Objects: 5
|
||||
- Obj 'Armature' ARMATURE data:'Armature'
|
||||
- pos 0.000, 0.000, 0.000
|
||||
- rot 0.000, 0.000, 0.000 (XYZ)
|
||||
- scl 1.000, 1.000, 1.000
|
||||
- Obj 'Armature.001' ARMATURE data:'Armature.001'
|
||||
- pos -0.737, -0.813, 0.695
|
||||
- rot 0.000, 0.000, 0.000 (XYZ)
|
||||
- scl 1.000, 1.000, 1.000
|
||||
- Obj 'Camera' CAMERA data:'Camera'
|
||||
- pos 7.481, -6.508, 5.344
|
||||
- rot 2.032, -0.011, -2.327 (XYZ)
|
||||
- scl 1.000, 1.000, 1.000
|
||||
- Obj 'Cubes' MESH data:'Cubes' par:'Armature.001'
|
||||
- pos 0.737, 0.813, -0.695
|
||||
- rot 0.000, 0.000, 0.000 (XYZ)
|
||||
- scl 1.000, 1.000, 1.000
|
||||
- 2 vertex groups
|
||||
- 'Bone_Top' 'Bone_Bottom'
|
||||
- 2 modifiers
|
||||
- ARMATURE 'Armature.001'
|
||||
- ARMATURE 'Armature'
|
||||
- Obj 'Lamp' LIGHT data:'Lamp'
|
||||
- pos 4.076, 1.005, 5.904
|
||||
- rot -2.491, 0.055, 1.866 (XYZ)
|
||||
- scl 1.000, 1.000, 1.000
|
||||
|
||||
==== Cameras: 1
|
||||
- Cam 'Camera' PERSP lens:35.0 MILLIMETERS near:0.100 far:100.0 orthosize:7.3
|
||||
- fov 0.858 (h 0.858 v 0.503)
|
||||
- sensor 32.0x18.0 shift 0.000,0.000
|
||||
|
||||
==== Lights: 1
|
||||
- Light 'Lamp' POINT col:(1.000, 1.000, 1.000) energy:1.000
|
||||
|
||||
==== Armatures: 2
|
||||
- Armature 'Armature' 2 bones
|
||||
- bone 'Bone_Bottom' h:(0.000, 0.000, 0.000) t:(0.000, 0.000, 1.000) radius h:0.100 t:0.050
|
||||
1.000 0.000 0.000 0.000
|
||||
0.000 0.000 -1.000 0.000
|
||||
0.000 1.000 0.000 0.000
|
||||
- bone 'Bone_Bottom_end' parent:'Bone_Bottom' h:(0.000, 0.000, 0.000) t:(0.000, 1.000, 0.000) connect radius h:0.100 t:0.050
|
||||
1.000 0.000 0.000 0.000
|
||||
0.000 0.000 -1.000 0.000
|
||||
0.000 1.000 0.000 1.000
|
||||
|
||||
- Armature 'Armature.001' 2 bones
|
||||
- bone 'Bone_Top' h:(-0.193, 0.022, 0.372) t:(-0.193, 0.022, 1.372) radius h:0.100 t:0.050
|
||||
1.000 0.000 0.000 -0.193
|
||||
0.000 0.000 -1.000 0.022
|
||||
0.000 1.000 0.000 0.372
|
||||
- bone 'Bone_Top_end' parent:'Bone_Top' h:(0.000, 0.000, 0.000) t:(0.000, 1.000, 0.000) connect radius h:0.100 t:0.050
|
||||
1.000 0.000 0.000 -0.193
|
||||
0.000 0.000 -1.000 0.022
|
||||
0.000 1.000 0.000 1.372
|
||||
|
||||
@@ -10,17 +10,17 @@
|
||||
- (0.000, 1.086, 0.110)
|
||||
|
||||
==== Objects: 2
|
||||
- Obj 'Armature' ARMATURE data:'Armature'
|
||||
- Obj 'rb00_battle_00' ARMATURE data:'rb00_battle_00'
|
||||
- pos 0.000, 0.000, 0.000
|
||||
- rot 1.571, 0.000, 0.000 (XYZ)
|
||||
- scl 0.010, 0.010, 0.010
|
||||
- Obj 'rb00_battle_00' MESH data:'Mesh'
|
||||
- Obj 'rb00_battle_00.001' MESH data:'Mesh'
|
||||
- pos 0.000, 0.000, 0.000
|
||||
- rot 1.571, 0.000, 0.000 (XYZ)
|
||||
- scl 0.010, 0.010, 0.010
|
||||
|
||||
==== Armatures: 1
|
||||
- Armature 'Armature' 1 bones
|
||||
- Armature 'rb00_battle_00' 1 bones
|
||||
- bone 'Character1_Reference' h:(0.000, 0.000, 0.000) t:(0.000, 1.000, 0.000) radius h:0.100 t:0.050
|
||||
1.000 0.000 0.000 0.000
|
||||
0.000 1.000 0.000 0.000
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
==== Meshes: 2
|
||||
- Mesh 'MeshUnderChild' vtx:3 face:1 loop:3 edge:3
|
||||
- 0 1 2
|
||||
- 0/2 0/1 1/2
|
||||
- attr 'position' FLOAT_VECTOR POINT
|
||||
- (0.000, 0.000, 0.000)
|
||||
- (1.000, 0.000, 0.000)
|
||||
- (0.000, 1.000, 0.000)
|
||||
|
||||
- Mesh 'MeshUnderRoot' vtx:3 face:1 loop:3 edge:3
|
||||
- 0 1 2
|
||||
- 0/2 0/1 1/2
|
||||
- attr 'position' FLOAT_VECTOR POINT
|
||||
- (0.000, 0.000, 0.000)
|
||||
- (1.000, 0.000, 0.000)
|
||||
- (0.000, 1.000, 0.000)
|
||||
|
||||
==== Objects: 3
|
||||
- Obj 'Armature' ARMATURE data:'Armature'
|
||||
- pos 0.000, 0.000, 0.000
|
||||
- rot 1.571, 0.000, 0.000 (XYZ)
|
||||
- scl 0.010, 0.010, 0.010
|
||||
- Obj 'MeshUnderChild' MESH data:'MeshUnderChild' par:'Armature' par_type:BONE par_bone:'ChildBone'
|
||||
- pos 0.000, -1.000, -1.000
|
||||
- rot 0.000, 0.000, 0.000 (XYZ)
|
||||
- scl 1.000, 1.000, 1.000
|
||||
- Obj 'MeshUnderRoot' MESH data:'MeshUnderRoot' par:'Armature' par_type:BONE par_bone:'MeshUnderRoot'
|
||||
- pos 0.000, -1.000, 0.000
|
||||
- rot 0.000, 0.000, 0.000 (XYZ)
|
||||
- scl 1.000, 1.000, 1.000
|
||||
|
||||
==== Armatures: 1
|
||||
- Armature 'Armature' 3 bones
|
||||
- bone 'ChildBone' parent:'MeshUnderRoot' h:(0.000, -2.000, 0.000) t:(0.000, -1.000, 0.000) radius h:0.100 t:0.050
|
||||
1.000 0.000 0.000 0.000
|
||||
0.000 1.000 0.000 1.000
|
||||
0.000 0.000 1.000 3.000
|
||||
- bone 'MeshUnderRoot' parent:'RootBone' h:(-1.000, -1.000, 0.000) t:(-1.000, 0.000, 0.000) radius h:0.100 t:0.050
|
||||
1.000 0.000 0.000 0.000
|
||||
0.000 1.000 0.000 2.000
|
||||
0.000 0.000 1.000 3.000
|
||||
- bone 'RootBone' h:(1.000, 2.000, 3.000) t:(1.000, 3.000, 3.000) radius h:0.100 t:0.050
|
||||
1.000 0.000 0.000 1.000
|
||||
0.000 1.000 0.000 2.000
|
||||
0.000 0.000 1.000 3.000
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
==== Meshes: 2
|
||||
- Mesh 'MeshUnderChild' vtx:3 face:1 loop:3 edge:3
|
||||
- 0 1 2
|
||||
- 0/2 0/1 1/2
|
||||
- attr 'position' FLOAT_VECTOR POINT
|
||||
- (0.000, 0.000, 0.000)
|
||||
- (1.000, 0.000, 0.000)
|
||||
- (0.000, 1.000, 0.000)
|
||||
|
||||
- Mesh 'MeshUnderRoot' vtx:3 face:1 loop:3 edge:3
|
||||
- 0 1 2
|
||||
- 0/2 0/1 1/2
|
||||
- attr 'position' FLOAT_VECTOR POINT
|
||||
- (0.000, 0.000, 0.000)
|
||||
- (1.000, 0.000, 0.000)
|
||||
- (0.000, 1.000, 0.000)
|
||||
|
||||
==== Objects: 3
|
||||
- Obj 'Armature' ARMATURE data:'Armature'
|
||||
- pos 0.000, 0.000, 0.000
|
||||
- rot 1.571, 0.000, 0.000 (XYZ)
|
||||
- scl 0.010, 0.010, 0.010
|
||||
- anim act:AnimStack slot:OBArmature blend:REPLACE drivers:0
|
||||
- Obj 'MeshUnderChild' MESH data:'MeshUnderChild' par:'Armature' par_type:BONE par_bone:'ChildBone'
|
||||
- pos 0.000, -1.000, -1.000
|
||||
- rot 0.000, 0.000, 0.000 (XYZ)
|
||||
- scl 1.000, 1.000, 1.000
|
||||
- Obj 'MeshUnderRoot' MESH data:'MeshUnderRoot' par:'Armature' par_type:BONE par_bone:'MeshUnderRoot'
|
||||
- pos 0.000, -1.000, 0.000
|
||||
- rot 0.000, 0.000, 0.000 (XYZ)
|
||||
- scl 1.000, 1.000, 1.000
|
||||
|
||||
==== Actions: 1
|
||||
- Action 'AnimStack' curverange:(1.0 .. 61.0) layers:1
|
||||
- ActionLayer Layer strips:1
|
||||
- Keyframe strip channelbags:1
|
||||
- Channelbag slot 'OBArmature' curves:30
|
||||
- fcu 'pose.bones["ChildBone"].location[0]' smooth:CONT_ACCEL extra:CONSTANT keyframes:3 grp:'ChildBone'
|
||||
- (1.000, 0.000) lh:(-9.000, 0.000 AUTO_CLAMPED) rh:(11.000, 0.000 AUTO_CLAMPED)
|
||||
- (31.000, 0.000) lh:(21.000, 0.000 AUTO_CLAMPED) rh:(41.000, 0.000 AUTO_CLAMPED)
|
||||
- (61.000, 0.000) lh:(51.000, 0.000 AUTO_CLAMPED) rh:(71.000, 0.000 AUTO_CLAMPED)
|
||||
- fcu 'pose.bones["ChildBone"].location[1]' smooth:CONT_ACCEL extra:CONSTANT keyframes:3 grp:'ChildBone'
|
||||
- (1.000, 0.000) lh:(-9.000, 0.000 AUTO_CLAMPED) rh:(11.000, 0.000 AUTO_CLAMPED)
|
||||
- (31.000, 0.000) lh:(21.000, 0.000 AUTO_CLAMPED) rh:(41.000, 0.000 AUTO_CLAMPED)
|
||||
- (61.000, 0.000) lh:(51.000, 0.000 AUTO_CLAMPED) rh:(71.000, 0.000 AUTO_CLAMPED)
|
||||
- fcu 'pose.bones["ChildBone"].location[2]' smooth:CONT_ACCEL extra:CONSTANT keyframes:3 grp:'ChildBone'
|
||||
- (1.000, 0.000) lh:(-9.000, 0.000 AUTO_CLAMPED) rh:(11.000, 0.000 AUTO_CLAMPED)
|
||||
- (31.000, 0.000) lh:(21.000, 0.000 AUTO_CLAMPED) rh:(41.000, 0.000 AUTO_CLAMPED)
|
||||
- (61.000, 0.000) lh:(51.000, 0.000 AUTO_CLAMPED) rh:(71.000, 0.000 AUTO_CLAMPED)
|
||||
- fcu 'pose.bones["ChildBone"].rotation_quaternion[0]' smooth:CONT_ACCEL extra:CONSTANT keyframes:3 grp:'ChildBone'
|
||||
- (1.000, 1.000) lh:(-9.000, 1.000 AUTO_CLAMPED) rh:(11.000, 1.000 AUTO_CLAMPED)
|
||||
- (31.000, 0.966) lh:(21.000, 0.999 AUTO_CLAMPED) rh:(41.000, 0.932 AUTO_CLAMPED)
|
||||
- (61.000, 0.866) lh:(51.000, 0.866 AUTO_CLAMPED) rh:(71.000, 0.866 AUTO_CLAMPED)
|
||||
- fcu 'pose.bones["ChildBone"].rotation_quaternion[1]' smooth:CONT_ACCEL extra:CONSTANT keyframes:3 grp:'ChildBone'
|
||||
- (1.000, 0.000) lh:(-9.000, 0.000 AUTO_CLAMPED) rh:(11.000, 0.000 AUTO_CLAMPED)
|
||||
- (31.000, 0.000) lh:(21.000, 0.000 AUTO_CLAMPED) rh:(41.000, 0.000 AUTO_CLAMPED)
|
||||
- (61.000, 0.000) lh:(51.000, 0.000 AUTO_CLAMPED) rh:(71.000, 0.000 AUTO_CLAMPED)
|
||||
- fcu 'pose.bones["ChildBone"].rotation_quaternion[2]' smooth:CONT_ACCEL extra:CONSTANT keyframes:3 grp:'ChildBone'
|
||||
- (1.000, 0.000) lh:(-9.000, 0.000 AUTO_CLAMPED) rh:(11.000, 0.000 AUTO_CLAMPED)
|
||||
- (31.000, 0.000) lh:(21.000, 0.000 AUTO_CLAMPED) rh:(41.000, 0.000 AUTO_CLAMPED)
|
||||
- (61.000, 0.000) lh:(51.000, 0.000 AUTO_CLAMPED) rh:(71.000, 0.000 AUTO_CLAMPED)
|
||||
- fcu 'pose.bones["ChildBone"].rotation_quaternion[3]' smooth:CONT_ACCEL extra:CONSTANT keyframes:3 grp:'ChildBone'
|
||||
- (1.000, 0.000) lh:(-9.000, 0.000 AUTO_CLAMPED) rh:(11.000, 0.000 AUTO_CLAMPED)
|
||||
- (31.000, 0.259) lh:(21.000, 0.134 AUTO_CLAMPED) rh:(41.000, 0.384 AUTO_CLAMPED)
|
||||
- (61.000, 0.500) lh:(51.000, 0.500 AUTO_CLAMPED) rh:(71.000, 0.500 AUTO_CLAMPED)
|
||||
- fcu 'pose.bones["ChildBone"].scale[0]' smooth:CONT_ACCEL extra:CONSTANT keyframes:3 grp:'ChildBone'
|
||||
- (1.000, 1.000) lh:(-9.000, 1.000 AUTO_CLAMPED) rh:(11.000, 1.000 AUTO_CLAMPED)
|
||||
- (31.000, 1.000) lh:(21.000, 1.000 AUTO_CLAMPED) rh:(41.000, 1.000 AUTO_CLAMPED)
|
||||
- (61.000, 1.000) lh:(51.000, 1.000 AUTO_CLAMPED) rh:(71.000, 1.000 AUTO_CLAMPED)
|
||||
- fcu 'pose.bones["ChildBone"].scale[1]' smooth:CONT_ACCEL extra:CONSTANT keyframes:3 grp:'ChildBone'
|
||||
- (1.000, 1.000) lh:(-9.000, 1.000 AUTO_CLAMPED) rh:(11.000, 1.000 AUTO_CLAMPED)
|
||||
- (31.000, 1.000) lh:(21.000, 1.000 AUTO_CLAMPED) rh:(41.000, 1.000 AUTO_CLAMPED)
|
||||
- (61.000, 1.000) lh:(51.000, 1.000 AUTO_CLAMPED) rh:(71.000, 1.000 AUTO_CLAMPED)
|
||||
- fcu 'pose.bones["ChildBone"].scale[2]' smooth:CONT_ACCEL extra:CONSTANT keyframes:3 grp:'ChildBone'
|
||||
- (1.000, 1.000) lh:(-9.000, 1.000 AUTO_CLAMPED) rh:(11.000, 1.000 AUTO_CLAMPED)
|
||||
- (31.000, 1.000) lh:(21.000, 1.000 AUTO_CLAMPED) rh:(41.000, 1.000 AUTO_CLAMPED)
|
||||
- (61.000, 1.000) lh:(51.000, 1.000 AUTO_CLAMPED) rh:(71.000, 1.000 AUTO_CLAMPED)
|
||||
- fcu 'pose.bones["MeshUnderRoot"].location[0]' smooth:CONT_ACCEL extra:CONSTANT keyframes:3 grp:'MeshUnderRoot'
|
||||
- (1.000, 0.000) lh:(-9.000, 0.000 AUTO_CLAMPED) rh:(11.000, 0.000 AUTO_CLAMPED)
|
||||
- (31.000, 0.000) lh:(21.000, 0.000 AUTO_CLAMPED) rh:(41.000, 0.000 AUTO_CLAMPED)
|
||||
- (61.000, 0.000) lh:(51.000, 0.000 AUTO_CLAMPED) rh:(71.000, 0.000 AUTO_CLAMPED)
|
||||
- fcu 'pose.bones["MeshUnderRoot"].location[1]' smooth:CONT_ACCEL extra:CONSTANT keyframes:3 grp:'MeshUnderRoot'
|
||||
- (1.000, 0.000) lh:(-9.000, 0.000 AUTO_CLAMPED) rh:(11.000, 0.000 AUTO_CLAMPED)
|
||||
- (31.000, 0.000) lh:(21.000, 0.000 AUTO_CLAMPED) rh:(41.000, 0.000 AUTO_CLAMPED)
|
||||
- (61.000, 0.000) lh:(51.000, 0.000 AUTO_CLAMPED) rh:(71.000, 0.000 AUTO_CLAMPED)
|
||||
- fcu 'pose.bones["MeshUnderRoot"].location[2]' smooth:CONT_ACCEL extra:CONSTANT keyframes:3 grp:'MeshUnderRoot'
|
||||
- (1.000, 0.000) lh:(-9.000, 0.000 AUTO_CLAMPED) rh:(11.000, 0.000 AUTO_CLAMPED)
|
||||
- (31.000, 0.000) lh:(21.000, 0.000 AUTO_CLAMPED) rh:(41.000, 0.000 AUTO_CLAMPED)
|
||||
- (61.000, 0.000) lh:(51.000, 0.000 AUTO_CLAMPED) rh:(71.000, 0.000 AUTO_CLAMPED)
|
||||
- fcu 'pose.bones["MeshUnderRoot"].rotation_quaternion[0]' smooth:CONT_ACCEL extra:CONSTANT keyframes:3 grp:'MeshUnderRoot'
|
||||
- (1.000, 1.000) lh:(-9.000, 1.000 AUTO_CLAMPED) rh:(11.000, 1.000 AUTO_CLAMPED)
|
||||
- (31.000, 0.866) lh:(21.000, 0.991 AUTO_CLAMPED) rh:(41.000, 0.741 AUTO_CLAMPED)
|
||||
- (61.000, 0.500) lh:(51.000, 0.500 AUTO_CLAMPED) rh:(71.000, 0.500 AUTO_CLAMPED)
|
||||
- fcu 'pose.bones["MeshUnderRoot"].rotation_quaternion[1]' smooth:CONT_ACCEL extra:CONSTANT keyframes:3 grp:'MeshUnderRoot'
|
||||
- (1.000, 0.000) lh:(-9.000, 0.000 AUTO_CLAMPED) rh:(11.000, 0.000 AUTO_CLAMPED)
|
||||
- (31.000, 0.000) lh:(21.000, 0.000 AUTO_CLAMPED) rh:(41.000, 0.000 AUTO_CLAMPED)
|
||||
- (61.000, 0.000) lh:(51.000, 0.000 AUTO_CLAMPED) rh:(71.000, 0.000 AUTO_CLAMPED)
|
||||
|
||||
==== Armatures: 1
|
||||
- Armature 'Armature' 3 bones
|
||||
- bone 'ChildBone' parent:'MeshUnderRoot' h:(0.000, -2.000, 0.000) t:(0.000, -1.000, 0.000) radius h:0.100 t:0.050
|
||||
1.000 0.000 0.000 0.000
|
||||
0.000 1.000 0.000 1.000
|
||||
0.000 0.000 1.000 3.000
|
||||
- bone 'MeshUnderRoot' parent:'RootBone' h:(-1.000, -1.000, 0.000) t:(-1.000, 0.000, 0.000) radius h:0.100 t:0.050
|
||||
1.000 0.000 0.000 0.000
|
||||
0.000 1.000 0.000 2.000
|
||||
0.000 0.000 1.000 3.000
|
||||
- bone 'RootBone' h:(1.000, 2.000, 3.000) t:(1.000, 3.000, 3.000) radius h:0.100 t:0.050
|
||||
1.000 0.000 0.000 1.000
|
||||
0.000 1.000 0.000 2.000
|
||||
0.000 0.000 1.000 3.000
|
||||
|
||||
BIN
tests/files/io_tests/fbx/synthetic_mesh_between_bone_nodes.fbx
(Stored with Git LFS)
Normal file
BIN
tests/files/io_tests/fbx/synthetic_mesh_between_bone_nodes.fbx
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
tests/files/io_tests/fbx/synthetic_mesh_between_bone_nodes_animated.fbx
(Stored with Git LFS)
Normal file
BIN
tests/files/io_tests/fbx/synthetic_mesh_between_bone_nodes_animated.fbx
(Stored with Git LFS)
Normal file
Binary file not shown.
Reference in New Issue
Block a user