From 817f732c08491765b666e33aadb54ff3654c35e4 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 12 Dec 2025 17:26:44 -0600 Subject: [PATCH] loaders --- cell.toml | 3 + cgltf.c | 0 fbx.c | 470 +++++++++++++++++++++++++++++++++++++++++++++++ gltf.c | 421 ++++++++++++++++++++++++++++++++++++++++++ obj.c | 463 ++++++++++++++++++++++++++++++++++++++++++++++ tests/readout.ce | 181 ++++++++++++++++++ 6 files changed, 1538 insertions(+) delete mode 100644 cgltf.c create mode 100644 gltf.c create mode 100644 tests/readout.ce diff --git a/cell.toml b/cell.toml index e69de29..3db3365 100644 --- a/cell.toml +++ b/cell.toml @@ -0,0 +1,3 @@ +[package] +name = "cell-model" +version = "0.1.0" diff --git a/cgltf.c b/cgltf.c deleted file mode 100644 index e69de29..0000000 diff --git a/fbx.c b/fbx.c index e69de29..c674f74 100644 --- a/fbx.c +++ b/fbx.c @@ -0,0 +1,470 @@ +#include "cell.h" +#include +#include + +#include "ufbx.h" + +static JSValue make_float_array(JSContext *js, const double *arr, int count) +{ + JSValue a = JS_NewArray(js); + for (int i = 0; i < count; i++) + JS_SetPropertyUint32(js, a, i, JS_NewFloat64(js, arr[i])); + return a; +} + +static JSValue make_float_array_f(JSContext *js, const float *arr, int count) +{ + JSValue a = JS_NewArray(js); + for (int i = 0; i < count; i++) + JS_SetPropertyUint32(js, a, i, JS_NewFloat64(js, arr[i])); + return a; +} + +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); + + JSValue 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 buffers_arr = JS_NewArray(js); + JSValue buf = JS_NewObject(js); + JS_SetPropertyStr(js, buf, "blob", js_new_blob_stoned_copy(js, buffer_data, total_buffer_size)); + JS_SetPropertyStr(js, buf, "byte_length", JS_NewInt64(js, total_buffer_size)); + JS_SetPropertyUint32(js, buffers_arr, 0, buf); + JS_SetPropertyStr(js, obj, "buffers", buffers_arr); + + // Create views + JSValue views_arr = JS_NewArray(js); + int view_idx = 0; + + JSValue pos_view = JS_NewObject(js); + JS_SetPropertyStr(js, pos_view, "buffer", JS_NewInt32(js, 0)); + JS_SetPropertyStr(js, pos_view, "byte_offset", JS_NewInt64(js, 0)); + JS_SetPropertyStr(js, pos_view, "byte_length", JS_NewInt64(js, pos_size)); + JS_SetPropertyStr(js, pos_view, "byte_stride", JS_NULL); + JS_SetPropertyStr(js, pos_view, "usage", JS_NewString(js, "vertex")); + int pos_view_idx = view_idx; + JS_SetPropertyUint32(js, views_arr, view_idx++, pos_view); + + int norm_view_idx = -1; + if (global_has_normals) { + JSValue norm_view = JS_NewObject(js); + JS_SetPropertyStr(js, norm_view, "buffer", JS_NewInt32(js, 0)); + JS_SetPropertyStr(js, norm_view, "byte_offset", JS_NewInt64(js, pos_size)); + JS_SetPropertyStr(js, norm_view, "byte_length", JS_NewInt64(js, norm_size)); + JS_SetPropertyStr(js, norm_view, "byte_stride", JS_NULL); + JS_SetPropertyStr(js, norm_view, "usage", JS_NewString(js, "vertex")); + norm_view_idx = view_idx; + JS_SetPropertyUint32(js, views_arr, view_idx++, norm_view); + } + + int uv_view_idx = -1; + if (global_has_uvs) { + JSValue uv_view = JS_NewObject(js); + JS_SetPropertyStr(js, uv_view, "buffer", JS_NewInt32(js, 0)); + JS_SetPropertyStr(js, uv_view, "byte_offset", JS_NewInt64(js, pos_size + norm_size)); + JS_SetPropertyStr(js, uv_view, "byte_length", JS_NewInt64(js, uv_size)); + JS_SetPropertyStr(js, uv_view, "byte_stride", JS_NULL); + JS_SetPropertyStr(js, uv_view, "usage", JS_NewString(js, "vertex")); + uv_view_idx = view_idx; + JS_SetPropertyUint32(js, views_arr, view_idx++, uv_view); + } + + JSValue idx_view = JS_NewObject(js); + JS_SetPropertyStr(js, idx_view, "buffer", JS_NewInt32(js, 0)); + JS_SetPropertyStr(js, idx_view, "byte_offset", JS_NewInt64(js, pos_size + norm_size + uv_size)); + JS_SetPropertyStr(js, idx_view, "byte_length", JS_NewInt64(js, idx_size)); + JS_SetPropertyStr(js, idx_view, "byte_stride", JS_NULL); + JS_SetPropertyStr(js, idx_view, "usage", JS_NewString(js, "index")); + int idx_view_idx = view_idx; + JS_SetPropertyUint32(js, views_arr, view_idx++, idx_view); + + JS_SetPropertyStr(js, obj, "views", views_arr); + + // Create accessors per mesh + JSValue accessors_arr = JS_NewArray(js); + int acc_idx = 0; + + JSValue 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; + JSValue spa = JS_NewObject(js); + JS_SetPropertyStr(js, spa, "view", JS_NewInt32(js, pos_view_idx)); + JS_SetPropertyStr(js, spa, "byte_offset", JS_NewInt64(js, info->vertex_start * 3 * sizeof(float))); + JS_SetPropertyStr(js, spa, "count", JS_NewInt64(js, info->vertex_count)); + JS_SetPropertyStr(js, spa, "component_type", JS_NewString(js, "f32")); + JS_SetPropertyStr(js, spa, "type", JS_NewString(js, "vec3")); + JS_SetPropertyStr(js, spa, "normalized", JS_FALSE); + JS_SetPropertyStr(js, spa, "min", JS_NULL); + JS_SetPropertyStr(js, spa, "max", JS_NULL); + JS_SetPropertyUint32(js, accessors_arr, acc_idx++, spa); + + int mesh_norm_acc = -1; + if (global_has_normals) { + mesh_norm_acc = acc_idx; + JSValue sna = JS_NewObject(js); + JS_SetPropertyStr(js, sna, "view", JS_NewInt32(js, norm_view_idx)); + JS_SetPropertyStr(js, sna, "byte_offset", JS_NewInt64(js, info->vertex_start * 3 * sizeof(float))); + JS_SetPropertyStr(js, sna, "count", JS_NewInt64(js, info->vertex_count)); + JS_SetPropertyStr(js, sna, "component_type", JS_NewString(js, "f32")); + JS_SetPropertyStr(js, sna, "type", JS_NewString(js, "vec3")); + JS_SetPropertyStr(js, sna, "normalized", JS_FALSE); + JS_SetPropertyStr(js, sna, "min", JS_NULL); + JS_SetPropertyStr(js, sna, "max", JS_NULL); + JS_SetPropertyUint32(js, accessors_arr, acc_idx++, sna); + } + + int mesh_uv_acc = -1; + if (global_has_uvs) { + mesh_uv_acc = acc_idx; + JSValue sua = JS_NewObject(js); + JS_SetPropertyStr(js, sua, "view", JS_NewInt32(js, uv_view_idx)); + JS_SetPropertyStr(js, sua, "byte_offset", JS_NewInt64(js, info->vertex_start * 2 * sizeof(float))); + JS_SetPropertyStr(js, sua, "count", JS_NewInt64(js, info->vertex_count)); + JS_SetPropertyStr(js, sua, "component_type", JS_NewString(js, "f32")); + JS_SetPropertyStr(js, sua, "type", JS_NewString(js, "vec2")); + JS_SetPropertyStr(js, sua, "normalized", JS_FALSE); + JS_SetPropertyStr(js, sua, "min", JS_NULL); + JS_SetPropertyStr(js, sua, "max", JS_NULL); + JS_SetPropertyUint32(js, accessors_arr, acc_idx++, sua); + } + + int mesh_idx_acc = acc_idx; + JSValue sia = JS_NewObject(js); + JS_SetPropertyStr(js, sia, "view", JS_NewInt32(js, idx_view_idx)); + JS_SetPropertyStr(js, sia, "byte_offset", JS_NewInt64(js, info->index_start * (use_32bit ? sizeof(uint32_t) : sizeof(uint16_t)))); + JS_SetPropertyStr(js, sia, "count", JS_NewInt64(js, info->index_count)); + JS_SetPropertyStr(js, sia, "component_type", JS_NewString(js, use_32bit ? "u32" : "u16")); + JS_SetPropertyStr(js, sia, "type", JS_NewString(js, "scalar")); + JS_SetPropertyStr(js, sia, "normalized", JS_FALSE); + JS_SetPropertyStr(js, sia, "min", JS_NULL); + JS_SetPropertyStr(js, sia, "max", JS_NULL); + JS_SetPropertyUint32(js, accessors_arr, acc_idx++, sia); + + // Create mesh + JSValue m = JS_NewObject(js); + JS_SetPropertyStr(js, m, "name", mesh->element.name.length > 0 ? JS_NewString(js, mesh->element.name.data) : JS_NULL); + + JSValue prims_arr = JS_NewArray(js); + JSValue prim = JS_NewObject(js); + JS_SetPropertyStr(js, prim, "topology", JS_NewString(js, "triangles")); + + JSValue attrs = JS_NewObject(js); + JS_SetPropertyStr(js, attrs, "POSITION", JS_NewInt32(js, mesh_pos_acc)); + if (mesh_norm_acc >= 0) + JS_SetPropertyStr(js, attrs, "NORMAL", JS_NewInt32(js, mesh_norm_acc)); + if (mesh_uv_acc >= 0) + JS_SetPropertyStr(js, attrs, "TEXCOORD_0", JS_NewInt32(js, mesh_uv_acc)); + JS_SetPropertyStr(js, prim, "attributes", attrs); + + JS_SetPropertyStr(js, prim, "indices", JS_NewInt32(js, mesh_idx_acc)); + JS_SetPropertyStr(js, prim, "material", mesh->materials.count > 0 ? JS_NewInt32(js, mesh->materials.data[0]->typed_id) : JS_NULL); + + JS_SetPropertyUint32(js, prims_arr, 0, prim); + JS_SetPropertyStr(js, m, "primitives", prims_arr); + + JS_SetPropertyUint32(js, meshes_arr, mi, m); + } + + JS_SetPropertyStr(js, obj, "accessors", accessors_arr); + JS_SetPropertyStr(js, obj, "meshes", meshes_arr); + + // Materials + JSValue materials_arr = JS_NewArray(js); + for (size_t i = 0; i < scene->materials.count; i++) { + ufbx_material *mat = scene->materials.data[i]; + JSValue m = JS_NewObject(js); + JS_SetPropertyStr(js, m, "name", mat->element.name.length > 0 ? JS_NewString(js, mat->element.name.data) : JS_NULL); + + JSValue 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 + }; + JS_SetPropertyStr(js, pbr, "base_color_factor", make_float_array_f(js, bc, 4)); + JS_SetPropertyStr(js, pbr, "base_color_texture", JS_NULL); + JS_SetPropertyStr(js, pbr, "metallic_factor", JS_NewFloat64(js, mat->pbr.metalness.value_real)); + JS_SetPropertyStr(js, pbr, "roughness_factor", JS_NewFloat64(js, mat->pbr.roughness.value_real)); + JS_SetPropertyStr(js, pbr, "metallic_roughness_texture", JS_NULL); + JS_SetPropertyStr(js, pbr, "normal_texture", JS_NULL); + JS_SetPropertyStr(js, pbr, "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 + }; + JS_SetPropertyStr(js, pbr, "emissive_factor", make_float_array_f(js, ef, 3)); + JS_SetPropertyStr(js, pbr, "emissive_texture", JS_NULL); + JS_SetPropertyStr(js, m, "pbr", pbr); + + JS_SetPropertyStr(js, m, "alpha_mode", JS_NewString(js, "OPAQUE")); + JS_SetPropertyStr(js, m, "alpha_cutoff", JS_NewFloat64(js, 0.5)); + JS_SetPropertyStr(js, m, "double_sided", JS_FALSE); + + JS_SetPropertyUint32(js, materials_arr, i, m); + } + JS_SetPropertyStr(js, obj, "materials", materials_arr); + + // Images/textures (simplified - just list texture files) + JSValue 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]; + JSValue im = JS_NewObject(js); + JS_SetPropertyStr(js, im, "kind", JS_NewString(js, "uri")); + JS_SetPropertyStr(js, im, "uri", tf->filename.length > 0 ? JS_NewString(js, tf->filename.data) : JS_NULL); + JS_SetPropertyStr(js, im, "mime", JS_NULL); + JS_SetPropertyUint32(js, images_arr, i, im); + } + JS_SetPropertyStr(js, obj, "images", images_arr); + + JSValue textures_arr = JS_NewArray(js); + for (size_t i = 0; i < scene->textures.count; i++) { + ufbx_texture *tex = scene->textures.data[i]; + JSValue t = JS_NewObject(js); + JS_SetPropertyStr(js, t, "image", tex->file_index != UFBX_NO_INDEX ? JS_NewInt32(js, tex->file_index) : JS_NULL); + JS_SetPropertyStr(js, t, "sampler", JS_NULL); + JS_SetPropertyUint32(js, textures_arr, i, t); + } + JS_SetPropertyStr(js, obj, "textures", textures_arr); + + JS_SetPropertyStr(js, obj, "samplers", JS_NewArray(js)); + + // Nodes + JSValue nodes_arr = JS_NewArray(js); + for (size_t i = 0; i < scene->nodes.count; i++) { + ufbx_node *node = scene->nodes.data[i]; + JSValue n = JS_NewObject(js); + JS_SetPropertyStr(js, n, "name", node->element.name.length > 0 ? JS_NewString(js, node->element.name.data) : JS_NULL); + + // 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, "mesh", mesh_idx >= 0 ? JS_NewInt32(js, mesh_idx) : JS_NULL); + + JSValue 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_SetPropertyUint32(js, children, ci, JS_NewInt32(js, ni)); + break; + } + } + } + JS_SetPropertyStr(js, n, "children", children); + + JS_SetPropertyStr(js, n, "matrix", JS_NULL); + double t[3] = {node->local_transform.translation.x, node->local_transform.translation.y, node->local_transform.translation.z}; + double r[4] = {node->local_transform.rotation.x, node->local_transform.rotation.y, node->local_transform.rotation.z, node->local_transform.rotation.w}; + double s[3] = {node->local_transform.scale.x, node->local_transform.scale.y, node->local_transform.scale.z}; + JS_SetPropertyStr(js, n, "translation", make_float_array(js, t, 3)); + JS_SetPropertyStr(js, n, "rotation", make_float_array(js, r, 4)); + JS_SetPropertyStr(js, n, "scale", make_float_array(js, s, 3)); + JS_SetPropertyStr(js, n, "skin", JS_NULL); + + JS_SetPropertyUint32(js, nodes_arr, i, n); + } + JS_SetPropertyStr(js, obj, "nodes", nodes_arr); + + // Scenes - FBX has one implicit scene with root node + JSValue scenes_arr = JS_NewArray(js); + JSValue scene_obj = JS_NewObject(js); + JSValue 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_SetPropertyUint32(js, scene_nodes, 0, JS_NewInt32(js, i)); + break; + } + } + JS_SetPropertyStr(js, scene_obj, "nodes", scene_nodes); + JS_SetPropertyUint32(js, scenes_arr, 0, scene_obj); + JS_SetPropertyStr(js, obj, "scenes", scenes_arr); + JS_SetPropertyStr(js, obj, "scene", JS_NewInt32(js, 0)); + + // Animations + JSValue 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]; + JSValue a = JS_NewObject(js); + JS_SetPropertyStr(js, a, "name", stack->element.name.length > 0 ? JS_NewString(js, stack->element.name.data) : JS_NULL); + JS_SetPropertyStr(js, a, "samplers", JS_NewArray(js)); + JS_SetPropertyStr(js, a, "channels", JS_NewArray(js)); + JS_SetPropertyUint32(js, anims_arr, ai, a); + } + JS_SetPropertyStr(js, obj, "animations", anims_arr); + + // Skins + JSValue 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]; + JSValue s = JS_NewObject(js); + JS_SetPropertyStr(js, s, "name", skin->element.name.length > 0 ? JS_NewString(js, skin->element.name.data) : JS_NULL); + + JSValue 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_SetPropertyUint32(js, joints, ci, JS_NewInt32(js, ni)); + break; + } + } + } + } + JS_SetPropertyStr(js, s, "joints", joints); + JS_SetPropertyStr(js, s, "inverse_bind_matrices", JS_NULL); + JS_SetPropertyStr(js, s, "skeleton", JS_NULL); + + JS_SetPropertyUint32(js, skins_arr, i, s); + } + JS_SetPropertyStr(js, obj, "skins", skins_arr); + + // Extensions + JSValue exts = JS_NewObject(js); + JS_SetPropertyStr(js, exts, "used", JS_NewArray(js)); + JS_SetPropertyStr(js, exts, "required", JS_NewArray(js)); + JS_SetPropertyStr(js, obj, "extensions", exts); + + free(mesh_infos); + free(buffer_data); + ufbx_free_scene(scene); + + return obj; +} + +static const JSCFunctionListEntry js_fbx_funcs[] = { + MIST_FUNC_DEF(fbx, decode, 1), +}; + +CELL_USE_FUNCS(js_fbx_funcs) diff --git a/gltf.c b/gltf.c new file mode 100644 index 0000000..639eec8 --- /dev/null +++ b/gltf.c @@ -0,0 +1,421 @@ +#include "cell.h" +#include +#include + +#define CGLTF_IMPLEMENTATION +#include "cgltf.h" + +static const char *component_type_str(cgltf_component_type t) +{ + switch (t) { + case cgltf_component_type_r_8: return "i8"; + case cgltf_component_type_r_8u: return "u8"; + case cgltf_component_type_r_16: return "i16"; + case cgltf_component_type_r_16u: return "u16"; + case cgltf_component_type_r_32u: return "u32"; + case cgltf_component_type_r_32f: return "f32"; + default: return "unknown"; + } +} + +static const char *type_str(cgltf_type t) +{ + switch (t) { + case cgltf_type_scalar: return "scalar"; + case cgltf_type_vec2: return "vec2"; + case cgltf_type_vec3: return "vec3"; + case cgltf_type_vec4: return "vec4"; + case cgltf_type_mat2: return "mat2"; + case cgltf_type_mat3: return "mat3"; + case cgltf_type_mat4: return "mat4"; + default: return "unknown"; + } +} + +static const char *topology_str(cgltf_primitive_type t) +{ + switch (t) { + case cgltf_primitive_type_points: return "points"; + case cgltf_primitive_type_lines: return "lines"; + case cgltf_primitive_type_line_loop: return "line_loop"; + case cgltf_primitive_type_line_strip: return "line_strip"; + case cgltf_primitive_type_triangles: return "triangles"; + case cgltf_primitive_type_triangle_strip: return "triangle_strip"; + case cgltf_primitive_type_triangle_fan: return "triangle_fan"; + default: return "unknown"; + } +} + +static const char *attribute_name(cgltf_attribute_type t, int index) +{ + static char buf[32]; + switch (t) { + case cgltf_attribute_type_position: return "POSITION"; + case cgltf_attribute_type_normal: return "NORMAL"; + case cgltf_attribute_type_tangent: return "TANGENT"; + case cgltf_attribute_type_texcoord: + snprintf(buf, sizeof(buf), "TEXCOORD_%d", index); + return buf; + case cgltf_attribute_type_color: + snprintf(buf, sizeof(buf), "COLOR_%d", index); + return buf; + case cgltf_attribute_type_joints: + snprintf(buf, sizeof(buf), "JOINTS_%d", index); + return buf; + case cgltf_attribute_type_weights: + snprintf(buf, sizeof(buf), "WEIGHTS_%d", index); + return buf; + default: return "UNKNOWN"; + } +} + +static JSValue make_float_array(JSContext *js, const float *arr, int count) +{ + JSValue a = JS_NewArray(js); + for (int i = 0; i < count; i++) + JS_SetPropertyUint32(js, a, i, JS_NewFloat64(js, arr[i])); + return a; +} + +static JSValue make_texture_info(JSContext *js, cgltf_texture_view *tv, cgltf_data *data) +{ + if (!tv->texture) return JS_NULL; + JSValue o = JS_NewObject(js); + JS_SetPropertyStr(js, o, "texture", JS_NewInt32(js, (int)(tv->texture - data->textures))); + JS_SetPropertyStr(js, o, "texcoord", JS_NewInt32(js, tv->texcoord)); + if (tv->has_transform) { + JSValue tr = JS_NewObject(js); + JS_SetPropertyStr(js, tr, "offset", make_float_array(js, tv->transform.offset, 2)); + JS_SetPropertyStr(js, tr, "scale", make_float_array(js, tv->transform.scale, 2)); + JS_SetPropertyStr(js, tr, "rotation", JS_NewFloat64(js, tv->transform.rotation)); + JS_SetPropertyStr(js, o, "transform", tr); + } + return o; +} + +JSValue js_gltf_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; + + cgltf_options options = {0}; + cgltf_data *data = NULL; + + cgltf_result result = cgltf_parse(&options, raw, len, &data); + if (result != cgltf_result_success) + return JS_ThrowReferenceError(js, "failed to parse glTF: %d", result); + + result = cgltf_load_buffers(&options, data, NULL); + if (result != cgltf_result_success) { + cgltf_free(data); + return JS_ThrowReferenceError(js, "failed to load glTF buffers: %d", result); + } + + JSValue obj = JS_NewObject(js); + + // Buffers + JSValue buffers_arr = JS_NewArray(js); + for (cgltf_size i = 0; i < data->buffers_count; i++) { + cgltf_buffer *buf = &data->buffers[i]; + JSValue b = JS_NewObject(js); + if (buf->data && buf->size > 0) + JS_SetPropertyStr(js, b, "blob", js_new_blob_stoned_copy(js, buf->data, buf->size)); + else + JS_SetPropertyStr(js, b, "blob", JS_NULL); + JS_SetPropertyStr(js, b, "byte_length", JS_NewInt64(js, buf->size)); + JS_SetPropertyUint32(js, buffers_arr, i, b); + } + JS_SetPropertyStr(js, obj, "buffers", buffers_arr); + + // Buffer views + JSValue views_arr = JS_NewArray(js); + for (cgltf_size i = 0; i < data->buffer_views_count; i++) { + cgltf_buffer_view *bv = &data->buffer_views[i]; + JSValue v = JS_NewObject(js); + JS_SetPropertyStr(js, v, "buffer", JS_NewInt32(js, (int)(bv->buffer - data->buffers))); + JS_SetPropertyStr(js, v, "byte_offset", JS_NewInt64(js, bv->offset)); + JS_SetPropertyStr(js, v, "byte_length", JS_NewInt64(js, bv->size)); + JS_SetPropertyStr(js, v, "byte_stride", bv->stride ? JS_NewInt32(js, bv->stride) : JS_NULL); + const char *usage = "unknown"; + if (bv->type == cgltf_buffer_view_type_vertices) usage = "vertex"; + else if (bv->type == cgltf_buffer_view_type_indices) usage = "index"; + JS_SetPropertyStr(js, v, "usage", JS_NewString(js, usage)); + JS_SetPropertyUint32(js, views_arr, i, v); + } + JS_SetPropertyStr(js, obj, "views", views_arr); + + // Accessors + JSValue accessors_arr = JS_NewArray(js); + for (cgltf_size i = 0; i < data->accessors_count; i++) { + cgltf_accessor *acc = &data->accessors[i]; + JSValue a = JS_NewObject(js); + if (acc->buffer_view) + JS_SetPropertyStr(js, a, "view", JS_NewInt32(js, (int)(acc->buffer_view - data->buffer_views))); + else + JS_SetPropertyStr(js, a, "view", JS_NULL); + JS_SetPropertyStr(js, a, "byte_offset", JS_NewInt64(js, acc->offset)); + JS_SetPropertyStr(js, a, "count", JS_NewInt64(js, acc->count)); + JS_SetPropertyStr(js, a, "component_type", JS_NewString(js, component_type_str(acc->component_type))); + JS_SetPropertyStr(js, a, "type", JS_NewString(js, type_str(acc->type))); + JS_SetPropertyStr(js, a, "normalized", JS_NewBool(js, acc->normalized)); + if (acc->has_min) { + int n = cgltf_num_components(acc->type); + JS_SetPropertyStr(js, a, "min", make_float_array(js, acc->min, n)); + } else { + JS_SetPropertyStr(js, a, "min", JS_NULL); + } + if (acc->has_max) { + int n = cgltf_num_components(acc->type); + JS_SetPropertyStr(js, a, "max", make_float_array(js, acc->max, n)); + } else { + JS_SetPropertyStr(js, a, "max", JS_NULL); + } + JS_SetPropertyUint32(js, accessors_arr, i, a); + } + JS_SetPropertyStr(js, obj, "accessors", accessors_arr); + + // Meshes + JSValue meshes_arr = JS_NewArray(js); + for (cgltf_size mi = 0; mi < data->meshes_count; mi++) { + cgltf_mesh *mesh = &data->meshes[mi]; + JSValue m = JS_NewObject(js); + JS_SetPropertyStr(js, m, "name", mesh->name ? JS_NewString(js, mesh->name) : JS_NULL); + + JSValue prims_arr = JS_NewArray(js); + for (cgltf_size pi = 0; pi < mesh->primitives_count; pi++) { + cgltf_primitive *prim = &mesh->primitives[pi]; + JSValue p = JS_NewObject(js); + JS_SetPropertyStr(js, p, "topology", JS_NewString(js, topology_str(prim->type))); + + JSValue attrs = JS_NewObject(js); + for (cgltf_size ai = 0; ai < prim->attributes_count; ai++) { + cgltf_attribute *attr = &prim->attributes[ai]; + const char *name = attribute_name(attr->type, attr->index); + JS_SetPropertyStr(js, attrs, name, JS_NewInt32(js, (int)(attr->data - data->accessors))); + } + JS_SetPropertyStr(js, p, "attributes", attrs); + + if (prim->indices) + JS_SetPropertyStr(js, p, "indices", JS_NewInt32(js, (int)(prim->indices - data->accessors))); + else + JS_SetPropertyStr(js, p, "indices", JS_NULL); + + if (prim->material) + JS_SetPropertyStr(js, p, "material", JS_NewInt32(js, (int)(prim->material - data->materials))); + else + JS_SetPropertyStr(js, p, "material", JS_NULL); + + JS_SetPropertyUint32(js, prims_arr, pi, p); + } + JS_SetPropertyStr(js, m, "primitives", prims_arr); + JS_SetPropertyUint32(js, meshes_arr, mi, m); + } + JS_SetPropertyStr(js, obj, "meshes", meshes_arr); + + // Images + JSValue images_arr = JS_NewArray(js); + for (cgltf_size i = 0; i < data->images_count; i++) { + cgltf_image *img = &data->images[i]; + JSValue im = JS_NewObject(js); + if (img->buffer_view) { + JS_SetPropertyStr(js, im, "kind", JS_NewString(js, "buffer_view")); + JS_SetPropertyStr(js, im, "view", JS_NewInt32(js, (int)(img->buffer_view - data->buffer_views))); + } else if (img->uri) { + JS_SetPropertyStr(js, im, "kind", JS_NewString(js, "uri")); + JS_SetPropertyStr(js, im, "uri", JS_NewString(js, img->uri)); + } + JS_SetPropertyStr(js, im, "mime", img->mime_type ? JS_NewString(js, img->mime_type) : JS_NULL); + JS_SetPropertyUint32(js, images_arr, i, im); + } + JS_SetPropertyStr(js, obj, "images", images_arr); + + // Textures + JSValue textures_arr = JS_NewArray(js); + for (cgltf_size i = 0; i < data->textures_count; i++) { + cgltf_texture *tex = &data->textures[i]; + JSValue t = JS_NewObject(js); + JS_SetPropertyStr(js, t, "image", tex->image ? JS_NewInt32(js, (int)(tex->image - data->images)) : JS_NULL); + JS_SetPropertyStr(js, t, "sampler", tex->sampler ? JS_NewInt32(js, (int)(tex->sampler - data->samplers)) : JS_NULL); + JS_SetPropertyUint32(js, textures_arr, i, t); + } + JS_SetPropertyStr(js, obj, "textures", textures_arr); + + // Samplers + JSValue samplers_arr = JS_NewArray(js); + for (cgltf_size i = 0; i < data->samplers_count; i++) { + cgltf_sampler *samp = &data->samplers[i]; + JSValue s = JS_NewObject(js); + JS_SetPropertyStr(js, s, "min_filter", JS_NewInt32(js, samp->min_filter)); + JS_SetPropertyStr(js, s, "mag_filter", JS_NewInt32(js, samp->mag_filter)); + JS_SetPropertyStr(js, s, "wrap_s", JS_NewInt32(js, samp->wrap_s)); + JS_SetPropertyStr(js, s, "wrap_t", JS_NewInt32(js, samp->wrap_t)); + JS_SetPropertyUint32(js, samplers_arr, i, s); + } + JS_SetPropertyStr(js, obj, "samplers", samplers_arr); + + // Materials + JSValue materials_arr = JS_NewArray(js); + for (cgltf_size i = 0; i < data->materials_count; i++) { + cgltf_material *mat = &data->materials[i]; + JSValue m = JS_NewObject(js); + JS_SetPropertyStr(js, m, "name", mat->name ? JS_NewString(js, mat->name) : JS_NULL); + + JSValue pbr = JS_NewObject(js); + if (mat->has_pbr_metallic_roughness) { + cgltf_pbr_metallic_roughness *pmr = &mat->pbr_metallic_roughness; + JS_SetPropertyStr(js, pbr, "base_color_factor", make_float_array(js, pmr->base_color_factor, 4)); + JS_SetPropertyStr(js, pbr, "base_color_texture", make_texture_info(js, &pmr->base_color_texture, data)); + JS_SetPropertyStr(js, pbr, "metallic_factor", JS_NewFloat64(js, pmr->metallic_factor)); + JS_SetPropertyStr(js, pbr, "roughness_factor", JS_NewFloat64(js, pmr->roughness_factor)); + JS_SetPropertyStr(js, pbr, "metallic_roughness_texture", make_texture_info(js, &pmr->metallic_roughness_texture, data)); + } + JS_SetPropertyStr(js, pbr, "normal_texture", make_texture_info(js, &mat->normal_texture, data)); + JS_SetPropertyStr(js, pbr, "occlusion_texture", make_texture_info(js, &mat->occlusion_texture, data)); + JS_SetPropertyStr(js, pbr, "emissive_factor", make_float_array(js, mat->emissive_factor, 3)); + JS_SetPropertyStr(js, pbr, "emissive_texture", make_texture_info(js, &mat->emissive_texture, data)); + JS_SetPropertyStr(js, m, "pbr", pbr); + + const char *alpha_mode = "OPAQUE"; + if (mat->alpha_mode == cgltf_alpha_mode_mask) alpha_mode = "MASK"; + else if (mat->alpha_mode == cgltf_alpha_mode_blend) alpha_mode = "BLEND"; + JS_SetPropertyStr(js, m, "alpha_mode", JS_NewString(js, alpha_mode)); + JS_SetPropertyStr(js, m, "alpha_cutoff", JS_NewFloat64(js, mat->alpha_cutoff)); + JS_SetPropertyStr(js, m, "double_sided", JS_NewBool(js, mat->double_sided)); + + JS_SetPropertyUint32(js, materials_arr, i, m); + } + JS_SetPropertyStr(js, obj, "materials", materials_arr); + + // Nodes + JSValue nodes_arr = JS_NewArray(js); + for (cgltf_size i = 0; i < data->nodes_count; i++) { + cgltf_node *node = &data->nodes[i]; + JSValue n = JS_NewObject(js); + JS_SetPropertyStr(js, n, "name", node->name ? JS_NewString(js, node->name) : JS_NULL); + JS_SetPropertyStr(js, n, "mesh", node->mesh ? JS_NewInt32(js, (int)(node->mesh - data->meshes)) : JS_NULL); + + JSValue children = JS_NewArray(js); + for (cgltf_size ci = 0; ci < node->children_count; ci++) + JS_SetPropertyUint32(js, children, ci, JS_NewInt32(js, (int)(node->children[ci] - data->nodes))); + JS_SetPropertyStr(js, n, "children", children); + + if (node->has_matrix) { + JS_SetPropertyStr(js, n, "matrix", make_float_array(js, node->matrix, 16)); + JS_SetPropertyStr(js, n, "translation", JS_NULL); + JS_SetPropertyStr(js, n, "rotation", JS_NULL); + JS_SetPropertyStr(js, n, "scale", JS_NULL); + } else { + JS_SetPropertyStr(js, n, "matrix", JS_NULL); + JS_SetPropertyStr(js, n, "translation", make_float_array(js, node->translation, 3)); + JS_SetPropertyStr(js, n, "rotation", make_float_array(js, node->rotation, 4)); + JS_SetPropertyStr(js, n, "scale", make_float_array(js, node->scale, 3)); + } + + JS_SetPropertyStr(js, n, "skin", node->skin ? JS_NewInt32(js, (int)(node->skin - data->skins)) : JS_NULL); + JS_SetPropertyUint32(js, nodes_arr, i, n); + } + JS_SetPropertyStr(js, obj, "nodes", nodes_arr); + + // Scenes + JSValue scenes_arr = JS_NewArray(js); + for (cgltf_size i = 0; i < data->scenes_count; i++) { + cgltf_scene *scene = &data->scenes[i]; + JSValue s = JS_NewObject(js); + JSValue snodes = JS_NewArray(js); + for (cgltf_size ni = 0; ni < scene->nodes_count; ni++) + JS_SetPropertyUint32(js, snodes, ni, JS_NewInt32(js, (int)(scene->nodes[ni] - data->nodes))); + JS_SetPropertyStr(js, s, "nodes", snodes); + JS_SetPropertyUint32(js, scenes_arr, i, s); + } + JS_SetPropertyStr(js, obj, "scenes", scenes_arr); + JS_SetPropertyStr(js, obj, "scene", data->scene ? JS_NewInt32(js, (int)(data->scene - data->scenes)) : JS_NULL); + + // Animations + JSValue anims_arr = JS_NewArray(js); + for (cgltf_size ai = 0; ai < data->animations_count; ai++) { + cgltf_animation *anim = &data->animations[ai]; + JSValue a = JS_NewObject(js); + JS_SetPropertyStr(js, a, "name", anim->name ? JS_NewString(js, anim->name) : JS_NULL); + + JSValue samplers = JS_NewArray(js); + for (cgltf_size si = 0; si < anim->samplers_count; si++) { + cgltf_animation_sampler *samp = &anim->samplers[si]; + JSValue s = JS_NewObject(js); + JS_SetPropertyStr(js, s, "input", samp->input ? JS_NewInt32(js, (int)(samp->input - data->accessors)) : JS_NULL); + JS_SetPropertyStr(js, s, "output", samp->output ? JS_NewInt32(js, (int)(samp->output - data->accessors)) : JS_NULL); + const char *interp = "LINEAR"; + if (samp->interpolation == cgltf_interpolation_type_step) interp = "STEP"; + else if (samp->interpolation == cgltf_interpolation_type_cubic_spline) interp = "CUBICSPLINE"; + JS_SetPropertyStr(js, s, "interpolation", JS_NewString(js, interp)); + JS_SetPropertyUint32(js, samplers, si, s); + } + JS_SetPropertyStr(js, a, "samplers", samplers); + + JSValue channels = JS_NewArray(js); + for (cgltf_size ci = 0; ci < anim->channels_count; ci++) { + cgltf_animation_channel *chan = &anim->channels[ci]; + JSValue c = JS_NewObject(js); + // Find sampler index within this animation + int samp_idx = -1; + for (cgltf_size si = 0; si < anim->samplers_count; si++) { + if (&anim->samplers[si] == chan->sampler) { samp_idx = (int)si; break; } + } + JS_SetPropertyStr(js, c, "sampler", JS_NewInt32(js, samp_idx)); + JSValue target = JS_NewObject(js); + JS_SetPropertyStr(js, target, "node", chan->target_node ? JS_NewInt32(js, (int)(chan->target_node - data->nodes)) : JS_NULL); + const char *path = "unknown"; + switch (chan->target_path) { + case cgltf_animation_path_type_translation: path = "translation"; break; + case cgltf_animation_path_type_rotation: path = "rotation"; break; + case cgltf_animation_path_type_scale: path = "scale"; break; + case cgltf_animation_path_type_weights: path = "weights"; break; + default: break; + } + JS_SetPropertyStr(js, target, "path", JS_NewString(js, path)); + JS_SetPropertyStr(js, c, "target", target); + JS_SetPropertyUint32(js, channels, ci, c); + } + JS_SetPropertyStr(js, a, "channels", channels); + + JS_SetPropertyUint32(js, anims_arr, ai, a); + } + JS_SetPropertyStr(js, obj, "animations", anims_arr); + + // Skins + JSValue skins_arr = JS_NewArray(js); + for (cgltf_size i = 0; i < data->skins_count; i++) { + cgltf_skin *skin = &data->skins[i]; + JSValue s = JS_NewObject(js); + JS_SetPropertyStr(js, s, "name", skin->name ? JS_NewString(js, skin->name) : JS_NULL); + JSValue joints = JS_NewArray(js); + for (cgltf_size ji = 0; ji < skin->joints_count; ji++) + JS_SetPropertyUint32(js, joints, ji, JS_NewInt32(js, (int)(skin->joints[ji] - data->nodes))); + JS_SetPropertyStr(js, s, "joints", joints); + JS_SetPropertyStr(js, s, "inverse_bind_matrices", skin->inverse_bind_matrices ? JS_NewInt32(js, (int)(skin->inverse_bind_matrices - data->accessors)) : JS_NULL); + JS_SetPropertyStr(js, s, "skeleton", skin->skeleton ? JS_NewInt32(js, (int)(skin->skeleton - data->nodes)) : JS_NULL); + JS_SetPropertyUint32(js, skins_arr, i, s); + } + JS_SetPropertyStr(js, obj, "skins", skins_arr); + + // Extensions + JSValue exts = JS_NewObject(js); + JSValue used = JS_NewArray(js); + for (cgltf_size i = 0; i < data->extensions_used_count; i++) + JS_SetPropertyUint32(js, used, i, JS_NewString(js, data->extensions_used[i])); + JS_SetPropertyStr(js, exts, "used", used); + JSValue required = JS_NewArray(js); + for (cgltf_size i = 0; i < data->extensions_required_count; i++) + JS_SetPropertyUint32(js, required, i, JS_NewString(js, data->extensions_required[i])); + JS_SetPropertyStr(js, exts, "required", required); + JS_SetPropertyStr(js, obj, "extensions", exts); + + cgltf_free(data); + return obj; +} + +static const JSCFunctionListEntry js_gltf_funcs[] = { + MIST_FUNC_DEF(gltf, decode, 1), +}; + +CELL_USE_FUNCS(js_gltf_funcs) diff --git a/obj.c b/obj.c index e69de29..df9625d 100644 --- a/obj.c +++ b/obj.c @@ -0,0 +1,463 @@ +#include "cell.h" +#include +#include + +#define TINYOBJ_LOADER_C_IMPLEMENTATION +#include "tinyobj_loader_c.h" + +typedef struct { + const char *data; + size_t len; +} obj_reader_ctx_t; + +static void obj_file_reader(void *ctx, const char *filename, int is_mtl, const char *obj_filename, char **buf, size_t *len) +{ + (void)filename; + (void)obj_filename; + + if (buf) *buf = NULL; + if (len) *len = 0; + if (is_mtl) return; + + if (!ctx || !buf || !len) return; + + obj_reader_ctx_t *rctx = (obj_reader_ctx_t *)ctx; + *buf = (char *)rctx->data; + *len = rctx->len; +} + +static JSValue make_float_array(JSContext *js, const float *arr, int count) +{ + JSValue a = JS_NewArray(js); + for (int i = 0; i < count; i++) + JS_SetPropertyUint32(js, a, i, JS_NewFloat64(js, arr[i])); + return a; +} + +JSValue js_obj_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; + + tinyobj_attrib_t attrib; + tinyobj_shape_t *shapes = NULL; + tinyobj_material_t *materials = NULL; + size_t num_shapes, num_materials; + + obj_reader_ctx_t rctx; + rctx.data = (const char *)raw; + rctx.len = len; + + int result = tinyobj_parse_obj(&attrib, &shapes, &num_shapes, &materials, &num_materials, + "blob.obj", obj_file_reader, &rctx, TINYOBJ_FLAG_TRIANGULATE); + + if (result != TINYOBJ_SUCCESS) + return JS_ThrowReferenceError(js, "failed to parse OBJ file"); + + JSValue obj = JS_NewObject(js); + + // Build unified buffer with all vertex data for all shapes + // First pass: count total vertices across all shapes + size_t total_vertices = 0; + int global_has_normals = 0; + int global_has_uvs = 0; + + for (size_t si = 0; si < num_shapes; si++) { + tinyobj_shape_t *shape = &shapes[si]; + total_vertices += shape->length * 3; + + // Check if any shape has normals/uvs + for (size_t fi = 0; fi < shape->length; fi++) { + size_t face_idx = shape->face_offset + fi; + for (int vi = 0; vi < 3; vi++) { + tinyobj_vertex_index_t idx = attrib.faces[face_idx * 3 + vi]; + if (idx.vn_idx >= 0) global_has_normals = 1; + if (idx.vt_idx >= 0) global_has_uvs = 1; + } + } + } + + // 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_vertices * (total_vertices > 65535 ? sizeof(uint32_t) : sizeof(uint16_t)); + int use_32bit = (total_vertices > 65535); + + size_t total_buffer_size = pos_size + norm_size + uv_size + idx_size; + + // Allocate and fill buffer + 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; + + size_t vertex_offset = 0; + for (size_t si = 0; si < num_shapes; si++) { + tinyobj_shape_t *shape = &shapes[si]; + size_t num_faces = shape->length; + + for (size_t fi = 0; fi < num_faces; fi++) { + size_t face_idx = shape->face_offset + fi; + + for (int vi = 0; vi < 3; vi++) { + tinyobj_vertex_index_t idx = attrib.faces[face_idx * 3 + vi]; + size_t v = vertex_offset; + + // Position + int v_idx = idx.v_idx; + if (v_idx >= 0 && v_idx < (int)(attrib.num_vertices)) { + positions[v * 3 + 0] = attrib.vertices[v_idx * 3 + 0]; + positions[v * 3 + 1] = attrib.vertices[v_idx * 3 + 1]; + positions[v * 3 + 2] = attrib.vertices[v_idx * 3 + 2]; + } else { + positions[v * 3 + 0] = 0; + positions[v * 3 + 1] = 0; + positions[v * 3 + 2] = 0; + } + + // Normal + if (normals) { + int vn_idx = idx.vn_idx; + if (vn_idx >= 0 && vn_idx < (int)(attrib.num_normals)) { + normals[v * 3 + 0] = attrib.normals[vn_idx * 3 + 0]; + normals[v * 3 + 1] = attrib.normals[vn_idx * 3 + 1]; + normals[v * 3 + 2] = attrib.normals[vn_idx * 3 + 2]; + } else { + normals[v * 3 + 0] = 0; + normals[v * 3 + 1] = 1; + normals[v * 3 + 2] = 0; + } + } + + // UV + if (uvs) { + int vt_idx = idx.vt_idx; + if (vt_idx >= 0 && vt_idx < (int)(attrib.num_texcoords)) { + uvs[v * 2 + 0] = attrib.texcoords[vt_idx * 2 + 0]; + uvs[v * 2 + 1] = attrib.texcoords[vt_idx * 2 + 1]; + } else { + uvs[v * 2 + 0] = 0; + uvs[v * 2 + 1] = 0; + } + } + + // Index + if (use_32bit) + ((uint32_t *)indices)[v] = (uint32_t)v; + else + ((uint16_t *)indices)[v] = (uint16_t)v; + + vertex_offset++; + } + } + } + + // Create buffer + JSValue buffers_arr = JS_NewArray(js); + JSValue buf = JS_NewObject(js); + JS_SetPropertyStr(js, buf, "blob", js_new_blob_stoned_copy(js, buffer_data, total_buffer_size)); + JS_SetPropertyStr(js, buf, "byte_length", JS_NewInt64(js, total_buffer_size)); + JS_SetPropertyUint32(js, buffers_arr, 0, buf); + JS_SetPropertyStr(js, obj, "buffers", buffers_arr); + + // Create views + JSValue views_arr = JS_NewArray(js); + int view_idx = 0; + + // Position view + JSValue pos_view = JS_NewObject(js); + JS_SetPropertyStr(js, pos_view, "buffer", JS_NewInt32(js, 0)); + JS_SetPropertyStr(js, pos_view, "byte_offset", JS_NewInt64(js, 0)); + JS_SetPropertyStr(js, pos_view, "byte_length", JS_NewInt64(js, pos_size)); + JS_SetPropertyStr(js, pos_view, "byte_stride", JS_NULL); + JS_SetPropertyStr(js, pos_view, "usage", JS_NewString(js, "vertex")); + JS_SetPropertyUint32(js, views_arr, view_idx++, pos_view); + int pos_view_idx = 0; + + int norm_view_idx = -1; + if (global_has_normals) { + JSValue norm_view = JS_NewObject(js); + JS_SetPropertyStr(js, norm_view, "buffer", JS_NewInt32(js, 0)); + JS_SetPropertyStr(js, norm_view, "byte_offset", JS_NewInt64(js, pos_size)); + JS_SetPropertyStr(js, norm_view, "byte_length", JS_NewInt64(js, norm_size)); + JS_SetPropertyStr(js, norm_view, "byte_stride", JS_NULL); + JS_SetPropertyStr(js, norm_view, "usage", JS_NewString(js, "vertex")); + norm_view_idx = view_idx; + JS_SetPropertyUint32(js, views_arr, view_idx++, norm_view); + } + + int uv_view_idx = -1; + if (global_has_uvs) { + JSValue uv_view = JS_NewObject(js); + JS_SetPropertyStr(js, uv_view, "buffer", JS_NewInt32(js, 0)); + JS_SetPropertyStr(js, uv_view, "byte_offset", JS_NewInt64(js, pos_size + norm_size)); + JS_SetPropertyStr(js, uv_view, "byte_length", JS_NewInt64(js, uv_size)); + JS_SetPropertyStr(js, uv_view, "byte_stride", JS_NULL); + JS_SetPropertyStr(js, uv_view, "usage", JS_NewString(js, "vertex")); + uv_view_idx = view_idx; + JS_SetPropertyUint32(js, views_arr, view_idx++, uv_view); + } + + // Index view + JSValue idx_view = JS_NewObject(js); + JS_SetPropertyStr(js, idx_view, "buffer", JS_NewInt32(js, 0)); + JS_SetPropertyStr(js, idx_view, "byte_offset", JS_NewInt64(js, pos_size + norm_size + uv_size)); + JS_SetPropertyStr(js, idx_view, "byte_length", JS_NewInt64(js, idx_size)); + JS_SetPropertyStr(js, idx_view, "byte_stride", JS_NULL); + JS_SetPropertyStr(js, idx_view, "usage", JS_NewString(js, "index")); + int idx_view_idx = view_idx; + JS_SetPropertyUint32(js, views_arr, view_idx++, idx_view); + + JS_SetPropertyStr(js, obj, "views", views_arr); + + // Create accessors + JSValue accessors_arr = JS_NewArray(js); + int acc_idx = 0; + + // Position accessor + JSValue pos_acc = JS_NewObject(js); + JS_SetPropertyStr(js, pos_acc, "view", JS_NewInt32(js, pos_view_idx)); + JS_SetPropertyStr(js, pos_acc, "byte_offset", JS_NewInt64(js, 0)); + JS_SetPropertyStr(js, pos_acc, "count", JS_NewInt64(js, total_vertices)); + JS_SetPropertyStr(js, pos_acc, "component_type", JS_NewString(js, "f32")); + JS_SetPropertyStr(js, pos_acc, "type", JS_NewString(js, "vec3")); + JS_SetPropertyStr(js, pos_acc, "normalized", JS_FALSE); + JS_SetPropertyStr(js, pos_acc, "min", JS_NULL); + JS_SetPropertyStr(js, pos_acc, "max", JS_NULL); + int pos_acc_idx = acc_idx; + JS_SetPropertyUint32(js, accessors_arr, acc_idx++, pos_acc); + + int norm_acc_idx = -1; + if (global_has_normals) { + JSValue norm_acc = JS_NewObject(js); + JS_SetPropertyStr(js, norm_acc, "view", JS_NewInt32(js, norm_view_idx)); + JS_SetPropertyStr(js, norm_acc, "byte_offset", JS_NewInt64(js, 0)); + JS_SetPropertyStr(js, norm_acc, "count", JS_NewInt64(js, total_vertices)); + JS_SetPropertyStr(js, norm_acc, "component_type", JS_NewString(js, "f32")); + JS_SetPropertyStr(js, norm_acc, "type", JS_NewString(js, "vec3")); + JS_SetPropertyStr(js, norm_acc, "normalized", JS_FALSE); + JS_SetPropertyStr(js, norm_acc, "min", JS_NULL); + JS_SetPropertyStr(js, norm_acc, "max", JS_NULL); + norm_acc_idx = acc_idx; + JS_SetPropertyUint32(js, accessors_arr, acc_idx++, norm_acc); + } + + int uv_acc_idx = -1; + if (global_has_uvs) { + JSValue uv_acc = JS_NewObject(js); + JS_SetPropertyStr(js, uv_acc, "view", JS_NewInt32(js, uv_view_idx)); + JS_SetPropertyStr(js, uv_acc, "byte_offset", JS_NewInt64(js, 0)); + JS_SetPropertyStr(js, uv_acc, "count", JS_NewInt64(js, total_vertices)); + JS_SetPropertyStr(js, uv_acc, "component_type", JS_NewString(js, "f32")); + JS_SetPropertyStr(js, uv_acc, "type", JS_NewString(js, "vec2")); + JS_SetPropertyStr(js, uv_acc, "normalized", JS_FALSE); + JS_SetPropertyStr(js, uv_acc, "min", JS_NULL); + JS_SetPropertyStr(js, uv_acc, "max", JS_NULL); + uv_acc_idx = acc_idx; + JS_SetPropertyUint32(js, accessors_arr, acc_idx++, uv_acc); + } + + // Index accessor + JSValue idx_acc = JS_NewObject(js); + JS_SetPropertyStr(js, idx_acc, "view", JS_NewInt32(js, idx_view_idx)); + JS_SetPropertyStr(js, idx_acc, "byte_offset", JS_NewInt64(js, 0)); + JS_SetPropertyStr(js, idx_acc, "count", JS_NewInt64(js, total_vertices)); + JS_SetPropertyStr(js, idx_acc, "component_type", JS_NewString(js, use_32bit ? "u32" : "u16")); + JS_SetPropertyStr(js, idx_acc, "type", JS_NewString(js, "scalar")); + JS_SetPropertyStr(js, idx_acc, "normalized", JS_FALSE); + JS_SetPropertyStr(js, idx_acc, "min", JS_NULL); + JS_SetPropertyStr(js, idx_acc, "max", JS_NULL); + int idx_acc_idx = acc_idx; + JS_SetPropertyUint32(js, accessors_arr, acc_idx++, idx_acc); + + JS_SetPropertyStr(js, obj, "accessors", accessors_arr); + + // Create meshes - each shape becomes a mesh with one primitive + // But we need per-shape accessors since they reference different ranges + // Actually, for OBJ we'll create per-shape accessors that point to subranges + + // Rebuild accessors per shape + JS_FreeValue(js, accessors_arr); + accessors_arr = JS_NewArray(js); + acc_idx = 0; + + JSValue meshes_arr = JS_NewArray(js); + vertex_offset = 0; + + for (size_t si = 0; si < num_shapes; si++) { + tinyobj_shape_t *shape = &shapes[si]; + size_t shape_vertices = shape->length * 3; + + // Create accessors for this shape + int shape_pos_acc = acc_idx; + JSValue spa = JS_NewObject(js); + JS_SetPropertyStr(js, spa, "view", JS_NewInt32(js, pos_view_idx)); + JS_SetPropertyStr(js, spa, "byte_offset", JS_NewInt64(js, vertex_offset * 3 * sizeof(float))); + JS_SetPropertyStr(js, spa, "count", JS_NewInt64(js, shape_vertices)); + JS_SetPropertyStr(js, spa, "component_type", JS_NewString(js, "f32")); + JS_SetPropertyStr(js, spa, "type", JS_NewString(js, "vec3")); + JS_SetPropertyStr(js, spa, "normalized", JS_FALSE); + JS_SetPropertyStr(js, spa, "min", JS_NULL); + JS_SetPropertyStr(js, spa, "max", JS_NULL); + JS_SetPropertyUint32(js, accessors_arr, acc_idx++, spa); + + int shape_norm_acc = -1; + if (global_has_normals) { + shape_norm_acc = acc_idx; + JSValue sna = JS_NewObject(js); + JS_SetPropertyStr(js, sna, "view", JS_NewInt32(js, norm_view_idx)); + JS_SetPropertyStr(js, sna, "byte_offset", JS_NewInt64(js, vertex_offset * 3 * sizeof(float))); + JS_SetPropertyStr(js, sna, "count", JS_NewInt64(js, shape_vertices)); + JS_SetPropertyStr(js, sna, "component_type", JS_NewString(js, "f32")); + JS_SetPropertyStr(js, sna, "type", JS_NewString(js, "vec3")); + JS_SetPropertyStr(js, sna, "normalized", JS_FALSE); + JS_SetPropertyStr(js, sna, "min", JS_NULL); + JS_SetPropertyStr(js, sna, "max", JS_NULL); + JS_SetPropertyUint32(js, accessors_arr, acc_idx++, sna); + } + + int shape_uv_acc = -1; + if (global_has_uvs) { + shape_uv_acc = acc_idx; + JSValue sua = JS_NewObject(js); + JS_SetPropertyStr(js, sua, "view", JS_NewInt32(js, uv_view_idx)); + JS_SetPropertyStr(js, sua, "byte_offset", JS_NewInt64(js, vertex_offset * 2 * sizeof(float))); + JS_SetPropertyStr(js, sua, "count", JS_NewInt64(js, shape_vertices)); + JS_SetPropertyStr(js, sua, "component_type", JS_NewString(js, "f32")); + JS_SetPropertyStr(js, sua, "type", JS_NewString(js, "vec2")); + JS_SetPropertyStr(js, sua, "normalized", JS_FALSE); + JS_SetPropertyStr(js, sua, "min", JS_NULL); + JS_SetPropertyStr(js, sua, "max", JS_NULL); + JS_SetPropertyUint32(js, accessors_arr, acc_idx++, sua); + } + + int shape_idx_acc = acc_idx; + JSValue sia = JS_NewObject(js); + JS_SetPropertyStr(js, sia, "view", JS_NewInt32(js, idx_view_idx)); + JS_SetPropertyStr(js, sia, "byte_offset", JS_NewInt64(js, vertex_offset * (use_32bit ? sizeof(uint32_t) : sizeof(uint16_t)))); + JS_SetPropertyStr(js, sia, "count", JS_NewInt64(js, shape_vertices)); + JS_SetPropertyStr(js, sia, "component_type", JS_NewString(js, use_32bit ? "u32" : "u16")); + JS_SetPropertyStr(js, sia, "type", JS_NewString(js, "scalar")); + JS_SetPropertyStr(js, sia, "normalized", JS_FALSE); + JS_SetPropertyStr(js, sia, "min", JS_NULL); + JS_SetPropertyStr(js, sia, "max", JS_NULL); + JS_SetPropertyUint32(js, accessors_arr, acc_idx++, sia); + + // Create mesh + JSValue mesh = JS_NewObject(js); + JS_SetPropertyStr(js, mesh, "name", shape->name ? JS_NewString(js, shape->name) : JS_NULL); + + JSValue prims_arr = JS_NewArray(js); + JSValue prim = JS_NewObject(js); + JS_SetPropertyStr(js, prim, "topology", JS_NewString(js, "triangles")); + + JSValue attrs = JS_NewObject(js); + JS_SetPropertyStr(js, attrs, "POSITION", JS_NewInt32(js, shape_pos_acc)); + if (shape_norm_acc >= 0) + JS_SetPropertyStr(js, attrs, "NORMAL", JS_NewInt32(js, shape_norm_acc)); + if (shape_uv_acc >= 0) + JS_SetPropertyStr(js, attrs, "TEXCOORD_0", JS_NewInt32(js, shape_uv_acc)); + JS_SetPropertyStr(js, prim, "attributes", attrs); + + JS_SetPropertyStr(js, prim, "indices", JS_NewInt32(js, shape_idx_acc)); + JS_SetPropertyStr(js, prim, "material", JS_NULL); + + JS_SetPropertyUint32(js, prims_arr, 0, prim); + JS_SetPropertyStr(js, mesh, "primitives", prims_arr); + + JS_SetPropertyUint32(js, meshes_arr, si, mesh); + vertex_offset += shape_vertices; + } + + JS_SetPropertyStr(js, obj, "accessors", accessors_arr); + JS_SetPropertyStr(js, obj, "meshes", meshes_arr); + + // Materials from OBJ + JSValue materials_arr = JS_NewArray(js); + for (size_t i = 0; i < num_materials; i++) { + tinyobj_material_t *mat = &materials[i]; + JSValue m = JS_NewObject(js); + JS_SetPropertyStr(js, m, "name", mat->name ? JS_NewString(js, mat->name) : JS_NULL); + + JSValue pbr = JS_NewObject(js); + float bc[4] = {mat->diffuse[0], mat->diffuse[1], mat->diffuse[2], 1.0f}; + JS_SetPropertyStr(js, pbr, "base_color_factor", make_float_array(js, bc, 4)); + JS_SetPropertyStr(js, pbr, "base_color_texture", JS_NULL); + JS_SetPropertyStr(js, pbr, "metallic_factor", JS_NewFloat64(js, 0.0)); + JS_SetPropertyStr(js, pbr, "roughness_factor", JS_NewFloat64(js, 1.0)); + JS_SetPropertyStr(js, pbr, "metallic_roughness_texture", JS_NULL); + JS_SetPropertyStr(js, pbr, "normal_texture", JS_NULL); + JS_SetPropertyStr(js, pbr, "occlusion_texture", JS_NULL); + float ef[3] = {mat->emission[0], mat->emission[1], mat->emission[2]}; + JS_SetPropertyStr(js, pbr, "emissive_factor", make_float_array(js, ef, 3)); + JS_SetPropertyStr(js, pbr, "emissive_texture", JS_NULL); + JS_SetPropertyStr(js, m, "pbr", pbr); + + JS_SetPropertyStr(js, m, "alpha_mode", JS_NewString(js, "OPAQUE")); + JS_SetPropertyStr(js, m, "alpha_cutoff", JS_NewFloat64(js, 0.5)); + JS_SetPropertyStr(js, m, "double_sided", JS_FALSE); + + JS_SetPropertyUint32(js, materials_arr, i, m); + } + JS_SetPropertyStr(js, obj, "materials", materials_arr); + + // Empty arrays for unsupported features + JS_SetPropertyStr(js, obj, "images", JS_NewArray(js)); + JS_SetPropertyStr(js, obj, "textures", JS_NewArray(js)); + JS_SetPropertyStr(js, obj, "samplers", JS_NewArray(js)); + + // Nodes - one node per mesh + JSValue nodes_arr = JS_NewArray(js); + JSValue scene_nodes = JS_NewArray(js); + for (size_t i = 0; i < num_shapes; i++) { + JSValue n = JS_NewObject(js); + JS_SetPropertyStr(js, n, "name", shapes[i].name ? JS_NewString(js, shapes[i].name) : JS_NULL); + JS_SetPropertyStr(js, n, "mesh", JS_NewInt32(js, i)); + JS_SetPropertyStr(js, n, "children", JS_NewArray(js)); + JS_SetPropertyStr(js, n, "matrix", JS_NULL); + float t[3] = {0, 0, 0}; + float r[4] = {0, 0, 0, 1}; + float s[3] = {1, 1, 1}; + JS_SetPropertyStr(js, n, "translation", make_float_array(js, t, 3)); + JS_SetPropertyStr(js, n, "rotation", make_float_array(js, r, 4)); + JS_SetPropertyStr(js, n, "scale", make_float_array(js, s, 3)); + JS_SetPropertyStr(js, n, "skin", JS_NULL); + JS_SetPropertyUint32(js, nodes_arr, i, n); + JS_SetPropertyUint32(js, scene_nodes, i, JS_NewInt32(js, i)); + } + JS_SetPropertyStr(js, obj, "nodes", nodes_arr); + + // Scenes + JSValue scenes_arr = JS_NewArray(js); + JSValue scene = JS_NewObject(js); + JS_SetPropertyStr(js, scene, "nodes", scene_nodes); + JS_SetPropertyUint32(js, scenes_arr, 0, scene); + JS_SetPropertyStr(js, obj, "scenes", scenes_arr); + JS_SetPropertyStr(js, obj, "scene", JS_NewInt32(js, 0)); + + // Empty arrays for animations/skins + JS_SetPropertyStr(js, obj, "animations", JS_NewArray(js)); + JS_SetPropertyStr(js, obj, "skins", JS_NewArray(js)); + + // Extensions + JSValue exts = JS_NewObject(js); + JS_SetPropertyStr(js, exts, "used", JS_NewArray(js)); + JS_SetPropertyStr(js, exts, "required", JS_NewArray(js)); + JS_SetPropertyStr(js, obj, "extensions", exts); + + free(buffer_data); + tinyobj_attrib_free(&attrib); + tinyobj_shapes_free(shapes, num_shapes); + tinyobj_materials_free(materials, num_materials); + + return obj; +} + +static const JSCFunctionListEntry js_obj_funcs[] = { + MIST_FUNC_DEF(obj, decode, 1), +}; + +CELL_USE_FUNCS(js_obj_funcs) diff --git a/tests/readout.ce b/tests/readout.ce new file mode 100644 index 0000000..250f652 --- /dev/null +++ b/tests/readout.ce @@ -0,0 +1,181 @@ +var gltf = use('gltf') +var obj = use('obj') +var fbx = use('fbx') +var fs = use('cellfs') + +if (args.length < 1) { + log.console("Usage: cell run tests/readout.ce ") + log.console("Supported formats: .gltf, .glb, .obj, .fbx") + $_.stop() +} + +var filepath = args[0] +var dot = filepath.lastIndexOf('.') +var ext = dot >= 0 ? filepath.slice(dot).toLowerCase() : '' + +var data = fs.slurp(filepath) +if (!data) { + log.console(`Error: Could not read file: ${filepath}`) + $_.stop() +} + +var model = null +if (ext == '.gltf' || ext == '.glb') { + model = gltf.decode(data) +} else if (ext == '.obj') { + model = obj.decode(data) +} else if (ext == '.fbx') { + model = fbx.decode(data) +} else { + log.console(`Error: Unsupported file format: ${ext}`) + log.console("Supported formats: .gltf, .glb, .obj, .fbx") + $_.stop() +} + +def print_array = function(arr, indent) { + if (!arr || arr.length == 0) { + log.console(`${indent}(empty)`) + return + } + for (var i = 0; i < arr.length; i++) { + var item = arr[i] + if (typeof item == 'object' && item != null) { + log.console(`${indent}[${i}]:`) + print_object(item, indent + " ") + } else { + log.console(`${indent}[${i}]: ${item}`) + } + } +} + +def print_object = function(obj, indent) { + if (!indent) indent = "" + var keys = Object.keys(obj) + for (var i = 0; i < keys.length; i++) { + var key = keys[i] + var val = obj[key] + if (val == null) { + log.console(`${indent}${key}: null`) + } else if (typeof val == 'object') { + if (val.constructor && val.constructor.name == 'Blob') { + log.console(`${indent}${key}: `) + } else if (Array.isArray(val)) { + if (val.length == 0) { + log.console(`${indent}${key}: []`) + } else if (val.length <= 4 && typeof val[0] != 'object') { + log.console(`${indent}${key}: [${val.join(', ')}]`) + } else if (typeof val[0] != 'object') { + log.console(`${indent}${key}: [${val.slice(0, 4).join(', ')}...] (${val.length} items)`) + } else { + log.console(`${indent}${key}: (${val.length} items)`) + print_array(val, indent + " ") + } + } else { + log.console(`${indent}${key}:`) + print_object(val, indent + " ") + } + } else { + log.console(`${indent}${key}: ${val}`) + } + } +} + +log.console(`\n=== Model Structure: ${filepath} ===\n`) + +log.console(`Buffers: ${model.buffers.length}`) +for (var i = 0; i < model.buffers.length; i++) { + var buf = model.buffers[i] + log.console(` [${i}] byte_length: ${buf.byte_length}`) +} + +log.console(`\nViews: ${model.views.length}`) +for (var i = 0; i < model.views.length; i++) { + var v = model.views[i] + log.console(` [${i}] buffer: ${v.buffer}, offset: ${v.byte_offset}, length: ${v.byte_length}, stride: ${v.byte_stride}, usage: ${v.usage}`) +} + +log.console(`\nAccessors: ${model.accessors.length}`) +for (var i = 0; i < model.accessors.length; i++) { + var a = model.accessors[i] + log.console(` [${i}] view: ${a.view}, offset: ${a.byte_offset}, count: ${a.count}, type: ${a.component_type}/${a.type}, normalized: ${a.normalized}`) +} + +log.console(`\nMeshes: ${model.meshes.length}`) +for (var i = 0; i < model.meshes.length; i++) { + var m = model.meshes[i] + log.console(` [${i}] name: ${m.name}`) + for (var j = 0; j < m.primitives.length; j++) { + var p = m.primitives[j] + log.console(` primitive[${j}]: topology: ${p.topology}, indices: ${p.indices}, material: ${p.material}`) + var ak = Object.keys(p.attributes) + var parts = [] + for (var k = 0; k < ak.length; k++) parts.push(ak[k] + ': ' + text(p.attributes[ak[k]])) + log.console(' attributes: {' + parts.join(', ') + '}') + } +} + +log.console(`\nMaterials: ${model.materials.length}`) +for (var i = 0; i < model.materials.length; i++) { + var m = model.materials[i] + log.console(` [${i}] name: ${m.name}, alpha_mode: ${m.alpha_mode}, double_sided: ${m.double_sided}`) + if (m.pbr) { + log.console(` pbr: base_color: [${m.pbr.base_color_factor}], metallic: ${m.pbr.metallic_factor}, roughness: ${m.pbr.roughness_factor}`) + } +} + +log.console(`\nImages: ${model.images.length}`) +for (var i = 0; i < model.images.length; i++) { + var im = model.images[i] + log.console(` [${i}] kind: ${im.kind}, ${im.kind == 'uri' ? 'uri: ' + im.uri : 'view: ' + im.view}, mime: ${im.mime}`) +} + +log.console(`\nTextures: ${model.textures.length}`) +for (var i = 0; i < model.textures.length; i++) { + var t = model.textures[i] + log.console(` [${i}] image: ${t.image}, sampler: ${t.sampler}`) +} + +log.console(`\nSamplers: ${model.samplers.length}`) +for (var i = 0; i < model.samplers.length; i++) { + var s = model.samplers[i] + log.console(` [${i}] min: ${s.min_filter}, mag: ${s.mag_filter}, wrap_s: ${s.wrap_s}, wrap_t: ${s.wrap_t}`) +} + +log.console(`\nNodes: ${model.nodes.length}`) +for (var i = 0; i < model.nodes.length; i++) { + var n = model.nodes[i] + log.console(` [${i}] name: ${n.name}, mesh: ${n.mesh}, children: [${n.children}], skin: ${n.skin}`) + if (n.matrix) { + log.console(` matrix: [${n.matrix.slice(0, 4)}...]`) + } else { + log.console(` T: [${n.translation}], R: [${n.rotation}], S: [${n.scale}]`) + } +} + +log.console(`\nScenes: ${model.scenes.length}, default: ${model.scene}`) +for (var i = 0; i < model.scenes.length; i++) { + var s = model.scenes[i] + log.console(` [${i}] nodes: [${s.nodes}]`) +} + +log.console(`\nAnimations: ${model.animations.length}`) +for (var i = 0; i < model.animations.length; i++) { + var a = model.animations[i] + log.console(` [${i}] name: ${a.name}, samplers: ${a.samplers.length}, channels: ${a.channels.length}`) +} + +log.console(`\nSkins: ${model.skins.length}`) +for (var i = 0; i < model.skins.length; i++) { + var s = model.skins[i] + log.console(` [${i}] name: ${s.name}, joints: ${s.joints.length}, ibm: ${s.inverse_bind_matrices}, skeleton: ${s.skeleton}`) +} + +log.console(`\nExtensions:`) +log.console(` used: [${model.extensions.used}]`) +log.console(` required: [${model.extensions.required}]`) + +log.console(`\n=== End ===\n`) + +log.console(json.encode(model)) + +$_.stop()