/* SPDX-License-Identifier: MIT */ /* * tools/konmodel.c * * Copyright (c) 2026 KonAki * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ /* * turns a glTF or glb file into a .kmdl file the engine can load * usage: konmodel in.glb out.kmdl [-t prefix] * textures inside a glb are saved next to the output, the material names them as prefix + file name */ #include #include #include #include #include "cgltf.h" #include "konmodel.h" /*** definition ***/ #define KON_CHUNK_VERTICES 65535 static const cgltf_data *data_; static const char *prefix_ = ""; static const char *outputPath_; static int warnings_; static void kon_warn_(const char *format, const char *a, int b) { fprintf(stderr, "konmodel: warning: "); fprintf(stderr, format, a, b); fputc('\n', stderr); warnings_++; } static int kon_fail_(const char *message) { fprintf(stderr, "konmodel: %s\n", message); return 0; } static int kon_die_(cgltf_data *data, const char *message) { kon_fail_(message); if (data) cgltf_free(data); return 1; } /*** math ***/ static void kon_copyName_(char *out, size_t size, const char *name, const char *fallback, int index) { if (!name || !name[0]) { snprintf(out, size, "%s%d", fallback, index); return; } if (strlen(name) >= size) kon_warn_("name '%s' is too long and was cut to %d characters", name, (int)size - 1); snprintf(out, size, "%s", name); } static void kon_decompose_(const float *m, kon_vec3_t *position, kon_vec4_t *rotation, kon_vec3_t *scale) { float sx = sqrtf(m[0] * m[0] + m[1] * m[1] + m[2] * m[2]); float sy = sqrtf(m[4] * m[4] + m[5] * m[5] + m[6] * m[6]); float sz = sqrtf(m[8] * m[8] + m[9] * m[9] + m[10] * m[10]); float det = m[0] * (m[5] * m[10] - m[9] * m[6]) - m[4] * (m[1] * m[10] - m[9] * m[2]) + m[8] * (m[1] * m[6] - m[5] * m[2]); float r[9], trace, s; if (det < 0.0f) sx = -sx; if (sx == 0.0f) sx = 1.0f; if (sy == 0.0f) sy = 1.0f; if (sz == 0.0f) sz = 1.0f; *position = KON_VEC3(m[12], m[13], m[14]); *scale = KON_VEC3(sx, sy, sz); r[0] = m[0] / sx; r[1] = m[1] / sx; r[2] = m[2] / sx; r[3] = m[4] / sy; r[4] = m[5] / sy; r[5] = m[6] / sy; r[6] = m[8] / sz; r[7] = m[9] / sz; r[8] = m[10] / sz; /* r holds the columns of the rotation, so r[3] is the second column's x */ trace = r[0] + r[4] + r[8]; if (trace > 0.0f) { s = sqrtf(trace + 1.0f) * 2.0f; *rotation = (kon_vec4_t){(r[5] - r[7]) / s, (r[6] - r[2]) / s, (r[1] - r[3]) / s, 0.25f * s}; } else if (r[0] > r[4] && r[0] > r[8]) { s = sqrtf(1.0f + r[0] - r[4] - r[8]) * 2.0f; *rotation = (kon_vec4_t){0.25f * s, (r[3] + r[1]) / s, (r[6] + r[2]) / s, (r[5] - r[7]) / s}; } else if (r[4] > r[8]) { s = sqrtf(1.0f + r[4] - r[0] - r[8]) * 2.0f; *rotation = (kon_vec4_t){(r[3] + r[1]) / s, 0.25f * s, (r[7] + r[5]) / s, (r[6] - r[2]) / s}; } else { s = sqrtf(1.0f + r[8] - r[0] - r[4]) * 2.0f; *rotation = (kon_vec4_t){(r[6] + r[2]) / s, (r[7] + r[5]) / s, 0.25f * s, (r[1] - r[3]) / s}; } } static int kon_isIdentity_(const float *m) { static const float identity[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}; int i; for (i = 0; i < 16; i++) if (fabsf(m[i] - identity[i]) > 1e-6f) return 0; return 1; } /*** materials ***/ static const char *kon_baseName_(const char *path) { const char *slash = strrchr(path, '/'), *back = strrchr(path, '\\'); if (back && (!slash || back > slash)) slash = back; return slash ? slash + 1 : path; } static int kon_writeFile_(const char *path, const void *bytes, size_t size) { FILE *f = fopen(path, "wb"); int ok; if (!f) return 0; ok = fwrite(bytes, 1, size, f) == size; return fclose(f) == 0 && ok; } static void kon_convertTexture_(kon_modelMaterial_t *out, const cgltf_texture *texture) { const cgltf_image *image = texture ? texture->image : NULL; char name[512]; if (!image) return; if (image->uri && strncmp(image->uri, "data:", 5) != 0) { size_t length = strlen(image->uri) + 1; char *uri = malloc(length); if (!uri) return; memcpy(uri, image->uri, length); cgltf_decode_uri(uri); snprintf(name, sizeof name, "%s%s", prefix_, uri); free(uri); } else if (image->buffer_view && image->buffer_view->buffer->data) { const char *extension = image->mime_type && strstr(image->mime_type, "jpeg") ? "jpg" : "png"; const char *base = kon_baseName_(outputPath_); char path[1024], stem[128], *dot; const cgltf_buffer_view *view = image->buffer_view; size_t length; snprintf(stem, sizeof stem, "%s", base); dot = strrchr(stem, '.'); if (dot) *dot = 0; snprintf(name, sizeof name, "%s%s_%d.%s", prefix_, stem, (int)cgltf_image_index(data_, image), extension); length = (size_t)(base - outputPath_); if (length + strlen(name) + 1 > sizeof path) return; memcpy(path, outputPath_, length); snprintf(path + length, sizeof path - length, "%s_%d.%s", stem, (int)cgltf_image_index(data_, image), extension); if (!kon_writeFile_(path, (const uint8_t *)view->buffer->data + view->offset, view->size)) { kon_warn_("could not write texture %s", path, 0); return; } } else { kon_warn_("a texture is stored as a data uri, which is not supported, it was left out%s", "", 0); return; } if (strlen(name) >= KON_MODEL_TEXTURE_SIZE) { kon_warn_("texture name '%s' is longer than %d characters, it was left out", name, KON_MODEL_TEXTURE_SIZE - 1); return; } strcpy(out->texture, name); /* 9728 is NEAREST in the glTF sampler */ out->filter = texture->sampler && texture->sampler->mag_filter == 9728 ? KON_FILTER_NEAREST : KON_FILTER_LINEAR; } static uint32_t kon_packColor_(const float *c) { uint32_t v[4]; int i; for (i = 0; i < 4; i++) { float f = c[i] < 0.0f ? 0.0f : c[i] > 1.0f ? 1.0f : c[i]; v[i] = (uint32_t)(f * 255.0f + 0.5f); } return v[3] << 24 | v[0] << 16 | v[1] << 8 | v[2]; } static int kon_convertMaterials_(kon_model_t *model) { size_t i; model->materialCount = (int)data_->materials_count + 1; model->materials = calloc((size_t)model->materialCount, sizeof *model->materials); if (!model->materials) return kon_fail_("out of memory"); for (i = 0; i < data_->materials_count; i++) { const cgltf_material *m = &data_->materials[i]; kon_modelMaterial_t *out = &model->materials[i]; out->color = 0xFFFFFFFF; if (m->has_pbr_metallic_roughness) { out->color = kon_packColor_(m->pbr_metallic_roughness.base_color_factor); kon_convertTexture_(out, m->pbr_metallic_roughness.base_color_texture.texture); } } model->materials[data_->materials_count].color = 0xFFFFFFFF; return 1; } /*** skeleton ***/ static int *boneOfNode_; static int boneBase_; static int kon_emitBone_(kon_model_t *model, const cgltf_skin *skin, const cgltf_node *node, int *next) { size_t index = cgltf_node_index(data_, node); kon_modelBone_t *bone; if (boneOfNode_[index] >= 0) return 1; if (node->parent && boneOfNode_[cgltf_node_index(data_, node->parent)] == -1) { size_t k; for (k = 0; k < skin->joints_count; k++) { if (skin->joints[k] == node->parent) { if (!kon_emitBone_(model, skin, node->parent, next)) return 0; break; } } } bone = &model->bones[*next]; boneOfNode_[index] = *next; kon_copyName_(bone->name, KON_MODEL_NAME_SIZE, node->name, "bone", *next); bone->inverseBind = kon_mat4Identity(); bone->rotation = (kon_vec4_t){0, 0, 0, 1}; bone->scale = KON_VEC3(1, 1, 1); if (node->has_matrix) { kon_decompose_(node->matrix, &bone->position, &bone->rotation, &bone->scale); } else { if (node->has_translation) bone->position = KON_VEC3(node->translation[0], node->translation[1], node->translation[2]); if (node->has_rotation) bone->rotation = (kon_vec4_t){node->rotation[0], node->rotation[1], node->rotation[2], node->rotation[3]}; if (node->has_scale) bone->scale = KON_VEC3(node->scale[0], node->scale[1], node->scale[2]); } bone->parent = -2; (*next)++; return 1; } static int kon_convertSkeleton_(kon_model_t *model) { const cgltf_skin *skin; size_t i, nodeCount = data_->nodes_count; int next, extra = 0; const cgltf_node *armature = NULL; boneOfNode_ = malloc((nodeCount ? nodeCount : 1) * sizeof *boneOfNode_); if (!boneOfNode_) return kon_fail_("out of memory"); for (i = 0; i < nodeCount; i++) boneOfNode_[i] = -1; if (data_->skins_count == 0) return 1; if (data_->skins_count > 1) kon_warn_("the file has %s skins, only the first one is used", "more than one", 0); skin = &data_->skins[0]; for (i = 0; i < skin->joints_count; i++) { const cgltf_node *parent = skin->joints[i]->parent; size_t k; int isJoint = 0; for (k = 0; parent && k < skin->joints_count; k++) if (skin->joints[k] == parent) isJoint = 1; if (!isJoint && parent && !armature) armature = parent; } if (armature) { float world[16]; cgltf_node_transform_world(armature, world); if (!kon_isIdentity_(world)) extra = 1; } if ((int)skin->joints_count + extra > KON_MODEL_MAX_BONES) return kon_fail_("too many bones, the limit is 128"); model->boneCount = (int)skin->joints_count + extra; model->bones = calloc((size_t)model->boneCount, sizeof *model->bones); if (!model->bones) return kon_fail_("out of memory"); next = 0; if (extra) { float world[16]; kon_modelBone_t *root = &model->bones[0]; snprintf(root->name, KON_MODEL_NAME_SIZE, "armature"); root->parent = -1; root->inverseBind = kon_mat4Identity(); cgltf_node_transform_world(armature, world); kon_decompose_(world, &root->position, &root->rotation, &root->scale); next = 1; } boneBase_ = extra; for (i = 0; i < skin->joints_count; i++) { if (!kon_emitBone_(model, skin, skin->joints[i], &next)) return 0; } for (i = 0; i < skin->joints_count; i++) { const cgltf_node *node = skin->joints[i]; kon_modelBone_t *bone = &model->bones[boneOfNode_[cgltf_node_index(data_, node)]]; int parent = node->parent ? boneOfNode_[cgltf_node_index(data_, node->parent)] : -1; bone->parent = parent >= 0 ? parent : (extra ? 0 : -1); if (skin->inverse_bind_matrices) { float m[16]; if (cgltf_accessor_read_float(skin->inverse_bind_matrices, i, m, 16)) memcpy(bone->inverseBind.m, m, sizeof m); } } return 1; } /*** meshes ***/ typedef struct kon_source { size_t count; float *position, *normal, *uv, *weights; uint8_t *joints; int skinned; } kon_source_t; static float *kon_unpack_(const cgltf_accessor *accessor, size_t components, size_t count) { float *out; if (accessor->count != count) return NULL; out = malloc(count * components * sizeof *out); if (!out) return NULL; if (cgltf_accessor_unpack_floats(accessor, out, count * components) != count * components) { free(out); return NULL; } return out; } static void kon_freeSource_(kon_source_t *s) { free(s->position); free(s->normal); free(s->uv); free(s->weights); free(s->joints); memset(s, 0, sizeof *s); } static void kon_computeNormals_(kon_source_t *s, const size_t *tris, size_t triCount) { size_t i; s->normal = calloc(s->count * 3, sizeof(float)); if (!s->normal) return; for (i = 0; i < triCount; i++) { const float *a = &s->position[tris[i * 3] * 3], *b = &s->position[tris[i * 3 + 1] * 3], *c = &s->position[tris[i * 3 + 2] * 3]; kon_vec3_t n = kon_vec3Cross(KON_VEC3(b[0] - a[0], b[1] - a[1], b[2] - a[2]), KON_VEC3(c[0] - a[0], c[1] - a[1], c[2] - a[2])); int k; for (k = 0; k < 3; k++) { float *dst = &s->normal[tris[i * 3 + k] * 3]; dst[0] += n.x; dst[1] += n.y; dst[2] += n.z; } } for (i = 0; i < s->count; i++) { kon_vec3_t n = kon_vec3Normalize(KON_VEC3(s->normal[i * 3], s->normal[i * 3 + 1], s->normal[i * 3 + 2])); s->normal[i * 3] = n.x; s->normal[i * 3 + 1] = n.y; s->normal[i * 3 + 2] = n.z; } } /* turns strips and fans into plain triangles. returns a malloc'd list of 3 vertex numbers per triangle */ static size_t *kon_triangles_(const cgltf_primitive *prim, size_t vertexCount, size_t *triCount) { size_t n = prim->indices ? prim->indices->count : vertexCount, i, out = 0; size_t *idx = malloc((n ? n : 1) * sizeof *idx), *tris; if (!idx) return NULL; for (i = 0; i < n; i++) idx[i] = prim->indices ? cgltf_accessor_read_index(prim->indices, i) : i; for (i = 0; i < n; i++) if (idx[i] >= vertexCount) { free(idx); return NULL; } if (prim->type == cgltf_primitive_type_triangles) { *triCount = n / 3; return idx; } tris = malloc((n ? n : 1) * 3 * sizeof *tris); if (!tris) { free(idx); return NULL; } for (i = 2; i < n; i++) { size_t a, b, c = idx[i]; if (prim->type == cgltf_primitive_type_triangle_strip) { a = idx[i - 2]; b = idx[i - 1]; if (i & 1) { size_t t = a; a = b; b = t; } } else { a = idx[0]; b = idx[i - 1]; } if (a == b || b == c || a == c) continue; tris[out * 3] = a; tris[out * 3 + 1] = b; tris[out * 3 + 2] = c; out++; } free(idx); *triCount = out; return tris; } static int kon_loadSource_(kon_source_t *s, const cgltf_primitive *prim, const size_t *tris, size_t triCount, int skinned) { const cgltf_accessor *position = NULL, *normal = NULL, *uv = NULL, *joints = NULL, *weights = NULL; size_t i; for (i = 0; i < prim->attributes_count; i++) { const cgltf_attribute *a = &prim->attributes[i]; if (a->index != 0) continue; if (a->type == cgltf_attribute_type_position) position = a->data; else if (a->type == cgltf_attribute_type_normal) normal = a->data; else if (a->type == cgltf_attribute_type_texcoord) uv = a->data; else if (a->type == cgltf_attribute_type_joints) joints = a->data; else if (a->type == cgltf_attribute_type_weights) weights = a->data; } if (!position) return 0; s->count = position->count; s->position = kon_unpack_(position, 3, s->count); if (!s->position) return 0; if (normal) s->normal = kon_unpack_(normal, 3, s->count); if (!s->normal) kon_computeNormals_(s, tris, triCount); if (uv) s->uv = kon_unpack_(uv, 2, s->count); if (skinned && joints && weights) { s->weights = kon_unpack_(weights, 4, s->count); s->joints = malloc(s->count * 4); if (!s->weights || !s->joints) return 0; for (i = 0; i < s->count; i++) { cgltf_uint j[4]; int k; if (!cgltf_accessor_read_uint(joints, i, j, 4)) return 0; for (k = 0; k < 4; k++) s->joints[i * 4 + k] = (uint8_t)j[k]; } s->skinned = 1; } return s->normal != NULL; } static kon_vec3_t kon_transformNormal_(const float *m, kon_vec3_t n, float det) { kon_vec3_t c0 = KON_VEC3(m[0], m[1], m[2]), c1 = KON_VEC3(m[4], m[5], m[6]), c2 = KON_VEC3(m[8], m[9], m[10]); float mirror = det < 0.0f ? -1.0f : 1.0f; kon_vec3_t r = kon_vec3Add(kon_vec3Add(kon_vec3Scale(kon_vec3Cross(c1, c2), n.x * mirror), kon_vec3Scale(kon_vec3Cross(c2, c0), n.y * mirror)), kon_vec3Scale(kon_vec3Cross(c0, c1), n.z * mirror)); return kon_vec3Normalize(r); } static int kon_addMesh_(kon_model_t *model, int *capacity, kon_modelMesh_t *mesh) { if (model->meshCount == *capacity) { int grown = *capacity ? *capacity * 2 : 8; kon_modelMesh_t *meshes = realloc(model->meshes, (size_t)grown * sizeof *meshes); if (!meshes) return 0; model->meshes = meshes; *capacity = grown; } model->meshes[model->meshCount++] = *mesh; return 1; } /* writes the triangles that use only the vertices handed out so far, then starts over */ static int kon_convertPrimitive_(kon_model_t *model, int *capacity, const cgltf_node *node, const cgltf_primitive *prim, int skinned) { kon_source_t s; size_t *tris = NULL, triCount = 0, t; int *remap = NULL, ok = 0; size_t *used = NULL, usedCount = 0; uint16_t *indices = NULL; size_t chunkTris = 0; float world[16], det = 1.0f; int transform = !skinned && node->mesh, flip = 0; memset(&s, 0, sizeof s); if (prim->type != cgltf_primitive_type_triangles && prim->type != cgltf_primitive_type_triangle_strip && prim->type != cgltf_primitive_type_triangle_fan) { kon_warn_("a primitive in '%s' is not made of triangles and was left out", node->name ? node->name : "mesh", 0); return 1; } tris = kon_triangles_(prim, prim->attributes_count ? prim->attributes[0].data->count : 0, &triCount); if (!tris) return kon_fail_("a mesh has bad indices"); if (!kon_loadSource_(&s, prim, tris, triCount, skinned)) { free(tris); kon_freeSource_(&s); return kon_fail_("a mesh is missing its positions or has bad attributes"); } for (t = 0; t < triCount * 3; t++) if (tris[t] >= s.count) { free(tris); kon_freeSource_(&s); return kon_fail_("a mesh has bad indices"); } if (transform) { cgltf_node_transform_world(node, world); det = world[0] * (world[5] * world[10] - world[9] * world[6]) - world[4] * (world[1] * world[10] - world[9] * world[2]) + world[8] * (world[1] * world[6] - world[5] * world[2]); flip = det < 0.0f; } remap = malloc(s.count * sizeof *remap); used = malloc(s.count * sizeof *used); indices = malloc((triCount ? triCount : 1) * 3 * sizeof *indices); if (!remap || !used || !indices) goto done; for (t = 0; t < s.count; t++) remap[t] = -1; t = 0; while (t < triCount) { kon_modelMesh_t mesh; size_t start = t, i; memset(&mesh, 0, sizeof mesh); usedCount = 0; chunkTris = 0; for (; t < triCount; t++) { size_t fresh = 0; int k; for (k = 0; k < 3; k++) if (remap[tris[t * 3 + k]] < 0) fresh++; /* a vertex can repeat inside one triangle, so be careful and count at most 3 new ones */ if (usedCount + fresh > KON_CHUNK_VERTICES) break; for (k = 0; k < 3; k++) { size_t v = tris[t * 3 + k]; if (remap[v] < 0) { remap[v] = (int)usedCount; used[usedCount++] = v; } indices[chunkTris * 3 + (size_t)(flip ? 2 - k : k)] = (uint16_t)remap[v]; } chunkTris++; } (void)start; mesh.material = prim->material ? (int)cgltf_material_index(data_, prim->material) : (int)data_->materials_count; mesh.vertexCount = (int)usedCount; mesh.triangleCount = (int)chunkTris; mesh.vertices = calloc(usedCount, sizeof *mesh.vertices); mesh.indices = malloc(chunkTris * 3 * sizeof *mesh.indices); if (s.skinned) { mesh.joints = malloc(usedCount * 4); mesh.weights = malloc(usedCount * 4 * sizeof *mesh.weights); } if (!mesh.vertices || !mesh.indices || (s.skinned && (!mesh.joints || !mesh.weights))) { free(mesh.vertices); free(mesh.indices); free(mesh.joints); free(mesh.weights); goto done; } memcpy(mesh.indices, indices, chunkTris * 3 * sizeof *mesh.indices); for (i = 0; i < usedCount; i++) { size_t v = used[i]; kon_vertex_t *out = &mesh.vertices[i]; kon_vec3_t p = KON_VEC3(s.position[v * 3], s.position[v * 3 + 1], s.position[v * 3 + 2]); kon_vec3_t n = KON_VEC3(s.normal[v * 3], s.normal[v * 3 + 1], s.normal[v * 3 + 2]); if (transform) { kon_mat4_t m; memcpy(m.m, world, sizeof world); p = kon_mat4MulPoint(m, p); n = kon_transformNormal_(world, n, det); } out->position = p; out->normal = n; if (s.uv) out->uv = (kon_vec2_t){s.uv[v * 2], s.uv[v * 2 + 1]}; if (s.skinned) { float sum = 0.0f; int k; for (k = 0; k < 4; k++) sum += s.weights[v * 4 + k]; for (k = 0; k < 4; k++) { mesh.joints[i * 4 + (size_t)k] = s.joints[v * 4 + (size_t)k]; mesh.weights[i * 4 + (size_t)k] = sum > 0.0f ? s.weights[v * 4 + (size_t)k] / sum : (k == 0 ? 1.0f : 0.0f); } } } if (!kon_addMesh_(model, capacity, &mesh)) { free(mesh.vertices); free(mesh.indices); free(mesh.joints); free(mesh.weights); goto done; } for (i = 0; i < usedCount; i++) remap[used[i]] = -1; } ok = 1; done: free(tris); free(remap); free(used); free(indices); kon_freeSource_(&s); return ok ? 1 : kon_fail_("out of memory"); } /* skinned joints are given as positions in the skin, change them to bone numbers */ static void kon_remapJoints_(kon_model_t *model, int firstMesh, const cgltf_skin *skin) { int m; size_t j; uint8_t map[256]; for (j = 0; j < 256; j++) map[j] = 0; for (j = 0; j < skin->joints_count && j < 256; j++) map[j] = (uint8_t)boneOfNode_[cgltf_node_index(data_, skin->joints[j])]; for (m = firstMesh; m < model->meshCount; m++) { kon_modelMesh_t *mesh = &model->meshes[m]; int i; if (!mesh->joints) continue; for (i = 0; i < mesh->vertexCount * 4; i++) mesh->joints[i] = map[mesh->joints[i]]; } } static int kon_convertMeshes_(kon_model_t *model) { int capacity = 0; size_t n, p; for (n = 0; n < data_->nodes_count; n++) { const cgltf_node *node = &data_->nodes[n]; int skinned, first = model->meshCount; if (!node->mesh) continue; skinned = node->skin && node->skin == &data_->skins[0]; if (node->skin && !skinned) kon_warn_("mesh '%s' uses another skin than the first one and is drawn without animation", node->name ? node->name : "mesh", 0); for (p = 0; p < node->mesh->primitives_count; p++) { if (!kon_convertPrimitive_(model, &capacity, node, &node->mesh->primitives[p], skinned)) return 0; } if (skinned) kon_remapJoints_(model, first, node->skin); } return 1; } /*** animations ***/ static int kon_convertChannel_(kon_modelChannel_t *out, const cgltf_animation_channel *in, float *duration) { const cgltf_animation_sampler *sampler = in->sampler; int path, width, cubic; size_t keys, i; float *values; int bone; if (!in->target_node || !sampler || !sampler->input || !sampler->output) return 0; bone = boneOfNode_[cgltf_node_index(data_, in->target_node)]; if (bone < 0) return 0; if (in->target_path == cgltf_animation_path_type_translation) path = KON_MODEL_POSITION; else if (in->target_path == cgltf_animation_path_type_rotation) path = KON_MODEL_ROTATION; else if (in->target_path == cgltf_animation_path_type_scale) path = KON_MODEL_SCALE; else return 0; width = path == KON_MODEL_ROTATION ? 4 : 3; cubic = sampler->interpolation == cgltf_interpolation_type_cubic_spline; keys = sampler->input->count; if (keys == 0 || sampler->output->count != keys * (cubic ? 3 : 1)) return 0; out->times = kon_unpack_(sampler->input, 1, keys); values = kon_unpack_(sampler->output, (size_t)width, sampler->output->count); if (!out->times || !values) { free(out->times); free(values); out->times = NULL; return 0; } for (i = 1; i < keys; i++) if (out->times[i] < out->times[i - 1]) { free(out->times); free(values); out->times = NULL; return 0; } out->values = malloc(keys * (size_t)width * sizeof(float)); if (!out->values) { free(out->times); free(values); out->times = NULL; return 0; } /* a cubic spline key is in tangent, value, out tangent, only the value is kept */ for (i = 0; i < keys; i++) memcpy(&out->values[i * (size_t)width], &values[(cubic ? i * 3 + 1 : i) * (size_t)width], (size_t)width * sizeof(float)); free(values); out->bone = bone; out->path = path; out->step = sampler->interpolation == cgltf_interpolation_type_step; out->keyCount = (int)keys; if (out->times[keys - 1] > *duration) *duration = out->times[keys - 1]; if (cubic) kon_warn_("a cubic spline animation in '%s' was turned into a linear one", in->target_node->name ? in->target_node->name : "bone", 0); return 1; } static int kon_convertAnimations_(kon_model_t *model) { size_t a, c; if (data_->animations_count == 0) return 1; if (model->boneCount == 0) { kon_warn_("the file has animations but no skin, they were left out%s", "", 0); return 1; } model->clips = calloc(data_->animations_count, sizeof *model->clips); if (!model->clips) return kon_fail_("out of memory"); model->clipCount = (int)data_->animations_count; for (a = 0; a < data_->animations_count; a++) { const cgltf_animation *in = &data_->animations[a]; kon_modelClip_t *clip = &model->clips[a]; int skipped = 0; kon_copyName_(clip->name, KON_MODEL_NAME_SIZE, in->name, "clip", (int)a); clip->channels = calloc(in->channels_count ? in->channels_count : 1, sizeof *clip->channels); if (!clip->channels) return kon_fail_("out of memory"); for (c = 0; c < in->channels_count; c++) { if (kon_convertChannel_(&clip->channels[clip->channelCount], &in->channels[c], &clip->duration)) clip->channelCount++; else skipped++; } if (skipped) kon_warn_("%s: some channels don't move a bone or aren't supported and were left out (%d)", clip->name, skipped); } return 1; } /*** main ***/ int main(int argc, char **argv) { cgltf_options options; cgltf_data *data = NULL; kon_model_t *model; uint8_t *bytes; size_t size, i; long vertices = 0, triangles = 0; const char *input = NULL; int a; memset(&options, 0, sizeof options); model = calloc(1, sizeof *model); if (!model) return 1; for (a = 1; a < argc; a++) { if (strcmp(argv[a], "-t") == 0 && a + 1 < argc) prefix_ = argv[++a]; else if (!input) input = argv[a]; else if (!outputPath_) outputPath_ = argv[a]; else input = NULL; } if (!input || !outputPath_) { fprintf(stderr, "usage: konmodel in.glb out.kmdl [-t texture-prefix]\n"); return 2; } if (cgltf_parse_file(&options, input, &data) != cgltf_result_success) return kon_die_(NULL, "could not read the file, is it a valid glTF?"); if (cgltf_load_buffers(&options, data, input) != cgltf_result_success) return kon_die_(data, "could not load the buffers, is the .bin next to the file?"); if (cgltf_validate(data) != cgltf_result_success) return kon_die_(data, "the file is not valid glTF"); data_ = data; if (!kon_convertMaterials_(model) || !kon_convertSkeleton_(model) || !kon_convertMeshes_(model) || !kon_convertAnimations_(model)) { cgltf_free(data); return 1; } bytes = kon_modelSerialize(model, &size); if (!bytes || !kon_writeFile_(outputPath_, bytes, size)) { return kon_die_(data, "could not write the output file"); } for (i = 0; i < (size_t)model->meshCount; i++) { vertices += model->meshes[i].vertexCount; triangles += model->meshes[i].triangleCount; } printf("%s: %d meshes, %ld vertices, %ld triangles, %d bones, %d clips, %lu bytes\n", outputPath_, model->meshCount, vertices, triangles, model->boneCount, model->clipCount, (unsigned long)size); free(bytes); kon_modelFree(model); cgltf_free(data); return 0; }