#ifndef UFBXT_TESTING_UTILS_INCLUDED #define UFBXT_TESTING_UTILS_INCLUDED #include #include #include #include #include #include "testing_basics.h" #ifndef ufbxt_soft_assert #define ufbxt_soft_assert(cond) ufbxt_assert(cond) #endif // -- Vector helpers static ufbx_real ufbxt_dot2(ufbx_vec2 a, ufbx_vec2 b) { return a.x*b.x + a.y*b.y; } static ufbx_real ufbxt_dot3(ufbx_vec3 a, ufbx_vec3 b) { return a.x*b.x + a.y*b.y + a.z*b.z; } static ufbx_real ufbxt_length2(ufbx_vec2 a) { return (ufbx_real)sqrt(ufbxt_dot2(a, a)); } static ufbx_real ufbxt_length3(ufbx_vec3 a) { return (ufbx_real)sqrt(ufbxt_dot3(a, a)); } static ufbx_vec2 ufbxt_add2(ufbx_vec2 a, ufbx_vec2 b) { ufbx_vec2 v; v.x = a.x + b.x; v.y = a.y + b.y; return v; } static ufbx_vec3 ufbxt_add3(ufbx_vec3 a, ufbx_vec3 b) { ufbx_vec3 v; v.x = a.x + b.x; v.y = a.y + b.y; v.z = a.z + b.z; return v; } static ufbx_vec2 ufbxt_sub2(ufbx_vec2 a, ufbx_vec2 b) { ufbx_vec2 v; v.x = a.x - b.x; v.y = a.y - b.y; return v; } static ufbx_vec3 ufbxt_sub3(ufbx_vec3 a, ufbx_vec3 b) { ufbx_vec3 v; v.x = a.x - b.x; v.y = a.y - b.y; v.z = a.z - b.z; return v; } static ufbx_vec2 ufbxt_mul2(ufbx_vec2 a, ufbx_real b) { ufbx_vec2 v; v.x = a.x * b; v.y = a.y * b; return v; } static ufbx_vec3 ufbxt_mul3(ufbx_vec3 a, ufbx_real b) { ufbx_vec3 v; v.x = a.x * b; v.y = a.y * b; v.z = a.z * b; return v; } static ufbx_vec3 ufbxt_cross3(ufbx_vec3 a, ufbx_vec3 b) { ufbx_vec3 v = { a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x }; return v; } static ufbx_vec3 ufbxt_normalize(ufbx_vec3 a) { ufbx_real len = (ufbx_real)sqrt(ufbxt_dot3(a, a)); if (len != 0.0) { return ufbxt_mul3(a, (ufbx_real)1.0 / len); } else { ufbx_vec3 zero = { (ufbx_real)0 }; return zero; } } static ufbx_real ufbxt_min(ufbx_real a, ufbx_real b) { return a < b ? a : b; } static ufbx_real ufbxt_max(ufbx_real a, ufbx_real b) { return a < b ? b : a; } static ufbx_real ufbxt_clamp(ufbx_real v, ufbx_real min_v, ufbx_real max_v) { return ufbxt_min(ufbxt_max(v, min_v), max_v); } // -- obj load and diff typedef struct { const char **groups; const char *original_group; size_t num_groups; size_t num_faces; size_t num_indices; ufbx_face *faces; ufbx_vertex_vec3 vertex_position; ufbx_vertex_vec3 vertex_normal; ufbx_vertex_vec2 vertex_uv; } ufbxt_obj_mesh; typedef enum { UFBXT_OBJ_EXPORTER_UNKNOWN, UFBXT_OBJ_EXPORTER_BLENDER, } ufbxt_obj_exporter; typedef struct { ufbxt_obj_mesh *meshes; size_t num_meshes; bool bad_normals; bool bad_order; bool bad_uvs; bool bad_faces; bool line_faces; bool point_faces; bool no_subdivision; bool root_groups_at_bone; bool remove_namespaces; bool match_by_order; ufbx_real tolerance; int32_t animation_frame; bool normalize_units; bool ignore_duplicates; ufbxt_obj_exporter exporter; double position_scale; char animation_name[128]; } ufbxt_obj_file; typedef struct { const char *name_end_chars; } ufbxt_load_obj_opts; static int ufbxt_cmp_obj_mesh(const void *va, const void *vb) { const ufbxt_obj_mesh *a = (const ufbxt_obj_mesh*)va, *b = (const ufbxt_obj_mesh*)vb; if (a->num_groups < b->num_groups) return -1; if (a->num_groups > b->num_groups) return +1; return 0; } static uint16_t ufbxt_read_u16(const void *ptr) { const char *p = (const char*)ptr; return (uint16_t)( (unsigned)(uint8_t)p[0] << 0u | (unsigned)(uint8_t)p[1] << 8u ); } static uint32_t ufbxt_read_u32(const void *ptr) { const char *p = (const char*)ptr; return (uint32_t)( (unsigned)(uint8_t)p[0] << 0u | (unsigned)(uint8_t)p[1] << 8u | (unsigned)(uint8_t)p[2] << 16u | (unsigned)(uint8_t)p[3] << 24u ); } static ufbxt_noinline void *ufbxt_decompress_gzip(const void *gz_data, size_t gz_size, size_t *p_size) { const uint8_t *gz = (const uint8_t*)gz_data; if (gz_size < 10) return NULL; if (gz[0] != 0x1f || gz[1] != 0x8b || gz[2] != 0x08) return NULL; uint8_t flags = gz[3]; size_t offset = 10; if (flags & 0x4) { // FEXTRA if (gz_size - offset < 2) return NULL; uint16_t xlen = ufbxt_read_u16(gz + offset); offset += 2; if (gz_size - offset < xlen) return NULL; offset += xlen; } if (flags & 0x8) { // FNAME const uint8_t *end = (const uint8_t*)memchr(gz + offset, 0, gz_size - offset); if (!end) return NULL; offset = (size_t)(end - gz) + 1; } if (flags & 0x10) { // FCOMMENT const uint8_t *end = (const uint8_t*)memchr(gz + offset, 0, gz_size - offset); if (!end) return NULL; offset = (size_t)(end - gz) + 1; } if (flags & 0x2) { // FHCRC if (gz_size - offset < 2) return NULL; offset += 2; } uint32_t isize = ufbxt_read_u32(gz + gz_size - 4); void *dst = malloc(isize + 1); if (!dst) return NULL; ufbx_inflate_retain retain; retain.initialized = false; ufbx_inflate_input input = { 0 }; engine.input.data = gz + offset; engine.input.data_size = gz_size - offset; engine.input.total_size = engine.input.data_size; engine.input.no_header = true; engine.input.no_checksum = true; ptrdiff_t result = ufbx_inflate(dst, isize, &input, &retain); if (result != isize) { free(dst); return NULL; } ((char*)dst)[isize] = '\0'; *p_size = isize; return dst; } #define UFBXT_MAX_PARSED_INT #define UFBXT_MAX_EXPONENT 325 static double ufbxt_pow10_table[UFBXT_MAX_EXPONENT*2 + 1]; static volatile bool ufbxt_parsing_init_done = false; static void ufbxt_init_parsing() { if (ufbxt_parsing_init_done) return; for (int32_t i = 0; i <= UFBXT_MAX_EXPONENT*2; i++) { ufbxt_pow10_table[i] = pow(10.0, (double)(i - UFBXT_MAX_EXPONENT)); } ufbxt_parsing_init_done = true; } static size_t ufbxt_parse_ints(int32_t *p_result, size_t max_count, const char **str) { const char *c = *str; size_t num; for (num = 0; num < max_count; num++) { if (*c == '/') { c++; p_result[num] = 0; continue; } int32_t sign = 1; if (*c == '+') { c++; } else if (*c == '-') { sign = -1; c++; } if (!(*c >= '0' && *c <= '9')) return num; const uint32_t max_value = UINT64_C(100000000); uint32_t value = 0; do { if (value < max_value) { value = value * 10 + (uint64_t)(*c++ - '0'); } else { return num; } } while (*c >= '0' && *c <= '9'); p_result[num] = (int32_t)value * sign; if (*c != '/') return num; c++; } *str = c; return num; } static bool ufbxt_parse_reals(ufbx_real *p_result, size_t count, const char *str) { const char *c = str; for (size_t i = 0; i < count; i++) { // Skip separator while (*c == ' ') c++; const uint64_t max_value = UINT64_C(1000000000000000000); uint64_t value = 0; double sign = 1.0; if (*c == '-') { sign = -1.0; c++; } else if (*c == '+') { c++; } int32_t exponent = 0; if (!(*c >= '0' && *c <= '9')) return false; do { if (value < max_value) { value = value * 10 + (uint64_t)(*c - '0'); } else { exponent--; } c++; } while (*c >= '0' && *c <= '9'); if (*c == '.') { c++; while (*c >= '0' && *c <= '9') { if (value < max_value) { value = value * 10 + (uint64_t)(*c - '0'); exponent--; } c++; } if ((*c | 0x20) == 'e') { c++; int32_t expsign = 1; if (*c == '+') { c++; } else if (*c == '-') { expsign = -1; c++; } int32_t exp = 0; while (*c >= '0' && *c <= '9') { if (exp < 4096) { exp = exp * 10 + (int32_t)(*c - '0'); } c++; } exponent += expsign * exp; } } if (exponent >= -UFBXT_MAX_EXPONENT && exponent <= UFBXT_MAX_EXPONENT) { p_result[i] = (ufbx_real)(sign * ufbxt_pow10_table[exponent + UFBXT_MAX_EXPONENT] * (double)value); } else if (exponent < 0) { p_result[i] = 0.0f; } else { p_result[i] = (ufbx_real)INFINITY; } } return true; } static char *ufbxt_find_newline(char *src) { for (;;) { char c = *src; if (c == '\0') return NULL; if (c == '\r' || c == '\n') return src; src++; } } static bool ufbxt_is_space(char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; } static ufbxt_noinline ufbxt_obj_file *ufbxt_load_obj(void *obj_data, size_t obj_size, const ufbxt_load_obj_opts *opts) { ufbxt_load_obj_opts zero_opts; if (!opts) { memset(&zero_opts, 0, sizeof(zero_opts)); opts = &zero_opts; } ufbxt_init_parsing(); size_t num_positions = 0; size_t num_normals = 0; size_t num_uvs = 0; size_t num_faces = 0; size_t num_meshes = 0; size_t num_indices = 0; size_t num_groups = 0; size_t total_name_length = 0; bool merge_groups = false; char *line = (char*)obj_data; for (;;) { char *end = ufbxt_find_newline(line); char prev = '\0'; if (end) { prev = *end; *end = '\0'; } if (!strncmp(line, "v ", 2)) num_positions++; else if (!strncmp(line, "vt ", 3)) num_uvs++; else if (!strncmp(line, "vn ", 3)) num_normals++; else if (!strncmp(line, "f ", 2)) { num_faces++; bool prev_space = false; for (char *c = line; *c; c++) { bool space = *c == ' ' || *c == '\t'; if (space && !prev_space) num_indices++; prev_space = space; } } else if (!strncmp(line, "g default", 7)) { /* ignore default group */ } else if ((!strncmp(line, "g ", 2) && !merge_groups) || !strncmp(line, "o ", 2)) { bool prev_space = false; num_groups++; for (char *c = line; *c; c++) { bool space = *c == ' ' || *c == '\t'; if (space && !prev_space) num_groups++; if (!space) total_name_length++; total_name_length++; prev_space = space; } total_name_length++; num_meshes++; } else if (strstr(line, "ufbx:merge_groups")) { merge_groups = true; } if (end) { *end = prev; line = end + 1; } else { break; } } bool implicit_mesh = false; if (num_meshes == 0) { num_meshes = 1; implicit_mesh = true; } total_name_length += num_groups; size_t alloc_size = 0; alloc_size += sizeof(ufbxt_obj_file); alloc_size += num_meshes * sizeof(ufbxt_obj_mesh); alloc_size += num_groups * sizeof(const char*); alloc_size += num_positions * sizeof(ufbx_vec3); alloc_size += num_normals * sizeof(ufbx_vec3); alloc_size += num_uvs * sizeof(ufbx_vec2); alloc_size += num_faces * sizeof(ufbx_face); alloc_size += num_indices * 3 * sizeof(int32_t); alloc_size += total_name_length * sizeof(char); void *data = malloc(alloc_size); ufbxt_assert(data); ufbxt_obj_file *obj = (ufbxt_obj_file*)data; const char **group_ptrs = (const char**)(obj + 1); ufbxt_obj_mesh *meshes = (ufbxt_obj_mesh*)(group_ptrs + num_groups); ufbx_vec3 *positions = (ufbx_vec3*)(meshes + num_meshes); ufbx_vec3 *normals = (ufbx_vec3*)(positions + num_positions); ufbx_vec2 *uvs = (ufbx_vec2*)(normals + num_normals); ufbx_face *faces = (ufbx_face*)(uvs + num_uvs); int32_t *position_indices = (int32_t*)(faces + num_faces); int32_t *normal_indices = (int32_t*)(position_indices + num_indices); int32_t *uv_indices = (int32_t*)(normal_indices + num_indices); char *name_data = (char*)(uv_indices + num_indices); void *data_end = name_data + total_name_length; ufbxt_assert((char*)data_end - (char*)data == alloc_size); memset(obj, 0, sizeof(ufbxt_obj_file)); ufbx_vec3 *dp = positions; ufbx_vec3 *dn = normals; ufbx_vec2 *du = uvs; ufbxt_obj_mesh *mesh = NULL; int32_t *dpi = position_indices; int32_t *dni = normal_indices; int32_t *dui = uv_indices; ufbx_face *df = faces; obj->meshes = meshes; obj->num_meshes = num_meshes; obj->tolerance = 0.001f; obj->normalize_units = false; obj->animation_frame = -1; obj->exporter = UFBXT_OBJ_EXPORTER_UNKNOWN; obj->position_scale = 1.0; if (implicit_mesh) { mesh = meshes; memset(mesh, 0, sizeof(ufbxt_obj_mesh)); mesh->faces = df; mesh->vertex_position.values.data = positions; mesh->vertex_normal.values.data = normals; mesh->vertex_uv.values.data = uvs; mesh->vertex_position.indices.data = (uint32_t*)dpi; mesh->vertex_normal.indices.data = (uint32_t*)dni; mesh->vertex_uv.indices.data = (uint32_t*)dui; } line = (char*)obj_data; for (;;) { char *line_end = ufbxt_find_newline(line); char prev = '\0'; if (line_end) { prev = *line_end; *line_end = '\0'; } if (!strncmp(line, "v ", 2)) { line += 2; ufbxt_assert(ufbxt_parse_reals(dp->v, 3, line)); dp++; } else if (!strncmp(line, "vt ", 3)) { line += 3; ufbxt_assert(ufbxt_parse_reals(du->v, 2, line)); du++; } else if (!strncmp(line, "vn ", 3)) { line += 3; ufbxt_assert(ufbxt_parse_reals(dn->v, 3, line)); dn++; } else if (!strncmp(line, "f ", 2)) { ufbxt_assert(mesh); df->index_begin = (uint32_t)mesh->num_indices; df->num_indices = 0; char *begin = line + 2; do { char *end = strchr(begin, ' '); if (end) *end++ = '\0'; while (ufbxt_is_space(*begin)) { begin++; } if (*begin == '\0') { begin = end; continue; } int32_t indices[3] = { 0, 0, 0 }; ufbxt_parse_ints(indices, 3, (const char**)&begin); mesh->vertex_position.indices.count++; mesh->vertex_normal.indices.count++; mesh->vertex_uv.indices.count++; mesh->vertex_position.values.count = (size_t)(dp - positions); mesh->vertex_normal.values.count = (size_t)(dn - normals); mesh->vertex_uv.values.count = (size_t)(du - uvs); *dpi++ = indices[0] - 1; *dni++ = indices[2] - 1; *dui++ = indices[1] - 1; mesh->num_indices++; df->num_indices++; begin = end; } while (begin); mesh->num_faces++; df++; } else if (!strncmp(line, "g default", 7)) { /* ignore default group */ } else if ((!strncmp(line, "g ", 2) && !merge_groups) || !strncmp(line, "o ", 2)) { mesh = mesh ? mesh + 1 : meshes; memset(mesh, 0, sizeof(ufbxt_obj_mesh)); size_t groups_len = strlen(line + 2); memcpy(name_data, line + 2, groups_len + 1); mesh->original_group = name_data; name_data += groups_len + 1; mesh->groups = group_ptrs; const char *c = line + 2; for (;;) { while (*c == ' ' || *c == '\t' || *c == '\r') { c++; } if (*c == '\n' || *c == '\0') break; const char *group_begin = c; char *group_copy = name_data; char *dst = group_copy; while (*c != ' ' && *c != '\t' && *c != '\r' && *c != '\n' && *c != '\0') { if (!strncmp(c, "FBXASC", 6)) { c += 6; char num[4] = { 0 }; if (*c != '\0') num[0] = *c++; if (*c != '\0') num[1] = *c++; if (*c != '\0') num[2] = *c++; *dst++ = (char)atoi(num); } else { *dst++ = *c++; } } *dst++ = '\0'; name_data = dst; *group_ptrs++ = group_copy; } mesh->num_groups = group_ptrs - mesh->groups; mesh->faces = df; mesh->vertex_position.values.data = positions; mesh->vertex_normal.values.data = normals; mesh->vertex_uv.values.data = uvs; mesh->vertex_position.indices.data = (uint32_t*)dpi; mesh->vertex_normal.indices.data = (uint32_t*)dni; mesh->vertex_uv.indices.data = (uint32_t*)dui; } if (line[0] == '#') { line += 1; char *end = line_end; while (line < end && (line[0] == ' ' || line[0] == '\t')) { line++; } while (end > line && (end[-1] == ' ' || end[-1] == '\t')) { *--end = '\0'; } if (!strcmp(line, "ufbx:bad_normals")) { obj->bad_normals = true; } if (!strcmp(line, "ufbx:bad_order")) { obj->bad_order = true; } if (!strcmp(line, "ufbx:bad_uvs")) { obj->bad_uvs = true; } if (!strcmp(line, "ufbx:bad_faces")) { obj->bad_faces = true; } if (!strcmp(line, "ufbx:line_faces")) { obj->line_faces = true; } if (!strcmp(line, "ufbx:point_faces")) { obj->point_faces = true; } if (!strcmp(line, "ufbx:no_subdivision")) { obj->no_subdivision = true; } if (!strcmp(line, "ufbx:ignore_duplicates")) { obj->ignore_duplicates = true; } if (!strcmp(line, "ufbx:root_groups_at_bone")) { obj->root_groups_at_bone = true; } if (!strcmp(line, "ufbx:remove_namespaces")) { obj->remove_namespaces = true; } if (!strcmp(line, "ufbx:match_by_order")) { obj->match_by_order = true; } if (!strcmp(line, "www.blender.org")) { obj->exporter = UFBXT_OBJ_EXPORTER_BLENDER; } if (!strncmp(line, "ufbx:animation=", 15)) { line += 15; size_t len = strcspn(line, "\r\n"); ufbxt_assert(len + 1 < sizeof(obj->animation_name)); memcpy(obj->animation_name, line, len); } double tolerance = 0.0; if (sscanf(line, "ufbx:tolerance=%lf", &tolerance) == 1) { obj->tolerance = (ufbx_real)tolerance; } double position_scale = 0.0; if (sscanf(line, "ufbx:position_scale=%lf", &position_scale) == 1) { obj->position_scale = (ufbx_real)position_scale; } int frame = 0; if (sscanf(line, "ufbx:frame=%d", &frame) == 1) { obj->animation_frame = (int32_t)frame; } } if (line_end) { *line_end = prev; line = line_end + 1; } else { break; } } qsort(obj->meshes, obj->num_meshes, sizeof(ufbxt_obj_mesh), ufbxt_cmp_obj_mesh); return obj; } static ufbxt_noinline void ufbxt_debug_dump_obj_mesh(const char *file, ufbx_node *node, ufbx_mesh *mesh) { FILE *f = fopen(file, "wb"); ufbxt_assert(f); fprintf(f, "s 1\n"); for (size_t i = 0; i < mesh->vertex_position.values.count; i++) { ufbx_vec3 v = mesh->vertex_position.values.data[i]; v = ufbx_transform_position(&node->geometry_to_world, v); fprintf(f, "v %f %f %f\n", v.x, v.y, v.z); } for (size_t i = 0; i < mesh->vertex_uv.values.count; i++) { ufbx_vec2 v = mesh->vertex_uv.values.data[i]; fprintf(f, "vt %f %f\n", v.x, v.y); } ufbx_matrix mat = ufbx_matrix_for_normals(&node->geometry_to_world); for (size_t i = 0; i < mesh->vertex_normal.values.count; i++) { ufbx_vec3 v = mesh->vertex_normal.values.data[i]; v = ufbx_transform_direction(&mat, v); fprintf(f, "vn %f %f %f\n", v.x, v.y, v.z); } for (size_t fi = 0; fi < mesh->num_faces; fi++) { ufbx_face face = mesh->faces.data[fi]; fprintf(f, "f"); for (size_t ci = 0; ci < face.num_indices; ci++) { int32_t vi = mesh->vertex_position.indices.data[face.index_begin + ci]; int32_t ti = mesh->vertex_uv.indices.data[face.index_begin + ci]; int32_t ni = mesh->vertex_normal.indices.data[face.index_begin + ci]; fprintf(f, " %d/%d/%d", vi + 1, ti + 1, ni + 1); } fprintf(f, "\n"); } fclose(f); } static ufbxt_noinline void ufbxt_debug_dump_obj_scene(const char *file, ufbx_scene *scene) { FILE *f = fopen(file, "wb"); ufbxt_assert(f); for (size_t mi = 0; mi < scene->meshes.count; mi++) { ufbx_mesh *mesh = scene->meshes.data[mi]; for (size_t ni = 0; ni < mesh->instances.count; ni++) { ufbx_node *node = mesh->instances.data[ni]; for (size_t i = 0; i < mesh->vertex_position.values.count; i++) { ufbx_vec3 v = mesh->skinned_position.values.data[i]; if (mesh->skinned_is_local) { v = ufbx_transform_position(&node->geometry_to_world, v); } fprintf(f, "v %f %f %f\n", v.x, v.y, v.z); } for (size_t i = 0; i < mesh->vertex_uv.values.count; i++) { ufbx_vec2 v = mesh->vertex_uv.values.data[i]; fprintf(f, "vt %f %f\n", v.x, v.y); } ufbx_matrix mat = ufbx_matrix_for_normals(&node->geometry_to_world); for (size_t i = 0; i < mesh->skinned_normal.values.count; i++) { ufbx_vec3 v = mesh->skinned_normal.values.data[i]; if (mesh->skinned_is_local) { v = ufbx_transform_direction(&mat, v); } fprintf(f, "vn %f %f %f\n", v.x, v.y, v.z); } fprintf(f, "\n"); } } int32_t v_off = 0, t_off = 0, n_off = 0; for (size_t mi = 0; mi < scene->meshes.count; mi++) { ufbx_mesh *mesh = scene->meshes.data[mi]; for (size_t ni = 0; ni < mesh->instances.count; ni++) { ufbx_node *node = mesh->instances.data[ni]; fprintf(f, "g %s\n", node->name.data); for (size_t fi = 0; fi < mesh->num_faces; fi++) { ufbx_face face = mesh->faces.data[fi]; fprintf(f, "f"); for (size_t ci = 0; ci < face.num_indices; ci++) { int32_t vi = v_off + mesh->skinned_position.indices.data[face.index_begin + ci]; int32_t ni = n_off + mesh->skinned_normal.indices.data[face.index_begin + ci]; if (mesh->vertex_uv.exists) { int32_t ti = t_off + mesh->vertex_uv.indices.data[face.index_begin + ci]; fprintf(f, " %d/%d/%d", vi + 1, ti + 1, ni + 1); } else { fprintf(f, " %d//%d", vi + 1, ni + 1); } } fprintf(f, "\n"); } fprintf(f, "\n"); v_off += (int32_t)mesh->skinned_position.values.count; t_off += (int32_t)mesh->vertex_uv.values.count; n_off += (int32_t)mesh->skinned_normal.values.count; } } fclose(f); } typedef struct { size_t num; ufbx_real sum; ufbx_real max; } ufbxt_diff_error; static void ufbxt_assert_close_real(ufbxt_diff_error *p_err, ufbx_real a, ufbx_real b) { ufbx_real err = fabs(a - b); ufbxt_soft_assert(err < 0.001); p_err->num++; p_err->sum += err; if (err > p_err->max) p_err->max = err; } static void ufbxt_assert_close_vec2(ufbxt_diff_error *p_err, ufbx_vec2 a, ufbx_vec2 b) { ufbxt_assert_close_real(p_err, a.x, b.x); ufbxt_assert_close_real(p_err, a.y, b.y); } static void ufbxt_assert_close_vec3(ufbxt_diff_error *p_err, ufbx_vec3 a, ufbx_vec3 b) { ufbxt_assert_close_real(p_err, a.x, b.x); ufbxt_assert_close_real(p_err, a.y, b.y); ufbxt_assert_close_real(p_err, a.z, b.z); } static void ufbxt_assert_close_vec4(ufbxt_diff_error *p_err, ufbx_vec4 a, ufbx_vec4 b) { ufbxt_assert_close_real(p_err, a.x, b.x); ufbxt_assert_close_real(p_err, a.y, b.y); ufbxt_assert_close_real(p_err, a.z, b.z); ufbxt_assert_close_real(p_err, a.w, b.w); } static void ufbxt_assert_close_quat(ufbxt_diff_error *p_err, ufbx_quat a, ufbx_quat b) { ufbxt_assert_close_real(p_err, a.x, b.x); ufbxt_assert_close_real(p_err, a.y, b.y); ufbxt_assert_close_real(p_err, a.z, b.z); ufbxt_assert_close_real(p_err, a.w, b.w); } static void ufbxt_assert_close_matrix(ufbxt_diff_error *p_err, ufbx_matrix a, ufbx_matrix b) { ufbxt_assert_close_vec3(p_err, a.cols[0], b.cols[0]); ufbxt_assert_close_vec3(p_err, a.cols[1], b.cols[1]); ufbxt_assert_close_vec3(p_err, a.cols[2], b.cols[2]); ufbxt_assert_close_vec3(p_err, a.cols[3], b.cols[3]); } static void ufbxt_assert_close_real_threshold(ufbxt_diff_error *p_err, ufbx_real a, ufbx_real b, ufbx_real threshold) { ufbx_real err = fabs(a - b); ufbxt_assert(err < threshold); p_err->num++; p_err->sum += err; if (err > p_err->max) p_err->max = err; } static void ufbxt_assert_close_vec2_threshold(ufbxt_diff_error *p_err, ufbx_vec2 a, ufbx_vec2 b, ufbx_real threshold) { ufbxt_assert_close_real_threshold(p_err, a.x, b.x, threshold); ufbxt_assert_close_real_threshold(p_err, a.y, b.y, threshold); } static void ufbxt_assert_close_vec3_threshold(ufbxt_diff_error *p_err, ufbx_vec3 a, ufbx_vec3 b, ufbx_real threshold) { ufbxt_assert_close_real_threshold(p_err, a.x, b.x, threshold); ufbxt_assert_close_real_threshold(p_err, a.y, b.y, threshold); ufbxt_assert_close_real_threshold(p_err, a.z, b.z, threshold); } static void ufbxt_assert_close_vec4_threshold(ufbxt_diff_error *p_err, ufbx_vec4 a, ufbx_vec4 b, ufbx_real threshold) { ufbxt_assert_close_real_threshold(p_err, a.x, b.x, threshold); ufbxt_assert_close_real_threshold(p_err, a.y, b.y, threshold); ufbxt_assert_close_real_threshold(p_err, a.z, b.z, threshold); ufbxt_assert_close_real_threshold(p_err, a.w, b.w, threshold); } static void ufbxt_assert_close_quat_threshold(ufbxt_diff_error *p_err, ufbx_quat a, ufbx_quat b, ufbx_real threshold) { ufbxt_assert_close_real_threshold(p_err, a.x, b.x, threshold); ufbxt_assert_close_real_threshold(p_err, a.y, b.y, threshold); ufbxt_assert_close_real_threshold(p_err, a.z, b.z, threshold); ufbxt_assert_close_real_threshold(p_err, a.w, b.w, threshold); } static ufbxt_noinline void ufbxt_check_source_vertices(ufbx_mesh *mesh, ufbx_mesh *src_mesh, ufbxt_diff_error *p_err) { ufbx_subdivision_result *sub = mesh->subdivision_result; ufbxt_assert(sub); size_t num_vertices = mesh->num_vertices; ufbxt_assert(sub->source_vertex_ranges.count == num_vertices); for (size_t vi = 0; vi < num_vertices; vi++) { ufbx_subdivision_weight_range range = sub->source_vertex_ranges.data[vi]; ufbx_vec3 sum = ufbx_zero_vec3; for (size_t i = 0; i < range.num_weights; i++) { ufbx_subdivision_weight weight = sub->source_vertex_weights.data[range.weight_begin + i]; ufbx_vec3 v = src_mesh->vertices.data[weight.index]; sum.x += v.x * weight.weight; sum.y += v.y * weight.weight; sum.z += v.z * weight.weight; } ufbx_vec3 ref = mesh->vertices.data[vi]; ufbxt_assert_close_vec3(p_err, ref, sum); ref = ref; } } typedef struct { ufbx_vec3 pos; ufbx_vec3 normal; ufbx_vec2 uv; } ufbxt_match_vertex; static int ufbxt_cmp_sub_vertex(const void *va, const void *vb) { const ufbxt_match_vertex *a = (const ufbxt_match_vertex*)va, *b = (const ufbxt_match_vertex*)vb; if (a->pos.x != b->pos.x) return a->pos.x < b->pos.x ? -1 : +1; if (a->pos.y != b->pos.y) return a->pos.y < b->pos.y ? -1 : +1; if (a->pos.z != b->pos.z) return a->pos.z < b->pos.z ? -1 : +1; if (a->normal.x != b->normal.x) return a->normal.x < b->normal.x ? -1 : +1; if (a->normal.y != b->normal.y) return a->normal.y < b->normal.y ? -1 : +1; if (a->normal.z != b->normal.z) return a->normal.z < b->normal.z ? -1 : +1; if (a->uv.x != b->uv.x) return a->uv.x < b->uv.x ? -1 : +1; if (a->uv.y != b->uv.y) return a->uv.y < b->uv.y ? -1 : +1; return 0; } static ufbxt_noinline void ufbxt_match_obj_mesh(ufbxt_obj_file *obj, ufbx_node *fbx_node, ufbx_mesh *fbx_mesh, ufbxt_obj_mesh *obj_mesh, ufbxt_diff_error *p_err, double scale) { ufbx_real tolerance = obj->tolerance; ufbxt_assert(fbx_mesh->num_faces == obj_mesh->num_faces); ufbxt_assert(fbx_mesh->num_indices == obj_mesh->num_indices); // Check that all vertices exist, anything more doesn't really make sense ufbxt_match_vertex *obj_verts = (ufbxt_match_vertex*)calloc(obj_mesh->num_indices, sizeof(ufbxt_match_vertex)); ufbxt_match_vertex *fbx_verts = (ufbxt_match_vertex*)calloc(fbx_mesh->num_indices, sizeof(ufbxt_match_vertex)); ufbxt_assert(obj_verts && fbx_verts); ufbx_matrix norm_mat = ufbx_get_compatible_matrix_for_normals(fbx_node); for (size_t i = 0; i < obj_mesh->num_indices; i++) { obj_verts[i].pos = ufbx_get_vertex_vec3(&obj_mesh->vertex_position, i); obj_verts[i].normal = ufbx_get_vertex_vec3(&obj_mesh->vertex_normal, i); if (obj_mesh->vertex_uv.exists) { obj_verts[i].uv = ufbx_get_vertex_vec2(&obj_mesh->vertex_uv, i); } if (scale != 1.0) { obj_verts[i].pos.x *= scale; obj_verts[i].pos.y *= scale; obj_verts[i].pos.x *= scale; } } for (size_t i = 0; i < fbx_mesh->num_indices; i++) { ufbx_vec3 fp = ufbx_get_vertex_vec3(&fbx_mesh->skinned_position, i); ufbx_vec3 fn = fbx_mesh->skinned_normal.exists ? ufbx_get_vertex_vec3(&fbx_mesh->skinned_normal, i) : ufbx_zero_vec3; if (fbx_mesh->skinned_is_local) { fp = ufbx_transform_position(&fbx_node->geometry_to_world, fp); fn = ufbx_transform_direction(&norm_mat, fn); fn = ufbxt_normalize(fn); } fbx_verts[i].pos = fp; fbx_verts[i].normal = fn; if (obj_mesh->vertex_uv.exists) { ufbxt_assert(fbx_mesh->vertex_uv.exists); fbx_verts[i].uv = ufbx_get_vertex_vec2(&fbx_mesh->vertex_uv, i); } if (scale != 1.0) { fbx_verts[i].pos.x *= scale; fbx_verts[i].pos.y *= scale; fbx_verts[i].pos.x *= scale; } } qsort(obj_verts, obj_mesh->num_indices, sizeof(ufbxt_match_vertex), &ufbxt_cmp_sub_vertex); qsort(fbx_verts, fbx_mesh->num_indices, sizeof(ufbxt_match_vertex), &ufbxt_cmp_sub_vertex); for (int32_t i = (int32_t)fbx_mesh->num_indices - 1; i >= 0; i--) { ufbxt_match_vertex v = fbx_verts[i]; bool found = false; for (int32_t j = i; j >= 0 && obj_verts[j].pos.x >= v.pos.x - tolerance; j--) { ufbx_real dx = obj_verts[j].pos.x - v.pos.x; ufbx_real dy = obj_verts[j].pos.y - v.pos.y; ufbx_real dz = obj_verts[j].pos.z - v.pos.z; ufbx_real dnx = obj_verts[j].normal.x - v.normal.x; ufbx_real dny = obj_verts[j].normal.y - v.normal.y; ufbx_real dnz = obj_verts[j].normal.z - v.normal.z; ufbx_real du = obj_verts[j].uv.x - v.uv.x; ufbx_real dv = obj_verts[j].uv.y - v.uv.y; if (obj->bad_normals) { dnx = 0.0f; dny = 0.0f; dnz = 0.0f; } if (obj->bad_uvs) { du = 0.0f; dv = 0.0f; } ufbxt_assert(dx <= tolerance); ufbx_real err = (ufbx_real)sqrt(dx*dx + dy*dy + dz*dz + dnx*dnx + dny*dny + dnz*dnz + du*du + dv*dv); if (err < tolerance) { if (err > p_err->max) p_err->max = err; p_err->sum += err; p_err->num++; obj_verts[j] = obj_verts[i]; found = true; break; } } ufbxt_assert(found); } free(obj_verts); free(fbx_verts); } typedef struct { const char *cat_name; ufbx_node *node; } ufbxt_obj_node; static int ufbxt_cmp_string(const void *va, const void *vb) { const char **a = (const char**)va, **b = (const char**)vb; return strcmp(*a, *b); } static int ufbxt_cmp_obj_node(const void *va, const void *vb) { const ufbxt_obj_node *a = (const ufbxt_obj_node*)va, *b = (const ufbxt_obj_node*)vb; return strcmp(a->cat_name, b->cat_name); } static ufbxt_noinline size_t ufbxt_obj_group_key(char *cat_buf, size_t cat_cap, const char **groups, size_t num_groups, bool remove_namespaces) { ufbxt_assert(num_groups > 0); qsort((void*)groups, num_groups, sizeof(const char*), &ufbxt_cmp_string); char *cat_ptr = cat_buf, *cat_end = cat_buf + cat_cap; for (size_t i = 0; i < num_groups; i++) { const char *group = groups[i]; if (remove_namespaces) { for (;;) { const char *next = strchr(group, ':'); if (!next) break; group = next + 1; } } size_t space = (size_t)(cat_end - cat_ptr); int res = snprintf(cat_ptr, space, "%s\n", group); ufbxt_assert(res >= 0 && (size_t)res < space); cat_ptr += res; } *--cat_ptr = '\0'; return cat_ptr - cat_buf; } static ufbxt_noinline bool ufbxt_face_has_duplicate_vertex(ufbx_mesh *mesh, ufbx_face face) { for (size_t i = 0; i < face.num_indices; i++) { uint32_t ix = mesh->vertex_indices.data[face.index_begin + i]; for (size_t j = 0; j < i; j++) { uint32_t jx = mesh->vertex_indices.data[face.index_begin + j]; if (ix == jx) { return true; } } } return false; } enum { UFBXT_OBJ_DIFF_FLAG_CHECK_DEFORMED_NORMALS = 0x1, UFBXT_OBJ_DIFF_FLAG_IGNORE_NORMALS = 0x2, }; static ufbxt_noinline void ufbxt_diff_to_obj(ufbx_scene *scene, ufbxt_obj_file *obj, ufbxt_diff_error *p_err, uint32_t flags) { ufbx_node **used_nodes = (ufbx_node**)malloc(obj->num_meshes * sizeof(ufbx_node*)); ufbxt_assert(used_nodes); size_t num_used_nodes = 0; char cat_name[4096]; const char *groups[64]; char *cat_name_data = NULL; size_t cat_name_offset = 0; ufbxt_obj_node *obj_nodes = (ufbxt_obj_node*)calloc(scene->nodes.count, sizeof(ufbxt_obj_node)); ufbxt_assert(obj_nodes); size_t num_obj_nodes = 0; for (int step = 0; step < 2; step++) { for (size_t node_i = 0; node_i < scene->nodes.count; node_i++) { ufbx_node *node = scene->nodes.data[node_i]; if (!node->name.length) continue; size_t num_groups = 0; ufbx_node *parent = node; while (parent && !parent->is_root) { if (obj->root_groups_at_bone && parent->attrib_type == UFBX_ELEMENT_BONE) break; ufbxt_assert(num_groups < ufbxt_arraycount(groups)); groups[num_groups++] = parent->name.data; parent = parent->parent; } if (num_groups == 0) continue; size_t cat_len = ufbxt_obj_group_key(cat_name, sizeof(cat_name), groups, num_groups, obj->remove_namespaces); if (cat_name_data) { char *dst = cat_name_data + cat_name_offset; memcpy(dst, cat_name, cat_len + 1); ufbxt_obj_node *obj_node = &obj_nodes[num_obj_nodes++]; obj_node->cat_name = dst; obj_node->node = node; } cat_name_offset += cat_len + 1; } if (cat_name_data) break; cat_name_data = (char*)malloc(cat_name_offset); ufbxt_assert(cat_name_data); cat_name_offset = 0; } qsort(obj_nodes, num_obj_nodes, sizeof(ufbxt_obj_node), &ufbxt_cmp_obj_node); for (size_t mesh_i = 0; mesh_i < obj->num_meshes; mesh_i++) { ufbxt_obj_mesh *obj_mesh = &obj->meshes[mesh_i]; if (obj_mesh->num_indices == 0) continue; ufbx_node *node = NULL; if (obj->match_by_order) { ufbxt_assert(mesh_i + 1 < scene->nodes.count); node = scene->nodes.data[mesh_i + 1]; } // Search for a node containing all the groups if (!node && obj_mesh->num_groups > 0) { ufbxt_obj_group_key(cat_name, sizeof(cat_name), obj_mesh->groups, obj_mesh->num_groups, obj->remove_namespaces); ufbxt_obj_node key = { cat_name }; ufbxt_obj_node *found = (ufbxt_obj_node*)bsearch(&key, obj_nodes, num_obj_nodes, sizeof(ufbxt_obj_node), &ufbxt_cmp_obj_node); if (found) { if (found->node && found->node->mesh) { bool seen = false; for (size_t i = 0; i < num_used_nodes; i++) { if (used_nodes[i] == found->node) { seen = true; break; } } if (!seen) { node = found->node; } } } } if (!node && obj_mesh->original_group && *obj_mesh->original_group) { node = ufbx_find_node(scene, obj_mesh->original_group); } if (!node) { for (size_t group_i = 0; group_i < obj_mesh->num_groups; group_i++) { const char *name = obj_mesh->groups[group_i]; node = ufbx_find_node(scene, name); if (!node && obj->exporter == UFBXT_OBJ_EXPORTER_BLENDER) { // Blender concatenates _Material to names size_t name_len = strcspn(name, "_"); node = ufbx_find_node_len(scene, name, name_len); } if (node && node->mesh) { bool seen = false; for (size_t i = 0; i < num_used_nodes; i++) { if (used_nodes[i] == node) { seen = true; break; } } if (!seen) break; } } } if (obj->ignore_duplicates) { if (!node) { bool is_ncl = false; for (size_t i = 0; i < obj_mesh->num_groups; i++) { if (strstr(obj_mesh->groups[i], "_ncl1_")) { is_ncl = true; break; } } if (is_ncl) continue; } if (node && node->parent) { bool duplicate_name = false; ufbx_node *parent = node->parent; for (size_t i = 0; i < parent->children.count; i++) { ufbx_node *child = parent->children.data[i]; if (child == node) continue; if (!strcmp(child->name.data, node->name.data)) { duplicate_name = true; break; } } if (duplicate_name) continue; } } if (!node) { ufbxt_assert(scene->meshes.count == 1); ufbxt_assert(scene->meshes.data[0]->instances.count == 1); node = scene->meshes.data[0]->instances.data[0]; } ufbxt_assert(node); if (node->geometry_transform_helper) { node = node->geometry_transform_helper; } ufbx_mesh *mesh = node->mesh; double scale = 1.0; if (obj->normalize_units && scene->settings.unit_meters != 1.0) { scale *= scene->settings.unit_meters; } if (obj->position_scale != 1.0) { scale *= obj->position_scale; } used_nodes[num_used_nodes++] = node; if (!mesh && node->attrib_type == UFBX_ELEMENT_NURBS_SURFACE) { ufbx_nurbs_surface *surface = (ufbx_nurbs_surface*)node->attrib; ufbx_tessellate_surface_opts opts = { 0 }; opts.span_subdivision_u = surface->span_subdivision_u; opts.span_subdivision_v = surface->span_subdivision_v; ufbx_mesh *tess_mesh = ufbx_tessellate_nurbs_surface(surface, &opts, NULL); ufbxt_assert(tess_mesh); // ufbxt_debug_dump_obj_mesh("test.obj", node, tess_mesh); ufbxt_check_mesh(scene, tess_mesh); ufbxt_match_obj_mesh(obj, node, tess_mesh, obj_mesh, p_err, scale); ufbx_free_mesh(tess_mesh); continue; } ufbxt_assert(mesh); // Multiple skin deformers are not supported if (mesh->skin_deformers.count > 1) continue; ufbx_matrix *mat = &node->geometry_to_world; ufbx_matrix norm_mat = ufbx_get_compatible_matrix_for_normals(node); if (!obj->no_subdivision && (mesh->subdivision_display_mode == UFBX_SUBDIVISION_DISPLAY_SMOOTH || mesh->subdivision_display_mode == UFBX_SUBDIVISION_DISPLAY_HULL_AND_SMOOTH)) { ufbx_subdivide_opts opts = { 0 }; opts.evaluate_source_vertices = true; opts.evaluate_skin_weights = true; ufbx_mesh *sub_mesh = ufbx_subdivide_mesh(mesh, mesh->subdivision_preview_levels, &opts, NULL); ufbxt_assert(sub_mesh); // Check that we didn't break the original mesh ufbxt_check_mesh(scene, mesh); ufbxt_check_source_vertices(sub_mesh, mesh, p_err); ufbxt_check_mesh(scene, sub_mesh); ufbxt_match_obj_mesh(obj, node, sub_mesh, obj_mesh, p_err, scale); ufbx_free_mesh(sub_mesh); continue; } if (!(obj->bad_faces || obj->line_faces || obj->point_faces)) { ufbxt_assert(obj_mesh->num_faces == mesh->num_faces); ufbxt_assert(obj_mesh->num_indices == mesh->num_indices); } bool check_normals = true; if (obj->bad_normals) check_normals = false; if ((flags & UFBXT_OBJ_DIFF_FLAG_CHECK_DEFORMED_NORMALS) == 0 && mesh->all_deformers.count > 0) check_normals = false; if ((flags & UFBXT_OBJ_DIFF_FLAG_IGNORE_NORMALS) != 0) check_normals = false; if (obj->bad_order) { ufbxt_match_obj_mesh(obj, node, mesh, obj_mesh, p_err, scale); } else { size_t obj_face_ix = 0; // Assume that the indices are in the same order! for (size_t face_ix = 0; face_ix < mesh->num_faces; face_ix++) { ufbx_face obj_face = obj_mesh->faces[obj_face_ix]; ufbx_face face = mesh->faces.data[face_ix]; if (obj->bad_faces && ufbxt_face_has_duplicate_vertex(mesh, face)) { continue; } else if (obj->line_faces && face.num_indices == 2) { continue; } else if (obj->point_faces && face.num_indices == 1) { continue; } else if (!obj->bad_faces) { ufbxt_assert(obj_face.index_begin == face.index_begin); } ufbxt_assert(obj_face.num_indices == face.num_indices); obj_face_ix++; for (size_t i = 0; i < face.num_indices; i++) { size_t oix = obj_face.index_begin + i; size_t fix = face.index_begin + i; ufbx_vec3 op = ufbx_get_vertex_vec3(&obj_mesh->vertex_position, oix); ufbx_vec3 fp = ufbx_get_vertex_vec3(&mesh->skinned_position, fix); ufbx_vec3 on = check_normals ? ufbx_get_vertex_vec3(&obj_mesh->vertex_normal, oix) : ufbx_zero_vec3; ufbx_vec3 fn = check_normals ? ufbx_get_vertex_vec3(&mesh->skinned_normal, fix) : ufbx_zero_vec3; if (mesh->skinned_is_local) { fp = ufbx_transform_position(mat, fp); fn = ufbx_transform_direction(&norm_mat, fn); ufbx_real fn_len = (ufbx_real)sqrt(fn.x*fn.x + fn.y*fn.y + fn.z*fn.z); if (fn_len > 0.00001f) { fn.x /= fn_len; fn.y /= fn_len; fn.z /= fn_len; } ufbx_real on_len = (ufbx_real)sqrt(on.x*on.x + on.y*on.y + on.z*on.z); if (on_len > 0.00001f) { on.x /= on_len; on.y /= on_len; on.z /= on_len; } } if (scale != 1.0) { fp.x *= (ufbx_real)scale; fp.y *= (ufbx_real)scale; fp.z *= (ufbx_real)scale; op.x *= (ufbx_real)scale; op.y *= (ufbx_real)scale; op.z *= (ufbx_real)scale; } ufbxt_assert_close_vec3(p_err, op, fp); if (check_normals && !mesh->generated_normals) { ufbx_real on2 = on.x*on.x + on.y*on.y + on.z*on.z; ufbx_real fn2 = fn.x*fn.x + fn.y*fn.y + fn.z*fn.z; if (on2 > 0.01f && fn2 > 0.01f) { ufbxt_assert_close_vec3(p_err, on, fn); } } if (obj_mesh->vertex_uv.exists && !obj->bad_uvs) { ufbxt_assert(mesh->vertex_uv.exists); ufbx_vec2 ou = ufbx_get_vertex_vec2(&obj_mesh->vertex_uv, oix); ufbx_vec2 fu = ufbx_get_vertex_vec2(&mesh->vertex_uv, fix); ufbxt_assert_close_vec2(p_err, ou, fu); } } } } } free(obj_nodes); free(cat_name_data); free(used_nodes); } // -- IO static ufbxt_noinline size_t ufbxt_file_size(const char *name) { FILE *file = fopen(name, "rb"); if (!file) return 0; fseek(file, 0, SEEK_END); #if defined(_WIN32) size_t size = (size_t)_ftelli64(file); #else size_t size = (size_t)ftell(file); #endif fclose(file); return size; } static ufbxt_noinline void *ufbxt_read_file(const char *name, size_t *p_size) { FILE *file = fopen(name, "rb"); if (!file) return NULL; fseek(file, 0, SEEK_END); #if defined(_WIN32) size_t size = (size_t)_ftelli64(file); #else size_t size = (size_t)ftell(file); #endif fseek(file, 0, SEEK_SET); char *data = malloc(size + 1); ufbxt_assert(data != NULL); size_t num_read = fread(data, 1, size, file); fclose(file); data[size] = '\0'; if (num_read != size) { ufbxt_assert_fail(__FILE__, __LINE__, "Failed to load file"); } *p_size = size; return data; } static ufbxt_noinline void *ufbxt_read_file_ex(const char *name, size_t *p_size) { size_t name_len = strlen(name); if (name_len >= 3 && !memcmp(name + name_len - 3, ".gz", 3)) { size_t gz_size = 0; void *gz_data = ufbxt_read_file(name, &gz_size); if (!gz_data) return NULL; void *result = ufbxt_decompress_gzip(gz_data, gz_size, p_size); free(gz_data); return result; } else { return ufbxt_read_file(name, p_size); } } #endif