#undef UFBXT_TEST_GROUP #define UFBXT_TEST_GROUP "skin" #if UFBXT_IMPL void ufbxt_check_stack_times(ufbx_scene *scene, ufbxt_diff_error *err, const char *stack_name, double begin, double end) { ufbx_anim_stack *stack = (ufbx_anim_stack*)ufbx_find_element(scene, UFBX_ELEMENT_ANIM_STACK, stack_name); ufbxt_assert(stack); ufbxt_assert(!strcmp(stack->name.data, stack_name)); ufbxt_assert_close_real(err, (ufbx_real)stack->time_begin, (ufbx_real)begin); ufbxt_assert_close_real(err, (ufbx_real)stack->time_end, (ufbx_real)end); } void ufbxt_check_frame(ufbx_scene *scene, ufbxt_diff_error *err, bool check_normals, const char *file_name, const char *anim_name, double time) { char buf[512]; snprintf(buf, sizeof(buf), "%s%s.obj", data_root, file_name); ufbxt_hintf("Frame from '%s' %s time %.2fs", anim_name ? anim_name : "(implicit animation)", buf, time); size_t obj_size = 0; void *obj_data = ufbxt_read_file(buf, &obj_size); ufbxt_obj_file *obj_file = obj_data ? ufbxt_load_obj(obj_data, obj_size, NULL) : NULL; ufbxt_assert(obj_file); free(obj_data); ufbx_evaluate_opts opts = { 0 }; opts.evaluate_skinning = true; opts.evaluate_caches = true; opts.load_external_files = true; ufbx_anim anim = scene->anim; if (anim_name) { for (size_t i = 0; i < scene->anim_stacks.count; i++) { ufbx_anim_stack *stack = scene->anim_stacks.data[i]; if (strstr(stack->name.data, anim_name)) { ufbxt_assert(stack->layers.count > 0); anim = stack->anim; break; } } } ufbx_scene *eval = ufbx_evaluate_scene(scene, &anim, time, &opts, NULL); ufbxt_assert(eval); ufbxt_check_scene(eval); uint32_t diff_flags = 0; if (check_normals) diff_flags |= UFBXT_OBJ_DIFF_FLAG_CHECK_DEFORMED_NORMALS; ufbxt_diff_to_obj(eval, obj_file, err, diff_flags); ufbx_free_scene(eval); free(obj_file); } #endif UFBXT_FILE_TEST(blender_279_sausage) #if UFBXT_IMPL { if (scene->metadata.ascii) { // ???: In the 6100 ASCII file Spin Take starts from -1 frames ufbxt_check_stack_times(scene, err, "Base", 0.0, 1.0/24.0); ufbxt_check_stack_times(scene, err, "Spin", -1.0/24.0, 18.0/24.0); ufbxt_check_stack_times(scene, err, "Wiggle", 0.0, 19.0/24.0); } else { ufbxt_check_stack_times(scene, err, "Skeleton|Base", 0.0, 1.0/24.0); ufbxt_check_stack_times(scene, err, "Skeleton|Spin", 0.0, 19.0/24.0); ufbxt_check_stack_times(scene, err, "Skeleton|Wiggle", 0.0, 19.0/24.0); } const char *cluster_names[][2] = { { "Bottom", "Cluster Skin Bottom" }, { "Middle", "Cluster Skin Middle" }, { "Top", "Cluster Skin Top" }, }; const ufbx_matrix cluster_bind_ref[][2] = { { { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f }, { 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }, }, { { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, -1.9f, 0.0f }, { 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f, 0.0f, -1.9f, 0.0f, 0.0f }, }, { { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, -3.8f, 0.0f }, { 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f, 0.0f, -3.8f, 0.0f, 0.0f }, }, }; for (size_t i = 0; i < 3; i++) { ufbx_skin_cluster *cluster = (ufbx_skin_cluster*)ufbx_find_element(scene, UFBX_ELEMENT_SKIN_CLUSTER, cluster_names[i][scene->metadata.ascii]); ufbxt_assert(cluster); ufbxt_assert_close_vec3(err, cluster->geometry_to_bone.cols[0], cluster_bind_ref[i][scene->metadata.ascii].cols[0]); ufbxt_assert_close_vec3(err, cluster->geometry_to_bone.cols[1], cluster_bind_ref[i][scene->metadata.ascii].cols[1]); ufbxt_assert_close_vec3(err, cluster->geometry_to_bone.cols[2], cluster_bind_ref[i][scene->metadata.ascii].cols[2]); ufbxt_assert_close_vec3(err, cluster->geometry_to_bone.cols[3], cluster_bind_ref[i][scene->metadata.ascii].cols[3]); } ufbxt_check_frame(scene, err, true, "blender_279_sausage_base_0", "Base", 0.0); ufbxt_check_frame(scene, err, false, "blender_279_sausage_spin_15", "Spin", 15.0/24.0); ufbxt_check_frame(scene, err, false, "blender_279_sausage_wiggle_20", "Wiggle", 20.0/24.0); } #endif UFBXT_FILE_TEST(maya_game_sausage) #if UFBXT_IMPL { } #endif UFBXT_FILE_TEST_SUFFIX(maya_game_sausage, wiggle) #if UFBXT_IMPL { ufbxt_check_frame(scene, err, true, "maya_game_sausage_wiggle_10", NULL, 10.0/24.0); ufbxt_check_frame(scene, err, true, "maya_game_sausage_wiggle_18", NULL, 18.0/24.0); } #endif UFBXT_FILE_TEST_SUFFIX(maya_game_sausage, spin) #if UFBXT_IMPL { ufbxt_check_frame(scene, err, false, "maya_game_sausage_spin_7", NULL, 27.0/24.0); ufbxt_check_frame(scene, err, false, "maya_game_sausage_spin_15", NULL, 35.0/24.0); } #endif UFBXT_FILE_TEST_SUFFIX(maya_game_sausage, deform) #if UFBXT_IMPL { ufbxt_check_frame(scene, err, false, "maya_game_sausage_deform_8", NULL, 48.0/24.0); ufbxt_check_frame(scene, err, false, "maya_game_sausage_deform_15", NULL, 55.0/24.0); } #endif UFBXT_FILE_TEST_SUFFIX(maya_game_sausage, combined) #if UFBXT_IMPL { ufbxt_check_stack_times(scene, err, "wiggle", 1.0/24.0, 20.0/24.0); ufbxt_check_stack_times(scene, err, "spin", 20.0/24.0, 40.0/24.0); ufbxt_check_stack_times(scene, err, "deform", 40.0/24.0, 60.0/24.0); ufbxt_check_frame(scene, err, true, "maya_game_sausage_wiggle_10", "wiggle", 10.0/24.0); ufbxt_check_frame(scene, err, true, "maya_game_sausage_wiggle_18", "wiggle", 18.0/24.0); ufbxt_check_frame(scene, err, false, "maya_game_sausage_spin_7", "spin", 27.0/24.0); ufbxt_check_frame(scene, err, false, "maya_game_sausage_spin_15", "spin", 35.0/24.0); ufbxt_check_frame(scene, err, false, "maya_game_sausage_deform_8", "deform", 48.0/24.0); ufbxt_check_frame(scene, err, false, "maya_game_sausage_deform_15", "deform", 55.0/24.0); } #endif UFBXT_FILE_TEST(synthetic_sausage_wiggle_no_link) #if UFBXT_IMPL { ufbxt_check_frame(scene, err, true, "maya_game_sausage_wiggle_10", NULL, 10.0/24.0); ufbxt_check_frame(scene, err, true, "maya_game_sausage_wiggle_18", NULL, 18.0/24.0); } #endif UFBXT_FILE_TEST(synthetic_sausage_wiggle_no_bind) #if UFBXT_IMPL { ufbxt_check_frame(scene, err, true, "maya_game_sausage_wiggle_10", NULL, 10.0/24.0); ufbxt_check_frame(scene, err, true, "maya_game_sausage_wiggle_18", NULL, 18.0/24.0); } #endif UFBXT_FILE_TEST(maya_blend_shape_cube) #if UFBXT_IMPL { ufbx_node *node = ufbx_find_node(scene, "pCube1"); ufbxt_assert(node && node->mesh); ufbx_mesh *mesh = node->mesh; ufbxt_assert(mesh->blend_deformers.count == 1); ufbx_blend_deformer *deformer = mesh->blend_deformers.data[0]; ufbxt_assert(deformer); ufbx_blend_channel *top[2] = { NULL, NULL }; for (size_t i = 0; i < deformer->channels.count; i++) { ufbx_blend_channel *chan = deformer->channels.data[i]; if (strstr(chan->name.data, "TopH")) top[0] = chan; if (strstr(chan->name.data, "TopV")) top[1] = chan; } ufbxt_assert(top[0] && top[1]); ufbxt_assert_close_real(err, top[0]->weight, 1.0); ufbxt_assert_close_real(err, top[1]->weight, 1.0); double keyframes[][3] = { { 1.0/24.0, 1.0, 1.0 }, { 16.0/24.0, 0.279, 0.670 }, { 53.0/24.0, 0.901, 0.168 }, { 120.0/24.0, 1.0, 1.0 }, }; ufbx_vec3 ref_offsets[2][8] = { { {0,0,0},{0,0,0},{0.317,0,0},{-0.317,0,0},{0.317,0,0},{-0.317,0,0},{0,0,0},{0,0,0}, }, { {0,0,0},{0,0,0},{0,0,-0.284},{0,0,-0.284},{0,0,0.284},{0,0,0.284},{0,0,0},{0,0,0}, }, }; for (size_t chan_ix = 0; chan_ix < 2; chan_ix++) { ufbx_blend_channel *chan = top[chan_ix]; ufbxt_assert(chan->keyframes.count == 1); ufbxt_assert_close_real(err, chan->keyframes.data[0].target_weight, 1.0); ufbx_blend_shape *shape = chan->keyframes.data[0].shape; ufbx_vec3 offsets[8] = { 0 }; ufbx_add_blend_shape_vertex_offsets(shape, offsets, 8, 1.0f); for (size_t i = 0; i < 8; i++) { ufbx_vec3 ref = ref_offsets[chan_ix][i]; ufbx_vec3 off_a = offsets[i]; ufbx_vec3 off_b = ufbx_get_blend_shape_vertex_offset(shape, i); ufbxt_assert_close_vec3(err, ref, off_a); ufbxt_assert_close_vec3(err, ref, off_b); } for (size_t key_ix = 0; key_ix < ufbxt_arraycount(keyframes); key_ix++) { double *frame = keyframes[key_ix]; double time = frame[0]; ufbx_real ref = (ufbx_real)frame[1 + chan_ix]; ufbx_real weight = ufbx_evaluate_blend_weight(&scene->anim, chan, time); ufbxt_assert_close_real(err, weight, ref); ufbx_prop prop = ufbx_evaluate_prop(&scene->anim, &chan->element, "DeformPercent", time); ufbxt_assert_close_real(err, prop.value_real / 100.0f, ref); } } { ufbx_vec3 offsets[8] = { 0 }; ufbx_add_blend_vertex_offsets(deformer, offsets, 8, 1.0f); for (size_t i = 0; i < 8; i++) { ufbx_vec3 ref = ufbxt_add3(ref_offsets[0][i], ref_offsets[1][i]); ufbxt_assert_close_vec3(err, ref, offsets[i]); } } for (int eval_skin = 0; eval_skin <= 1; eval_skin++) { for (size_t key_ix = 0; key_ix < ufbxt_arraycount(keyframes); key_ix++) { double *frame = keyframes[key_ix]; double time = frame[0]; ufbx_evaluate_opts opts = { 0 }; opts.evaluate_skinning = eval_skin != 0; ufbx_scene *state = ufbx_evaluate_scene(scene, &scene->anim, time, &opts, NULL); ufbxt_assert(state); ufbx_real weights[2]; for (size_t chan_ix = 0; chan_ix < 2; chan_ix++) { ufbx_real ref = (ufbx_real)frame[1 + chan_ix]; ufbx_blend_channel *chan = state->blend_channels.data[top[chan_ix]->typed_id]; ufbxt_assert(chan); ufbx_prop prop = ufbx_evaluate_prop(&scene->anim, &chan->element, "DeformPercent", time); ufbxt_assert_close_real(err, prop.value_real / 100.0, ref); ufbxt_assert_close_real(err, chan->weight, ref); weights[chan_ix] = chan->weight; } if (eval_skin) { ufbx_blend_deformer *eval_deformer = state->blend_deformers.data[deformer->typed_id]; ufbx_mesh *eval_mesh = state->meshes.data[mesh->typed_id]; for (size_t i = 0; i < 8; i++) { ufbx_vec3 original_pos = eval_mesh->vertex_position.values.data[i]; ufbx_vec3 skinned_pos = eval_mesh->skinned_position.values.data[i]; ufbx_vec3 ref = original_pos; ufbx_vec3 blend_pos = ufbx_get_blend_vertex_offset(eval_deformer, i); ref = ufbxt_add3(ref, ufbxt_mul3(ref_offsets[0][i], weights[0])); ref = ufbxt_add3(ref, ufbxt_mul3(ref_offsets[1][i], weights[1])); blend_pos = ufbxt_add3(original_pos, blend_pos); ufbxt_assert_close_vec3(err, ref, skinned_pos); ufbxt_assert_close_vec3(err, ref, blend_pos); } } ufbxt_check_scene(state); ufbx_free_scene(state); } } } #endif UFBXT_FILE_TEST(maya_blend_inbetween) #if UFBXT_IMPL { ufbxt_check_frame(scene, err, false, "maya_blend_inbetween_1", NULL, 1.0/24.0); ufbxt_check_frame(scene, err, false, "maya_blend_inbetween_30", NULL, 30.0/24.0); ufbxt_check_frame(scene, err, false, "maya_blend_inbetween_60", NULL, 60.0/24.0); ufbxt_check_frame(scene, err, false, "maya_blend_inbetween_65", NULL, 65.0/24.0); ufbxt_check_frame(scene, err, false, "maya_blend_inbetween_71", NULL, 71.0/24.0); ufbxt_check_frame(scene, err, false, "maya_blend_inbetween_80", NULL, 80.0/24.0); ufbxt_check_frame(scene, err, false, "maya_blend_inbetween_89", NULL, 89.0/24.0); ufbxt_check_frame(scene, err, false, "maya_blend_inbetween_120", NULL, 120.0/24.0); } #endif UFBXT_FILE_TEST(synthetic_blend_shape_order) #if UFBXT_IMPL { ufbx_node *node = ufbx_find_node(scene, "pPlatonic1"); ufbxt_assert(node); ufbx_mesh *mesh = node->mesh; ufbxt_assert(mesh); static const ufbx_vec3 ref[] = { { -0.041956f, -0.004164f, -1.064113f }, { 0.736301f, 0.734684f, -0.451944f }, { -0.263699f, 1.059604f, -0.451944f }, { -0.951595f, -0.168521f, -0.372847f }, { -0.333561f, -1.019172f, -0.372847f }, { 1.004034f, -0.525731f, -0.447214f }, { 1.019802f, -0.010427f, 0.466597f }, { 0.289087f, 1.059604f, 0.442483f }, { -0.816271f, 0.564766f, 0.459767f }, { -0.870921f, -0.699814f, 0.400384f }, { 0.514865f, -0.854815f, 0.383101f }, { 0.238471f, -0.004164f, 0.935887f }, }; ufbx_blend_shape *shape = (ufbx_blend_shape*)ufbx_find_element(scene, UFBX_ELEMENT_BLEND_SHAPE, "Target"); ufbxt_assert(shape); ufbx_vec3 pos[12]; memcpy(pos, mesh->vertices.data, sizeof(pos)); ufbx_add_blend_shape_vertex_offsets(shape, pos, 12, 0.5f); for (size_t i = 0; i < mesh->num_vertices; i++) { ufbx_vec3 vert = mesh->vertices.data[i]; ufbx_vec3 off = ufbx_get_blend_shape_vertex_offset(shape, i); ufbx_vec3 res = { vert.x+off.x*0.5f, vert.y+off.y*0.5f, vert.z+off.z*0.5f }; ufbxt_assert_close_vec3(err, res, ref[i]); ufbxt_assert_close_vec3(err, pos[i], ref[i]); } } #endif UFBXT_FILE_TEST(blender_293_half_skinned) #if UFBXT_IMPL { ufbx_node *node = ufbx_find_node(scene, "Plane"); ufbxt_assert(node); ufbx_mesh *mesh = node->mesh; ufbxt_assert(mesh); ufbxt_assert(mesh->num_vertices == 4); ufbxt_assert(mesh->skin_deformers.count == 1); ufbx_skin_deformer *skin = mesh->skin_deformers.data[0]; ufbxt_assert(skin->vertices.count == 4); ufbxt_assert(skin->vertices.data[0].num_weights == 1); ufbxt_assert(skin->vertices.data[0].weight_begin == 0); ufbxt_assert(skin->vertices.data[1].num_weights == 1); ufbxt_assert(skin->vertices.data[1].weight_begin == 1); ufbxt_assert(skin->vertices.data[2].num_weights == 0); ufbxt_assert(skin->vertices.data[3].num_weights == 0); } #endif UFBXT_FILE_TEST(maya_dual_quaternion) #if UFBXT_IMPL { ufbx_node *node = ufbx_find_node(scene, "pCube1"); ufbxt_assert(node && node->mesh); ufbx_mesh *mesh = node->mesh; ufbxt_assert(mesh->skin_deformers.count == 1); ufbx_skin_deformer *skin = mesh->skin_deformers.data[0]; ufbxt_assert(skin->skinning_method == UFBX_SKINNING_METHOD_DUAL_QUATERNION); } #endif UFBXT_FILE_TEST(maya_dual_quaternion_scale) #if UFBXT_IMPL { ufbx_node *node = ufbx_find_node(scene, "pCube1"); ufbxt_assert(node && node->mesh); ufbx_mesh *mesh = node->mesh; ufbxt_assert(mesh->skin_deformers.count == 1); ufbx_skin_deformer *skin = mesh->skin_deformers.data[0]; ufbxt_assert(skin->skinning_method == UFBX_SKINNING_METHOD_DUAL_QUATERNION); } #endif UFBXT_FILE_TEST(maya_dq_weights) #if UFBXT_IMPL { ufbxt_check_frame(scene, err, false, "maya_dq_weights_10", NULL, 10.0/24.0); ufbxt_check_frame(scene, err, false, "maya_dq_weights_18", NULL, 18.0/24.0); } #endif UFBXT_FILE_TEST(maya_bone_radius) #if UFBXT_IMPL { for (size_t i = 1; i < scene->nodes.count; i++) { ufbx_node *node = scene->nodes.data[i]; ufbxt_assert(strstr(node->name.data, "joint")); ufbxt_assert(node->bone); ufbxt_assert_close_real(err, node->bone->radius, (ufbx_real)i * 0.2f); ufbxt_assert_close_real(err, node->bone->relative_length, 1.0f); } } #endif UFBXT_FILE_TEST(blender_279_bone_radius) #if UFBXT_IMPL { ufbx_node *armature = ufbx_find_node(scene, "Armature"); ufbx_node *node = armature; for (size_t i = 0; node->children.count > 0; i++) { node = node->children.data[0]; ufbxt_assert(strstr(node->name.data, "Bone")); ufbxt_assert(node->bone); // Blender end bones have some weird scaling factor // TODO: Add quirks mode for this? if (strstr(node->name.data, "end")) continue; if (scene->metadata.ascii) { ufbxt_assert_close_real(err, node->bone->radius, 1.0f); } else { ufbxt_assert_close_real(err, node->bone->radius, (ufbx_real)(i + 1) * 0.1f); } ufbxt_assert_close_real(err, node->bone->relative_length, 1.0f); } } #endif UFBXT_FILE_TEST_FLAGS(synthetic_broken_cluster, UFBXT_FILE_TEST_FLAG_ALLOW_STRICT_ERROR) #if UFBXT_IMPL { ufbx_node *cube = ufbx_find_node(scene, "pCube1"); ufbxt_assert(cube && cube->mesh); ufbx_mesh *mesh = cube->mesh; ufbxt_assert(mesh->skin_deformers.count == 1); ufbx_skin_deformer *skin = mesh->skin_deformers.data[0]; ufbxt_assert(skin->clusters.count == 2); ufbxt_assert(!strcmp(skin->clusters.data[0]->bone_node->name.data, "joint3")); ufbxt_assert(!strcmp(skin->clusters.data[1]->bone_node->name.data, "joint1")); } #endif UFBXT_TEST(synthetic_broken_cluster_connect) #if UFBXT_IMPL { char path[512]; ufbxt_file_iterator iter = { "synthetic_broken_cluster" }; while (ufbxt_next_file(&iter, path, sizeof(path))) { ufbx_load_opts opts = { 0 }; opts.connect_broken_elements = true; ufbx_scene *scene = ufbx_load_file(path, &opts, NULL); ufbxt_assert(scene); ufbx_node *cube = ufbx_find_node(scene, "pCube1"); ufbxt_assert(cube && cube->mesh); ufbx_mesh *mesh = cube->mesh; ufbxt_assert(mesh->skin_deformers.count == 1); ufbx_skin_deformer *skin = mesh->skin_deformers.data[0]; ufbxt_assert(skin->clusters.count == 3); ufbxt_assert(!strcmp(skin->clusters.data[0]->bone_node->name.data, "joint3")); ufbxt_assert(skin->clusters.data[1]->bone_node == NULL); ufbxt_assert(!strcmp(skin->clusters.data[2]->bone_node->name.data, "joint1")); // HACK: Patch the bone of `clusters.data[1]` to be valid for `ufbxt_check_scene()`... skin->clusters.data[1]->bone_node = skin->clusters.data[0]->bone_node; ufbxt_check_scene(scene); ufbx_free_scene(scene); } } #endif UFBXT_TEST(synthetic_broken_cluster_safe) #if UFBXT_IMPL { char path[512]; // Same test as above but check that the scene is valid if we don't // pass `connect_broken_elements`. ufbxt_file_iterator iter = { "synthetic_broken_cluster" }; while (ufbxt_next_file(&iter, path, sizeof(path))) { ufbx_load_opts opts = { 0 }; ufbx_scene *scene = ufbx_load_file(path, &opts, NULL); ufbxt_assert(scene); ufbxt_check_scene(scene); ufbx_free_scene(scene); } } #endif UFBXT_FILE_TEST(max_transformed_skin) #if UFBXT_IMPL { ufbxt_check_frame(scene, err, false, "max_transformed_skin_5", NULL, 5.0/30.0); ufbxt_check_frame(scene, err, false, "max_transformed_skin_15", NULL, 15.0/30.0); } #endif UFBXT_FILE_TEST(synthetic_bind_to_root) #if UFBXT_IMPL { ufbxt_check_warning(scene, UFBX_WARNING_BAD_ELEMENT_CONNECTED_TO_ROOT, SIZE_MAX, NULL); // Some unknown exporter is exporting skin deformers being parented to root // This test exists to check that it is handled gracefully if quirks are enabled } #endif