Files
coven/modules/ufbx/test/test_skin.h

520 lines
18 KiB
C

#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