/* * loader.c — tinygltf v3 C WASM bridge for the three.js demo. * * Parses glTF/GLB bytes passed in from JavaScript (Emscripten HEAPU8), * then exposes flattened per-primitive vertex/index data plus materials, * textures and the node hierarchy through EMSCRIPTEN_KEEPALIVE functions. * * Build with emcc (see Makefile). No filesystem or image decoding is * used: all assets must be embedded in the file (GLB chunk or data URI). */ #include #include #include #include #include "tiny_gltf_v3.h" #if defined(__EMSCRIPTEN__) #include #define TG3W_EXPORT EMSCRIPTEN_KEEPALIVE #else #define TG3W_EXPORT #endif /* ------------------------------------------------------------------ */ /* Exported model info */ /* ------------------------------------------------------------------ */ typedef struct { int32_t material; /* material index or -1 */ int32_t mode; /* TG3_MODE_* */ uint32_t vertex_count; uint32_t index_count; uint32_t pos_offset; /* offset in tg3w_positions (floats, 3 * vertex_count) */ int32_t nrm_offset; /* -1 = absent; floats, 3 * vertex_count */ int32_t uv_offset; /* -1 = absent; floats, 2 * vertex_count */ uint32_t idx_offset; /* offset in tg3w_indices (uint32) */ } tg3w_prim; typedef struct { float base_color[4]; float metallic; float roughness; int32_t base_color_texture; /* texture index or -1 */ int32_t alpha_mode; /* 0 = OPAQUE, 1 = MASK, 2 = BLEND */ float alpha_cutoff; int32_t double_sided; } tg3w_mat; typedef struct { int32_t mesh; /* mesh index or -1 */ float translation[3]; float rotation[4]; float scale[3]; } tg3w_node; /* ------------------------------------------------------------------ */ /* Module state (single parse at a time, like a simple loading screen) */ /* ------------------------------------------------------------------ */ static tg3_model g_model; static tg3_error_stack g_errors; static tg3_parse_options g_opts_; static tg3w_prim *g_prims = NULL; static uint32_t g_prim_count = 0; static uint32_t *g_mesh_prim_start = NULL; static uint32_t *g_mesh_prim_count = NULL; static float *g_positions = NULL; static float *g_normals = NULL; static float *g_uvs = NULL; static uint32_t *g_indices = NULL; static char g_last_error[512]; static int g_inited = 0; /* Free the current model and all flattened buffers. */ TG3W_EXPORT void tg3w_clear(void) { tg3_model_free(&g_model); free(g_prims); g_prims = NULL; free(g_positions); g_positions = NULL; free(g_normals); g_normals = NULL; free(g_uvs); g_uvs = NULL; free(g_indices); g_indices = NULL; free(g_mesh_prim_start); g_mesh_prim_start = NULL; free(g_mesh_prim_count); g_mesh_prim_count = NULL; g_prim_count = 0; } static void tg3w_init_once(void) { if (!g_inited) { tg3_parse_options_init(&g_opts_); tg3_error_stack_init(&g_errors); g_inited = 1; } } /* ------------------------------------------------------------------ */ /* Accessor helpers */ /* ------------------------------------------------------------------ */ static uint32_t tg3w_type_components(int32_t type) { switch (type) { case TG3_TYPE_SCALAR: return 1; case TG3_TYPE_VEC2: return 2; case TG3_TYPE_VEC3: return 3; case TG3_TYPE_VEC4: return 4; case TG3_TYPE_MAT4: return 16; default: return 0; } } static const uint8_t *tg3w_accessor_ptr(const tg3_model *m, const tg3_accessor *a, uint64_t *stride_out) { if (a->buffer_view < 0 || a->sparse.is_sparse) { return NULL; } const tg3_buffer_view *bv = &m->buffer_views[a->buffer_view]; if (bv->buffer < 0) { return NULL; } const tg3_buffer *b = &m->buffers[bv->buffer]; if (!b->data.data || b->data.count < bv->byte_offset + bv->byte_length) { return NULL; } uint32_t comps = tg3w_type_components(a->type); uint64_t elem = 0; switch (a->component_type) { case TG3_COMPONENT_TYPE_FLOAT: elem = 4; break; case TG3_COMPONENT_TYPE_DOUBLE: elem = 8; break; case TG3_COMPONENT_TYPE_UNSIGNED_BYTE: elem = 1; break; case TG3_COMPONENT_TYPE_BYTE: elem = 1; break; case TG3_COMPONENT_TYPE_UNSIGNED_SHORT: elem = 2; break; case TG3_COMPONENT_TYPE_SHORT: elem = 2; break; case TG3_COMPONENT_TYPE_UNSIGNED_INT: elem = 4; break; case TG3_COMPONENT_TYPE_INT: elem = 4; break; default: return NULL; } uint64_t stride = bv->byte_stride ? bv->byte_stride : elem * comps; *stride_out = stride; return b->data.data + bv->byte_offset + a->byte_offset; } static int tg3w_attr_index(const tg3_primitive *p, const char *name) { for (uint32_t i = 0; i < p->attributes_count; i++) { const tg3_str_int_pair *a = &p->attributes[i]; if (a->key.len == (uint32_t)strlen(name) && strncmp(a->key.data, name, a->key.len) == 0) { return a->value; } } return -1; } /* ------------------------------------------------------------------ */ /* Parse + flatten */ /* ------------------------------------------------------------------ */ TG3W_EXPORT int tg3w_parse(const uint8_t *data, uint32_t size) { tg3w_init_once(); tg3_model_free(&g_model); tg3_error_stack_free(&g_errors); tg3_error_stack_init(&g_errors); tg3_parse_options_init(&g_opts_); tg3_error_code err = tg3_parse_auto(&g_model, &g_errors, data, size, NULL, 0, &g_opts_); if (err != TG3_OK) { snprintf(g_last_error, sizeof(g_last_error), "parse failed: %d", (int)err); return (int)err; } /* Pass 1: count vertices/indices per primitive. */ uint32_t prims = 0; uint64_t total_pos = 0, total_nrm = 0, total_uv = 0, total_idx = 0; for (uint32_t mi = 0; mi < g_model.meshes_count; mi++) { const tg3_mesh *mesh = &g_model.meshes[mi]; for (uint32_t pi = 0; pi < mesh->primitives_count; pi++) { const tg3_primitive *p = &mesh->primitives[pi]; int pos_i = tg3w_attr_index(p, "POSITION"); int nrm_i = tg3w_attr_index(p, "NORMAL"); int uv_i = tg3w_attr_index(p, "TEXCOORD_0"); uint64_t vcount = 0; if (pos_i >= 0) { const tg3_accessor *a = &g_model.accessors[pos_i]; if (a->component_type == TG3_COMPONENT_TYPE_FLOAT && a->type == TG3_TYPE_VEC3 && !a->sparse.is_sparse) { vcount = a->count; } } uint64_t icount = 0; if (p->indices >= 0) { const tg3_accessor *a = &g_model.accessors[p->indices]; if (!a->sparse.is_sparse) { switch (a->component_type) { case TG3_COMPONENT_TYPE_UNSIGNED_BYTE: case TG3_COMPONENT_TYPE_UNSIGNED_SHORT: case TG3_COMPONENT_TYPE_UNSIGNED_INT: icount = a->count; break; default: break; } } } if (vcount == 0) { continue; /* unsupported primitive (e.g. sparse/double) */ } total_pos += vcount * 3; if (nrm_i >= 0) { const tg3_accessor *a = &g_model.accessors[nrm_i]; if (a->component_type == TG3_COMPONENT_TYPE_FLOAT && a->type == TG3_TYPE_VEC3 && !a->sparse.is_sparse) { total_nrm += vcount * 3; } } if (uv_i >= 0) { const tg3_accessor *a = &g_model.accessors[uv_i]; if (a->component_type == TG3_COMPONENT_TYPE_FLOAT && a->type == TG3_TYPE_VEC2 && !a->sparse.is_sparse) { total_uv += vcount * 2; } } total_idx += icount; prims++; } } if (prims == 0) { snprintf(g_last_error, sizeof(g_last_error), "no renderable primitives (float VEC3 POSITION required)"); return -1; } /* Allocate flattened arrays. */ free(g_prims); free(g_positions); free(g_normals); free(g_uvs); free(g_indices); free(g_mesh_prim_start); free(g_mesh_prim_count); g_prims = (tg3w_prim *)malloc(prims * sizeof(tg3w_prim)); g_positions = (float *)malloc(total_pos * sizeof(float)); g_normals = total_nrm ? (float *)malloc(total_nrm * sizeof(float)) : NULL; g_uvs = total_uv ? (float *)malloc(total_uv * sizeof(float)) : NULL; g_indices = (uint32_t *)malloc(total_idx * sizeof(uint32_t)); g_mesh_prim_start = (uint32_t *)malloc(g_model.meshes_count * sizeof(uint32_t)); g_mesh_prim_count = (uint32_t *)malloc(g_model.meshes_count * sizeof(uint32_t)); g_prim_count = 0; if (!g_prims || (total_pos && !g_positions) || (total_nrm && !g_normals) || (total_uv && !g_uvs) || (total_idx && !g_indices) || !g_mesh_prim_start || !g_mesh_prim_count) { snprintf(g_last_error, sizeof(g_last_error), "out of memory"); tg3w_clear(); return -1; } memset(g_mesh_prim_start, 0, g_model.meshes_count * sizeof(uint32_t)); memset(g_mesh_prim_count, 0, g_model.meshes_count * sizeof(uint32_t)); /* Pass 2: fill. */ uint32_t p_off = 0, n_off = 0, u_off = 0, i_off = 0; for (uint32_t mi = 0; mi < g_model.meshes_count; mi++) { const tg3_mesh *mesh = &g_model.meshes[mi]; g_mesh_prim_start[mi] = g_prim_count; for (uint32_t pi = 0; pi < mesh->primitives_count; pi++) { const tg3_primitive *p = &mesh->primitives[pi]; int pos_i = tg3w_attr_index(p, "POSITION"); uint64_t vcount = 0; uint64_t pstride = 0; const uint8_t *pp = NULL; if (pos_i >= 0) { const tg3_accessor *a = &g_model.accessors[pos_i]; if (a->component_type == TG3_COMPONENT_TYPE_FLOAT && a->type == TG3_TYPE_VEC3 && !a->sparse.is_sparse) { vcount = a->count; pp = tg3w_accessor_ptr(&g_model, a, &pstride); } } if (!pp || vcount == 0) { continue; } tg3w_prim *out = &g_prims[g_prim_count]; memset(out, 0, sizeof(*out)); out->nrm_offset = -1; out->uv_offset = -1; out->material = p->material; out->mode = (p->mode == -1) ? TG3_MODE_TRIANGLES : p->mode; out->vertex_count = (uint32_t)vcount; out->pos_offset = p_off; for (uint64_t v = 0; v < vcount; v++) { const float *f = (const float *)(pp + v * pstride); g_positions[p_off + (uint32_t)v * 3 + 0] = f[0]; g_positions[p_off + (uint32_t)v * 3 + 1] = f[1]; g_positions[p_off + (uint32_t)v * 3 + 2] = f[2]; } p_off += (uint32_t)vcount * 3; int nrm_i = tg3w_attr_index(p, "NORMAL"); if (nrm_i >= 0) { const tg3_accessor *a = &g_model.accessors[nrm_i]; uint64_t stride = 0; const uint8_t *np = tg3w_accessor_ptr(&g_model, a, &stride); if (np && a->component_type == TG3_COMPONENT_TYPE_FLOAT && a->type == TG3_TYPE_VEC3 && a->count == vcount) { out->nrm_offset = (int32_t)n_off; for (uint64_t v = 0; v < vcount; v++) { const float *f = (const float *)(np + v * stride); g_normals[n_off + (uint32_t)v * 3 + 0] = f[0]; g_normals[n_off + (uint32_t)v * 3 + 1] = f[1]; g_normals[n_off + (uint32_t)v * 3 + 2] = f[2]; } n_off += (uint32_t)vcount * 3; } } int uv_i = tg3w_attr_index(p, "TEXCOORD_0"); if (uv_i >= 0) { const tg3_accessor *a = &g_model.accessors[uv_i]; uint64_t stride = 0; const uint8_t *up = tg3w_accessor_ptr(&g_model, a, &stride); if (up && a->component_type == TG3_COMPONENT_TYPE_FLOAT && a->type == TG3_TYPE_VEC2 && a->count == vcount) { out->uv_offset = (int32_t)u_off; for (uint64_t v = 0; v < vcount; v++) { const float *f = (const float *)(up + v * stride); g_uvs[u_off + (uint32_t)v * 2 + 0] = f[0]; g_uvs[u_off + (uint32_t)v * 2 + 1] = f[1]; } u_off += (uint32_t)vcount * 2; } } if (p->indices >= 0) { const tg3_accessor *a = &g_model.accessors[p->indices]; uint64_t stride = 0; const uint8_t *ip = tg3w_accessor_ptr(&g_model, a, &stride); if (ip && !a->sparse.is_sparse) { out->index_count = (uint32_t)a->count; out->idx_offset = i_off; for (uint64_t v = 0; v < a->count; v++) { uint32_t idx = 0; switch (a->component_type) { case TG3_COMPONENT_TYPE_UNSIGNED_BYTE: idx = ((const uint8_t *)ip)[v * stride]; break; case TG3_COMPONENT_TYPE_UNSIGNED_SHORT: idx = ((const uint16_t *)ip)[v * stride / 2]; break; case TG3_COMPONENT_TYPE_UNSIGNED_INT: idx = ((const uint32_t *)ip)[v * stride / 4]; break; default: break; } g_indices[i_off + (uint32_t)v] = idx; } i_off += (uint32_t)a->count; } } g_prim_count++; } g_mesh_prim_count[mi] = g_prim_count - g_mesh_prim_start[mi]; } g_last_error[0] = '\0'; return 0; } /* ------------------------------------------------------------------ */ /* Getters */ /* ------------------------------------------------------------------ */ TG3W_EXPORT const char *tg3w_last_error(void) { return g_last_error; } TG3W_EXPORT uint32_t tg3w_error_count(void) { return g_errors.count; } TG3W_EXPORT const char *tg3w_error_message(uint32_t i) { if (i >= g_errors.count || !g_errors.entries[i].message) return ""; return g_errors.entries[i].message; } TG3W_EXPORT int tg3w_error_severity(uint32_t i) { if (i >= g_errors.count) return -1; return (int)g_errors.entries[i].severity; } TG3W_EXPORT uint32_t tg3w_prim_count(void) { return g_prim_count; } TG3W_EXPORT const tg3w_prim *tg3w_prim_at(uint32_t i) { if (i >= g_prim_count) return NULL; return &g_prims[i]; } TG3W_EXPORT const float *tg3w_positions(void) { return g_positions; } TG3W_EXPORT const float *tg3w_normals(void) { return g_normals; } TG3W_EXPORT const float *tg3w_uvs(void) { return g_uvs; } TG3W_EXPORT const uint32_t *tg3w_indices(void) { return g_indices; } TG3W_EXPORT uint32_t tg3w_mesh_count(void) { return g_model.meshes_count; } TG3W_EXPORT uint32_t tg3w_mesh_prim_start(uint32_t m) { if (m >= g_model.meshes_count) return 0; return g_mesh_prim_start[m]; } TG3W_EXPORT uint32_t tg3w_mesh_prim_count(uint32_t m) { if (m >= g_model.meshes_count) return 0; return g_mesh_prim_count[m]; } /* ------------------------------------------------------------------ */ /* Materials */ /* ------------------------------------------------------------------ */ TG3W_EXPORT uint32_t tg3w_material_count(void) { return g_model.materials_count; } TG3W_EXPORT const float *tg3w_material_base_color(uint32_t i) { static float c[4]; if (i >= g_model.materials_count) { c[0]=1;c[1]=1;c[2]=1;c[3]=1; return c; } const tg3_material *m = &g_model.materials[i]; for (int k = 0; k < 4; k++) c[k] = (float)m->pbr_metallic_roughness.base_color_factor[k]; return c; } TG3W_EXPORT float tg3w_material_metallic(uint32_t i) { if (i >= g_model.materials_count) return 1.0f; return (float)g_model.materials[i].pbr_metallic_roughness.metallic_factor; } TG3W_EXPORT float tg3w_material_roughness(uint32_t i) { if (i >= g_model.materials_count) return 1.0f; return (float)g_model.materials[i].pbr_metallic_roughness.roughness_factor; } TG3W_EXPORT int tg3w_material_base_color_texture(uint32_t i) { if (i >= g_model.materials_count) return -1; return g_model.materials[i].pbr_metallic_roughness.base_color_texture.index; } TG3W_EXPORT int tg3w_material_alpha_mode(uint32_t i) { if (i >= g_model.materials_count) return 0; const tg3_str *a = &g_model.materials[i].alpha_mode; if (a->len == 5 && strncmp(a->data, "BLEND", 5) == 0) return 2; if (a->len == 4 && strncmp(a->data, "MASK", 4) == 0) return 1; return 0; } TG3W_EXPORT float tg3w_material_alpha_cutoff(uint32_t i) { if (i >= g_model.materials_count) return 0.5f; return (float)g_model.materials[i].alpha_cutoff; } TG3W_EXPORT int tg3w_material_double_sided(uint32_t i) { if (i >= g_model.materials_count) return 0; return g_model.materials[i].double_sided; } /* ------------------------------------------------------------------ */ /* Textures (raw image bytes, decoded client-side) */ /* ------------------------------------------------------------------ */ TG3W_EXPORT uint32_t tg3w_texture_count(void) { return g_model.textures_count; } TG3W_EXPORT uint32_t tg3w_image_count(void) { return g_model.images_count; } TG3W_EXPORT int tg3w_texture_source(uint32_t i) { if (i >= g_model.textures_count) return -1; return g_model.textures[i].source; } /* Image payload: either a bufferView (GLB / .bin) or a data URI in the * glTF JSON. The v3 parser does not decode image bytes itself, so expose * both paths to JS. */ TG3W_EXPORT int tg3w_image_buffer_view(uint32_t i) { if (i >= g_model.images_count) return -1; return g_model.images[i].buffer_view; } TG3W_EXPORT const char *tg3w_image_uri(uint32_t i) { if (i >= g_model.images_count) return NULL; return g_model.images[i].uri.data ? g_model.images[i].uri.data : NULL; } TG3W_EXPORT const uint8_t *tg3w_image_bytes(uint32_t i) { if (i >= g_model.images_count) return NULL; int32_t bv = g_model.images[i].buffer_view; if (bv < 0 || bv >= (int32_t)g_model.buffer_views_count) return NULL; const tg3_buffer_view *v = &g_model.buffer_views[bv]; if (v->buffer < 0 || v->buffer >= (int32_t)g_model.buffers_count) return NULL; const tg3_buffer *b = &g_model.buffers[v->buffer]; if (!b->data.data || b->data.count < v->byte_offset + v->byte_length) return NULL; return b->data.data + v->byte_offset; } TG3W_EXPORT uint64_t tg3w_image_size(uint32_t i) { if (i >= g_model.images_count) return 0; int32_t bv = g_model.images[i].buffer_view; if (bv < 0 || bv >= (int32_t)g_model.buffer_views_count) return 0; return g_model.buffer_views[bv].byte_length; } TG3W_EXPORT const char *tg3w_image_mime(uint32_t i) { if (i >= g_model.images_count) return ""; return g_model.images[i].mime_type.data ? g_model.images[i].mime_type.data : ""; } /* ------------------------------------------------------------------ */ /* Scene graph (default scene only) */ /* ------------------------------------------------------------------ */ TG3W_EXPORT uint32_t tg3w_node_count(void) { return g_model.nodes_count; } TG3W_EXPORT int32_t tg3w_node_mesh(uint32_t i) { if (i >= g_model.nodes_count) return -1; return g_model.nodes[i].mesh; } TG3W_EXPORT uint32_t tg3w_node_child_count(uint32_t i) { if (i >= g_model.nodes_count) return 0; return g_model.nodes[i].children_count; } TG3W_EXPORT const int32_t *tg3w_node_children(uint32_t i) { if (i >= g_model.nodes_count) return NULL; return g_model.nodes[i].children; } TG3W_EXPORT const float *tg3w_node_trs(uint32_t i) { static float trs[10]; if (i >= g_model.nodes_count) { memset(trs, 0, sizeof(trs)); trs[6] = 1.0f; return trs; } const tg3_node *n = &g_model.nodes[i]; for (int k = 0; k < 3; k++) trs[k] = (float)n->translation[k]; for (int k = 0; k < 4; k++) trs[3 + k] = (float)n->rotation[k]; for (int k = 0; k < 3; k++) trs[7 + k] = (float)n->scale[k]; return trs; } TG3W_EXPORT int tg3w_default_scene(void) { return g_model.default_scene; } TG3W_EXPORT uint32_t tg3w_scene_count(void) { return g_model.scenes_count; } TG3W_EXPORT uint32_t tg3w_scene_node_count(uint32_t i) { if (i >= g_model.scenes_count) return 0; return g_model.scenes[i].nodes_count; } TG3W_EXPORT const int32_t *tg3w_scene_nodes(uint32_t i) { if (i >= g_model.scenes_count) return NULL; return g_model.scenes[i].nodes; }