#include "cell.h" #include #include #include "ufbx.h" static JSValue make_float_array(JSContext *js, const double *arr, int count) { JS_FRAME(js); JS_ROOT(a, JS_NewArray(js)); for (int i = 0; i < count; i++) JS_SetPropertyNumber(js, a.val, i, JS_NewFloat64(js, arr[i])); JS_RETURN(a.val); } static JSValue make_float_array_f(JSContext *js, const float *arr, int count) { JS_FRAME(js); JS_ROOT(a, JS_NewArray(js)); for (int i = 0; i < count; i++) JS_SetPropertyNumber(js, a.val, i, JS_NewFloat64(js, arr[i])); JS_RETURN(a.val); } JSValue js_fbx_decode(JSContext *js, JSValue this_val, int argc, JSValueConst *argv) { size_t len; void *raw = js_get_blob_data(js, &len, argv[0]); if (raw == NULL) return JS_EXCEPTION; ufbx_load_opts opts = {0}; opts.generate_missing_normals = true; opts.target_axes = ufbx_axes_right_handed_y_up; opts.target_unit_meters = 1.0; ufbx_error error; ufbx_scene *scene = ufbx_load_memory(raw, len, &opts, &error); if (!scene) return JS_ThrowReferenceError(js, "failed to parse FBX: %s", error.description.data); JS_FRAME(js); JS_ROOT(obj, JS_NewObject(js)); // Count total vertices and indices across all meshes size_t total_vertices = 0; size_t total_indices = 0; int global_has_normals = 0; int global_has_uvs = 0; for (size_t mi = 0; mi < scene->meshes.count; mi++) { ufbx_mesh *mesh = scene->meshes.data[mi]; total_vertices += mesh->num_indices; total_indices += mesh->num_triangles * 3; if (mesh->vertex_normal.exists) global_has_normals = 1; if (mesh->vertex_uv.exists) global_has_uvs = 1; } int use_32bit = (total_vertices > 65535); // Calculate buffer layout size_t pos_size = total_vertices * 3 * sizeof(float); size_t norm_size = global_has_normals ? total_vertices * 3 * sizeof(float) : 0; size_t uv_size = global_has_uvs ? total_vertices * 2 * sizeof(float) : 0; size_t idx_size = total_indices * (use_32bit ? sizeof(uint32_t) : sizeof(uint16_t)); size_t total_buffer_size = pos_size + norm_size + uv_size + idx_size; uint8_t *buffer_data = malloc(total_buffer_size); float *positions = (float *)buffer_data; float *normals = global_has_normals ? (float *)(buffer_data + pos_size) : NULL; float *uvs = global_has_uvs ? (float *)(buffer_data + pos_size + norm_size) : NULL; void *indices = buffer_data + pos_size + norm_size + uv_size; // Fill vertex data and track per-mesh offsets size_t vertex_offset = 0; size_t index_offset = 0; // Store mesh info for later typedef struct { size_t vertex_start; size_t vertex_count; size_t index_start; size_t index_count; } mesh_info_t; mesh_info_t *mesh_infos = malloc(scene->meshes.count * sizeof(mesh_info_t)); for (size_t mi = 0; mi < scene->meshes.count; mi++) { ufbx_mesh *mesh = scene->meshes.data[mi]; mesh_infos[mi].vertex_start = vertex_offset; mesh_infos[mi].vertex_count = mesh->num_indices; mesh_infos[mi].index_start = index_offset; // Copy vertex data (unindexed - one vertex per index) for (size_t i = 0; i < mesh->num_indices; i++) { ufbx_vec3 pos = ufbx_get_vertex_vec3(&mesh->vertex_position, i); positions[(vertex_offset + i) * 3 + 0] = (float)pos.x; positions[(vertex_offset + i) * 3 + 1] = (float)pos.y; positions[(vertex_offset + i) * 3 + 2] = (float)pos.z; if (normals && mesh->vertex_normal.exists) { ufbx_vec3 norm = ufbx_get_vertex_vec3(&mesh->vertex_normal, i); normals[(vertex_offset + i) * 3 + 0] = (float)norm.x; normals[(vertex_offset + i) * 3 + 1] = (float)norm.y; normals[(vertex_offset + i) * 3 + 2] = (float)norm.z; } else if (normals) { normals[(vertex_offset + i) * 3 + 0] = 0; normals[(vertex_offset + i) * 3 + 1] = 1; normals[(vertex_offset + i) * 3 + 2] = 0; } if (uvs && mesh->vertex_uv.exists) { ufbx_vec2 uv = ufbx_get_vertex_vec2(&mesh->vertex_uv, i); uvs[(vertex_offset + i) * 2 + 0] = (float)uv.x; uvs[(vertex_offset + i) * 2 + 1] = (float)uv.y; } else if (uvs) { uvs[(vertex_offset + i) * 2 + 0] = 0; uvs[(vertex_offset + i) * 2 + 1] = 0; } } // Triangulate and create indices size_t mesh_index_count = 0; for (size_t fi = 0; fi < mesh->num_faces; fi++) { ufbx_face face = mesh->faces.data[fi]; // Triangulate the face for (uint32_t ti = 0; ti < face.num_indices - 2; ti++) { size_t idx = index_offset + mesh_index_count; if (use_32bit) { ((uint32_t *)indices)[idx + 0] = (uint32_t)(vertex_offset + face.index_begin); ((uint32_t *)indices)[idx + 1] = (uint32_t)(vertex_offset + face.index_begin + ti + 1); ((uint32_t *)indices)[idx + 2] = (uint32_t)(vertex_offset + face.index_begin + ti + 2); } else { ((uint16_t *)indices)[idx + 0] = (uint16_t)(vertex_offset + face.index_begin); ((uint16_t *)indices)[idx + 1] = (uint16_t)(vertex_offset + face.index_begin + ti + 1); ((uint16_t *)indices)[idx + 2] = (uint16_t)(vertex_offset + face.index_begin + ti + 2); } mesh_index_count += 3; } } mesh_infos[mi].index_count = mesh_index_count; vertex_offset += mesh->num_indices; index_offset += mesh_index_count; } // Create buffer JSValue tmp; JS_ROOT(buffers_arr, JS_NewArray(js)); { JS_ROOT(buf, JS_NewObject(js)); tmp = js_new_blob_stoned_copy(js, buffer_data, total_buffer_size); JS_SetPropertyStr(js, buf.val, "blob", tmp); JS_SetPropertyStr(js, buf.val, "byte_length", JS_NewInt64(js, total_buffer_size)); JS_SetPropertyNumber(js, buffers_arr.val, 0, buf.val); } JS_SetPropertyStr(js, obj.val, "buffers", buffers_arr.val); // Create views JS_ROOT(views_arr, JS_NewArray(js)); int view_idx = 0; { JS_ROOT(pos_view, JS_NewObject(js)); JS_SetPropertyStr(js, pos_view.val, "buffer", JS_NewInt32(js, 0)); JS_SetPropertyStr(js, pos_view.val, "byte_offset", JS_NewInt64(js, 0)); JS_SetPropertyStr(js, pos_view.val, "byte_length", JS_NewInt64(js, pos_size)); JS_SetPropertyStr(js, pos_view.val, "byte_stride", JS_NULL); tmp = JS_NewString(js, "vertex"); JS_SetPropertyStr(js, pos_view.val, "usage", tmp); JS_SetPropertyNumber(js, views_arr.val, view_idx++, pos_view.val); } int pos_view_idx = 0; int norm_view_idx = -1; if (global_has_normals) { JS_ROOT(norm_view, JS_NewObject(js)); JS_SetPropertyStr(js, norm_view.val, "buffer", JS_NewInt32(js, 0)); JS_SetPropertyStr(js, norm_view.val, "byte_offset", JS_NewInt64(js, pos_size)); JS_SetPropertyStr(js, norm_view.val, "byte_length", JS_NewInt64(js, norm_size)); JS_SetPropertyStr(js, norm_view.val, "byte_stride", JS_NULL); tmp = JS_NewString(js, "vertex"); JS_SetPropertyStr(js, norm_view.val, "usage", tmp); norm_view_idx = view_idx; JS_SetPropertyNumber(js, views_arr.val, view_idx++, norm_view.val); } int uv_view_idx = -1; if (global_has_uvs) { JS_ROOT(uv_view, JS_NewObject(js)); JS_SetPropertyStr(js, uv_view.val, "buffer", JS_NewInt32(js, 0)); JS_SetPropertyStr(js, uv_view.val, "byte_offset", JS_NewInt64(js, pos_size + norm_size)); JS_SetPropertyStr(js, uv_view.val, "byte_length", JS_NewInt64(js, uv_size)); JS_SetPropertyStr(js, uv_view.val, "byte_stride", JS_NULL); tmp = JS_NewString(js, "vertex"); JS_SetPropertyStr(js, uv_view.val, "usage", tmp); uv_view_idx = view_idx; JS_SetPropertyNumber(js, views_arr.val, view_idx++, uv_view.val); } { JS_ROOT(idx_view, JS_NewObject(js)); JS_SetPropertyStr(js, idx_view.val, "buffer", JS_NewInt32(js, 0)); JS_SetPropertyStr(js, idx_view.val, "byte_offset", JS_NewInt64(js, pos_size + norm_size + uv_size)); JS_SetPropertyStr(js, idx_view.val, "byte_length", JS_NewInt64(js, idx_size)); JS_SetPropertyStr(js, idx_view.val, "byte_stride", JS_NULL); tmp = JS_NewString(js, "index"); JS_SetPropertyStr(js, idx_view.val, "usage", tmp); int idx_view_idx_tmp = view_idx; JS_SetPropertyNumber(js, views_arr.val, view_idx++, idx_view.val); } int idx_view_idx = view_idx - 1; JS_SetPropertyStr(js, obj.val, "views", views_arr.val); // Create accessors per mesh JS_ROOT(accessors_arr, JS_NewArray(js)); int acc_idx = 0; JS_ROOT(meshes_arr, JS_NewArray(js)); for (size_t mi = 0; mi < scene->meshes.count; mi++) { ufbx_mesh *mesh = scene->meshes.data[mi]; mesh_info_t *info = &mesh_infos[mi]; // Position accessor int mesh_pos_acc = acc_idx; { JS_ROOT(spa, JS_NewObject(js)); JS_SetPropertyStr(js, spa.val, "view", JS_NewInt32(js, pos_view_idx)); JS_SetPropertyStr(js, spa.val, "byte_offset", JS_NewInt64(js, info->vertex_start * 3 * sizeof(float))); JS_SetPropertyStr(js, spa.val, "count", JS_NewInt64(js, info->vertex_count)); tmp = JS_NewString(js, "f32"); JS_SetPropertyStr(js, spa.val, "component_type", tmp); tmp = JS_NewString(js, "vec3"); JS_SetPropertyStr(js, spa.val, "type", tmp); JS_SetPropertyStr(js, spa.val, "normalized", JS_FALSE); JS_SetPropertyStr(js, spa.val, "min", JS_NULL); JS_SetPropertyStr(js, spa.val, "max", JS_NULL); JS_SetPropertyNumber(js, accessors_arr.val, acc_idx++, spa.val); } int mesh_norm_acc = -1; if (global_has_normals) { mesh_norm_acc = acc_idx; JS_ROOT(sna, JS_NewObject(js)); JS_SetPropertyStr(js, sna.val, "view", JS_NewInt32(js, norm_view_idx)); JS_SetPropertyStr(js, sna.val, "byte_offset", JS_NewInt64(js, info->vertex_start * 3 * sizeof(float))); JS_SetPropertyStr(js, sna.val, "count", JS_NewInt64(js, info->vertex_count)); tmp = JS_NewString(js, "f32"); JS_SetPropertyStr(js, sna.val, "component_type", tmp); tmp = JS_NewString(js, "vec3"); JS_SetPropertyStr(js, sna.val, "type", tmp); JS_SetPropertyStr(js, sna.val, "normalized", JS_FALSE); JS_SetPropertyStr(js, sna.val, "min", JS_NULL); JS_SetPropertyStr(js, sna.val, "max", JS_NULL); JS_SetPropertyNumber(js, accessors_arr.val, acc_idx++, sna.val); } int mesh_uv_acc = -1; if (global_has_uvs) { mesh_uv_acc = acc_idx; JS_ROOT(sua, JS_NewObject(js)); JS_SetPropertyStr(js, sua.val, "view", JS_NewInt32(js, uv_view_idx)); JS_SetPropertyStr(js, sua.val, "byte_offset", JS_NewInt64(js, info->vertex_start * 2 * sizeof(float))); JS_SetPropertyStr(js, sua.val, "count", JS_NewInt64(js, info->vertex_count)); tmp = JS_NewString(js, "f32"); JS_SetPropertyStr(js, sua.val, "component_type", tmp); tmp = JS_NewString(js, "vec2"); JS_SetPropertyStr(js, sua.val, "type", tmp); JS_SetPropertyStr(js, sua.val, "normalized", JS_FALSE); JS_SetPropertyStr(js, sua.val, "min", JS_NULL); JS_SetPropertyStr(js, sua.val, "max", JS_NULL); JS_SetPropertyNumber(js, accessors_arr.val, acc_idx++, sua.val); } int mesh_idx_acc = acc_idx; { JS_ROOT(sia, JS_NewObject(js)); JS_SetPropertyStr(js, sia.val, "view", JS_NewInt32(js, idx_view_idx)); JS_SetPropertyStr(js, sia.val, "byte_offset", JS_NewInt64(js, info->index_start * (use_32bit ? sizeof(uint32_t) : sizeof(uint16_t)))); JS_SetPropertyStr(js, sia.val, "count", JS_NewInt64(js, info->index_count)); tmp = JS_NewString(js, use_32bit ? "u32" : "u16"); JS_SetPropertyStr(js, sia.val, "component_type", tmp); tmp = JS_NewString(js, "scalar"); JS_SetPropertyStr(js, sia.val, "type", tmp); JS_SetPropertyStr(js, sia.val, "normalized", JS_FALSE); JS_SetPropertyStr(js, sia.val, "min", JS_NULL); JS_SetPropertyStr(js, sia.val, "max", JS_NULL); JS_SetPropertyNumber(js, accessors_arr.val, acc_idx++, sia.val); } // Create mesh JS_ROOT(m, JS_NewObject(js)); tmp = mesh->element.name.length > 0 ? JS_NewString(js, mesh->element.name.data) : JS_NULL; JS_SetPropertyStr(js, m.val, "name", tmp); JS_ROOT(prims_arr, JS_NewArray(js)); { JS_ROOT(prim, JS_NewObject(js)); tmp = JS_NewString(js, "triangles"); JS_SetPropertyStr(js, prim.val, "topology", tmp); JS_ROOT(attrs, JS_NewObject(js)); JS_SetPropertyStr(js, attrs.val, "POSITION", JS_NewInt32(js, mesh_pos_acc)); if (mesh_norm_acc >= 0) JS_SetPropertyStr(js, attrs.val, "NORMAL", JS_NewInt32(js, mesh_norm_acc)); if (mesh_uv_acc >= 0) JS_SetPropertyStr(js, attrs.val, "TEXCOORD_0", JS_NewInt32(js, mesh_uv_acc)); JS_SetPropertyStr(js, prim.val, "attributes", attrs.val); JS_SetPropertyStr(js, prim.val, "indices", JS_NewInt32(js, mesh_idx_acc)); JS_SetPropertyStr(js, prim.val, "material", mesh->materials.count > 0 ? JS_NewInt32(js, mesh->materials.data[0]->typed_id) : JS_NULL); JS_SetPropertyNumber(js, prims_arr.val, 0, prim.val); } JS_SetPropertyStr(js, m.val, "primitives", prims_arr.val); JS_SetPropertyNumber(js, meshes_arr.val, mi, m.val); } JS_SetPropertyStr(js, obj.val, "accessors", accessors_arr.val); JS_SetPropertyStr(js, obj.val, "meshes", meshes_arr.val); // Materials JS_ROOT(materials_arr, JS_NewArray(js)); for (size_t i = 0; i < scene->materials.count; i++) { ufbx_material *mat = scene->materials.data[i]; JS_ROOT(m, JS_NewObject(js)); tmp = mat->element.name.length > 0 ? JS_NewString(js, mat->element.name.data) : JS_NULL; JS_SetPropertyStr(js, m.val, "name", tmp); JS_ROOT(pbr, JS_NewObject(js)); float bc[4] = { (float)mat->pbr.base_color.value_vec4.x, (float)mat->pbr.base_color.value_vec4.y, (float)mat->pbr.base_color.value_vec4.z, (float)mat->pbr.base_color.value_vec4.w }; tmp = make_float_array_f(js, bc, 4); JS_SetPropertyStr(js, pbr.val, "base_color_factor", tmp); JS_SetPropertyStr(js, pbr.val, "base_color_texture", JS_NULL); JS_SetPropertyStr(js, pbr.val, "metallic_factor", JS_NewFloat64(js, mat->pbr.metalness.value_real)); JS_SetPropertyStr(js, pbr.val, "roughness_factor", JS_NewFloat64(js, mat->pbr.roughness.value_real)); JS_SetPropertyStr(js, pbr.val, "metallic_roughness_texture", JS_NULL); JS_SetPropertyStr(js, pbr.val, "normal_texture", JS_NULL); JS_SetPropertyStr(js, pbr.val, "occlusion_texture", JS_NULL); float ef[3] = { (float)mat->pbr.emission_color.value_vec3.x, (float)mat->pbr.emission_color.value_vec3.y, (float)mat->pbr.emission_color.value_vec3.z }; tmp = make_float_array_f(js, ef, 3); JS_SetPropertyStr(js, pbr.val, "emissive_factor", tmp); JS_SetPropertyStr(js, pbr.val, "emissive_texture", JS_NULL); JS_SetPropertyStr(js, m.val, "pbr", pbr.val); tmp = JS_NewString(js, "OPAQUE"); JS_SetPropertyStr(js, m.val, "alpha_mode", tmp); JS_SetPropertyStr(js, m.val, "alpha_cutoff", JS_NewFloat64(js, 0.5)); JS_SetPropertyStr(js, m.val, "double_sided", JS_FALSE); JS_SetPropertyNumber(js, materials_arr.val, i, m.val); } JS_SetPropertyStr(js, obj.val, "materials", materials_arr.val); // Images/textures (simplified - just list texture files) JS_ROOT(images_arr, JS_NewArray(js)); for (size_t i = 0; i < scene->texture_files.count; i++) { ufbx_texture_file *tf = &scene->texture_files.data[i]; JS_ROOT(im, JS_NewObject(js)); tmp = JS_NewString(js, "uri"); JS_SetPropertyStr(js, im.val, "kind", tmp); tmp = tf->filename.length > 0 ? JS_NewString(js, tf->filename.data) : JS_NULL; JS_SetPropertyStr(js, im.val, "uri", tmp); JS_SetPropertyStr(js, im.val, "mime", JS_NULL); JS_SetPropertyNumber(js, images_arr.val, i, im.val); } JS_SetPropertyStr(js, obj.val, "images", images_arr.val); JS_ROOT(textures_arr, JS_NewArray(js)); for (size_t i = 0; i < scene->textures.count; i++) { ufbx_texture *tex = scene->textures.data[i]; JS_ROOT(t, JS_NewObject(js)); JS_SetPropertyStr(js, t.val, "image", tex->file_index != UFBX_NO_INDEX ? JS_NewInt32(js, tex->file_index) : JS_NULL); JS_SetPropertyStr(js, t.val, "sampler", JS_NULL); JS_SetPropertyNumber(js, textures_arr.val, i, t.val); } JS_SetPropertyStr(js, obj.val, "textures", textures_arr.val); tmp = JS_NewArray(js); JS_SetPropertyStr(js, obj.val, "samplers", tmp); // Nodes JS_ROOT(nodes_arr, JS_NewArray(js)); for (size_t i = 0; i < scene->nodes.count; i++) { ufbx_node *node = scene->nodes.data[i]; JS_ROOT(n, JS_NewObject(js)); tmp = node->element.name.length > 0 ? JS_NewString(js, node->element.name.data) : JS_NULL; JS_SetPropertyStr(js, n.val, "name", tmp); // Find mesh index if this node has a mesh int mesh_idx = -1; if (node->mesh) { for (size_t mi = 0; mi < scene->meshes.count; mi++) { if (scene->meshes.data[mi] == node->mesh) { mesh_idx = (int)mi; break; } } } JS_SetPropertyStr(js, n.val, "mesh", mesh_idx >= 0 ? JS_NewInt32(js, mesh_idx) : JS_NULL); JS_ROOT(children, JS_NewArray(js)); for (size_t ci = 0; ci < node->children.count; ci++) { // Find child node index for (size_t ni = 0; ni < scene->nodes.count; ni++) { if (scene->nodes.data[ni] == node->children.data[ci]) { JS_SetPropertyNumber(js, children.val, ci, JS_NewInt32(js, ni)); break; } } } JS_SetPropertyStr(js, n.val, "children", children.val); JS_SetPropertyStr(js, n.val, "matrix", JS_NULL); double tr[3] = {node->local_transform.translation.x, node->local_transform.translation.y, node->local_transform.translation.z}; double ro[4] = {node->local_transform.rotation.x, node->local_transform.rotation.y, node->local_transform.rotation.z, node->local_transform.rotation.w}; double sc[3] = {node->local_transform.scale.x, node->local_transform.scale.y, node->local_transform.scale.z}; tmp = make_float_array(js, tr, 3); JS_SetPropertyStr(js, n.val, "translation", tmp); tmp = make_float_array(js, ro, 4); JS_SetPropertyStr(js, n.val, "rotation", tmp); tmp = make_float_array(js, sc, 3); JS_SetPropertyStr(js, n.val, "scale", tmp); JS_SetPropertyStr(js, n.val, "skin", JS_NULL); JS_SetPropertyNumber(js, nodes_arr.val, i, n.val); } JS_SetPropertyStr(js, obj.val, "nodes", nodes_arr.val); // Scenes - FBX has one implicit scene with root node JS_ROOT(scenes_arr, JS_NewArray(js)); { JS_ROOT(scene_obj, JS_NewObject(js)); JS_ROOT(scene_nodes, JS_NewArray(js)); // Find root node index for (size_t i = 0; i < scene->nodes.count; i++) { if (scene->nodes.data[i] == scene->root_node) { JS_SetPropertyNumber(js, scene_nodes.val, 0, JS_NewInt32(js, i)); break; } } JS_SetPropertyStr(js, scene_obj.val, "nodes", scene_nodes.val); JS_SetPropertyNumber(js, scenes_arr.val, 0, scene_obj.val); } JS_SetPropertyStr(js, obj.val, "scenes", scenes_arr.val); JS_SetPropertyStr(js, obj.val, "scene", JS_NewInt32(js, 0)); // Animations JS_ROOT(anims_arr, JS_NewArray(js)); for (size_t ai = 0; ai < scene->anim_stacks.count; ai++) { ufbx_anim_stack *stack = scene->anim_stacks.data[ai]; JS_ROOT(a, JS_NewObject(js)); tmp = stack->element.name.length > 0 ? JS_NewString(js, stack->element.name.data) : JS_NULL; JS_SetPropertyStr(js, a.val, "name", tmp); tmp = JS_NewArray(js); JS_SetPropertyStr(js, a.val, "samplers", tmp); tmp = JS_NewArray(js); JS_SetPropertyStr(js, a.val, "channels", tmp); JS_SetPropertyNumber(js, anims_arr.val, ai, a.val); } JS_SetPropertyStr(js, obj.val, "animations", anims_arr.val); // Skins JS_ROOT(skins_arr, JS_NewArray(js)); for (size_t i = 0; i < scene->skin_deformers.count; i++) { ufbx_skin_deformer *skin = scene->skin_deformers.data[i]; JS_ROOT(s, JS_NewObject(js)); tmp = skin->element.name.length > 0 ? JS_NewString(js, skin->element.name.data) : JS_NULL; JS_SetPropertyStr(js, s.val, "name", tmp); JS_ROOT(joints, JS_NewArray(js)); for (size_t ci = 0; ci < skin->clusters.count; ci++) { ufbx_skin_cluster *cluster = skin->clusters.data[ci]; if (cluster->bone_node) { for (size_t ni = 0; ni < scene->nodes.count; ni++) { if (scene->nodes.data[ni] == cluster->bone_node) { JS_SetPropertyNumber(js, joints.val, ci, JS_NewInt32(js, ni)); break; } } } } JS_SetPropertyStr(js, s.val, "joints", joints.val); JS_SetPropertyStr(js, s.val, "inverse_bind_matrices", JS_NULL); JS_SetPropertyStr(js, s.val, "skeleton", JS_NULL); JS_SetPropertyNumber(js, skins_arr.val, i, s.val); } JS_SetPropertyStr(js, obj.val, "skins", skins_arr.val); // Extensions JS_ROOT(exts, JS_NewObject(js)); tmp = JS_NewArray(js); JS_SetPropertyStr(js, exts.val, "used", tmp); tmp = JS_NewArray(js); JS_SetPropertyStr(js, exts.val, "required", tmp); JS_SetPropertyStr(js, obj.val, "extensions", exts.val); free(mesh_infos); free(buffer_data); ufbx_free_scene(scene); JS_RETURN(obj.val); } static const JSCFunctionListEntry js_fbx_funcs[] = { MIST_FUNC_DEF(fbx, decode, 1), }; CELL_USE_FUNCS(js_fbx_funcs)