Files
tinygltf/web/loader.c
Syoyo Fujita a040827363 Add Emscripten WASM build and three.js web demo
web/ compiles the v3 C runtime with emcc (Emscripten SDK from
~/work/emsdk by default) and ships a simple browser demo that loads
glTF/GLB files via drag & drop and renders them with three.js:

- loader.c: C bridge exporting flattened primitives, materials,
  textures (bufferView + data-URI paths) and the scene graph to JS
- main.js/index.html/style.css: three.js viewer (OrbitControls,
  PBR materials, textures, node hierarchy, primitive modes)
- gen_sample.py: generates a small self-contained textured Cube.glb
  sample from tracked assets (no large binaries committed)
- Makefile: emcc build (MODULARIZE + ES6, no filesystem), sample
  generation and local http server targets

Verified end-to-end: native bridge tests, Node harness exercising the
WASM exports (parse, geometry, images, clear) and HTTP serving.
2026-07-31 21:42:31 +09:00

549 lines
21 KiB
C

/*
* 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 <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "tiny_gltf_v3.h"
#if defined(__EMSCRIPTEN__)
#include <emscripten.h>
#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;
}