Files
cell-model/internal/gltf.c
2026-02-25 23:26:12 -06:00

465 lines
19 KiB
C

#include "cell.h"
#include <string.h>
#include <stdlib.h>
#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)
{
JS_FRAME(js);
JS_ROOT(a, JS_NewArray(js));
for (int i = 0; i < count; i++)
JS_SetPropertyNumber(js, a.val, i, JS_NewFloat64(js, arr[i]));
JS_RETURN(a.val);
}
static JSValue make_texture_info(JSContext *js, cgltf_texture_view *tv, cgltf_data *data)
{
if (!tv->texture) return JS_NULL;
JS_FRAME(js);
JS_ROOT(o, JS_NewObject(js));
JS_SetPropertyStr(js, o.val, "texture", JS_NewInt32(js, (int)(tv->texture - data->textures)));
JS_SetPropertyStr(js, o.val, "texcoord", JS_NewInt32(js, tv->texcoord));
if (tv->has_transform) {
JSValue t;
JS_ROOT(tr, JS_NewObject(js));
t = make_float_array(js, tv->transform.offset, 2);
JS_SetPropertyStr(js, tr.val, "offset", t);
t = make_float_array(js, tv->transform.scale, 2);
JS_SetPropertyStr(js, tr.val, "scale", t);
JS_SetPropertyStr(js, tr.val, "rotation", JS_NewFloat64(js, tv->transform.rotation));
JS_SetPropertyStr(js, o.val, "transform", tr.val);
}
JS_RETURN(o.val);
}
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);
}
JS_FRAME(js);
JS_ROOT(obj, JS_NewObject(js));
// Buffers
JSValue tmp;
JS_ROOT(buffers_arr, JS_NewArray(js));
for (cgltf_size i = 0; i < data->buffers_count; i++) {
cgltf_buffer *buf = &data->buffers[i];
JS_ROOT(b, JS_NewObject(js));
if (buf->data && buf->size > 0) {
tmp = js_new_blob_stoned_copy(js, buf->data, buf->size);
JS_SetPropertyStr(js, b.val, "blob", tmp);
} else {
JS_SetPropertyStr(js, b.val, "blob", JS_NULL);
}
JS_SetPropertyStr(js, b.val, "byte_length", JS_NewInt64(js, buf->size));
JS_SetPropertyNumber(js, buffers_arr.val, i, b.val);
}
JS_SetPropertyStr(js, obj.val, "buffers", buffers_arr.val);
// Buffer views
JS_ROOT(views_arr, JS_NewArray(js));
for (cgltf_size i = 0; i < data->buffer_views_count; i++) {
cgltf_buffer_view *bv = &data->buffer_views[i];
JS_ROOT(v, JS_NewObject(js));
JS_SetPropertyStr(js, v.val, "buffer", JS_NewInt32(js, (int)(bv->buffer - data->buffers)));
JS_SetPropertyStr(js, v.val, "byte_offset", JS_NewInt64(js, bv->offset));
JS_SetPropertyStr(js, v.val, "byte_length", JS_NewInt64(js, bv->size));
JS_SetPropertyStr(js, v.val, "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";
tmp = JS_NewString(js, usage);
JS_SetPropertyStr(js, v.val, "usage", tmp);
JS_SetPropertyNumber(js, views_arr.val, i, v.val);
}
JS_SetPropertyStr(js, obj.val, "views", views_arr.val);
// Accessors
JS_ROOT(accessors_arr, JS_NewArray(js));
for (cgltf_size i = 0; i < data->accessors_count; i++) {
cgltf_accessor *acc = &data->accessors[i];
JS_ROOT(a, JS_NewObject(js));
if (acc->buffer_view)
JS_SetPropertyStr(js, a.val, "view", JS_NewInt32(js, (int)(acc->buffer_view - data->buffer_views)));
else
JS_SetPropertyStr(js, a.val, "view", JS_NULL);
JS_SetPropertyStr(js, a.val, "byte_offset", JS_NewInt64(js, acc->offset));
JS_SetPropertyStr(js, a.val, "count", JS_NewInt64(js, acc->count));
tmp = JS_NewString(js, component_type_str(acc->component_type));
JS_SetPropertyStr(js, a.val, "component_type", tmp);
tmp = JS_NewString(js, type_str(acc->type));
JS_SetPropertyStr(js, a.val, "type", tmp);
JS_SetPropertyStr(js, a.val, "normalized", JS_NewBool(js, acc->normalized));
if (acc->has_min) {
int n = cgltf_num_components(acc->type);
tmp = make_float_array(js, acc->min, n);
JS_SetPropertyStr(js, a.val, "min", tmp);
} else {
JS_SetPropertyStr(js, a.val, "min", JS_NULL);
}
if (acc->has_max) {
int n = cgltf_num_components(acc->type);
tmp = make_float_array(js, acc->max, n);
JS_SetPropertyStr(js, a.val, "max", tmp);
} else {
JS_SetPropertyStr(js, a.val, "max", JS_NULL);
}
JS_SetPropertyNumber(js, accessors_arr.val, i, a.val);
}
JS_SetPropertyStr(js, obj.val, "accessors", accessors_arr.val);
// Meshes
JS_ROOT(meshes_arr, JS_NewArray(js));
for (cgltf_size mi = 0; mi < data->meshes_count; mi++) {
cgltf_mesh *mesh = &data->meshes[mi];
JS_ROOT(m, JS_NewObject(js));
tmp = mesh->name ? JS_NewString(js, mesh->name) : JS_NULL;
JS_SetPropertyStr(js, m.val, "name", tmp);
JS_ROOT(prims_arr, JS_NewArray(js));
for (cgltf_size pi = 0; pi < mesh->primitives_count; pi++) {
cgltf_primitive *prim = &mesh->primitives[pi];
JS_ROOT(p, JS_NewObject(js));
tmp = JS_NewString(js, topology_str(prim->type));
JS_SetPropertyStr(js, p.val, "topology", tmp);
JS_ROOT(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.val, name, JS_NewInt32(js, (int)(attr->data - data->accessors)));
}
JS_SetPropertyStr(js, p.val, "attributes", attrs.val);
if (prim->indices)
JS_SetPropertyStr(js, p.val, "indices", JS_NewInt32(js, (int)(prim->indices - data->accessors)));
else
JS_SetPropertyStr(js, p.val, "indices", JS_NULL);
if (prim->material)
JS_SetPropertyStr(js, p.val, "material", JS_NewInt32(js, (int)(prim->material - data->materials)));
else
JS_SetPropertyStr(js, p.val, "material", JS_NULL);
JS_SetPropertyNumber(js, prims_arr.val, pi, p.val);
}
JS_SetPropertyStr(js, m.val, "primitives", prims_arr.val);
JS_SetPropertyNumber(js, meshes_arr.val, mi, m.val);
}
JS_SetPropertyStr(js, obj.val, "meshes", meshes_arr.val);
// Images
JS_ROOT(images_arr, JS_NewArray(js));
for (cgltf_size i = 0; i < data->images_count; i++) {
cgltf_image *img = &data->images[i];
JS_ROOT(im, JS_NewObject(js));
if (img->buffer_view) {
tmp = JS_NewString(js, "buffer_view");
JS_SetPropertyStr(js, im.val, "kind", tmp);
JS_SetPropertyStr(js, im.val, "view", JS_NewInt32(js, (int)(img->buffer_view - data->buffer_views)));
} else if (img->uri) {
tmp = JS_NewString(js, "uri");
JS_SetPropertyStr(js, im.val, "kind", tmp);
tmp = JS_NewString(js, img->uri);
JS_SetPropertyStr(js, im.val, "uri", tmp);
}
tmp = img->mime_type ? JS_NewString(js, img->mime_type) : JS_NULL;
JS_SetPropertyStr(js, im.val, "mime", tmp);
JS_SetPropertyNumber(js, images_arr.val, i, im.val);
}
JS_SetPropertyStr(js, obj.val, "images", images_arr.val);
// Textures
JS_ROOT(textures_arr, JS_NewArray(js));
for (cgltf_size i = 0; i < data->textures_count; i++) {
cgltf_texture *tex = &data->textures[i];
JS_ROOT(t, JS_NewObject(js));
JS_SetPropertyStr(js, t.val, "image", tex->image ? JS_NewInt32(js, (int)(tex->image - data->images)) : JS_NULL);
JS_SetPropertyStr(js, t.val, "sampler", tex->sampler ? JS_NewInt32(js, (int)(tex->sampler - data->samplers)) : JS_NULL);
JS_SetPropertyNumber(js, textures_arr.val, i, t.val);
}
JS_SetPropertyStr(js, obj.val, "textures", textures_arr.val);
// Samplers
JS_ROOT(samplers_arr, JS_NewArray(js));
for (cgltf_size i = 0; i < data->samplers_count; i++) {
cgltf_sampler *samp = &data->samplers[i];
JS_ROOT(s, JS_NewObject(js));
JS_SetPropertyStr(js, s.val, "min_filter", JS_NewInt32(js, samp->min_filter));
JS_SetPropertyStr(js, s.val, "mag_filter", JS_NewInt32(js, samp->mag_filter));
JS_SetPropertyStr(js, s.val, "wrap_s", JS_NewInt32(js, samp->wrap_s));
JS_SetPropertyStr(js, s.val, "wrap_t", JS_NewInt32(js, samp->wrap_t));
JS_SetPropertyNumber(js, samplers_arr.val, i, s.val);
}
JS_SetPropertyStr(js, obj.val, "samplers", samplers_arr.val);
// Materials
JS_ROOT(materials_arr, JS_NewArray(js));
for (cgltf_size i = 0; i < data->materials_count; i++) {
cgltf_material *mat = &data->materials[i];
JS_ROOT(m, JS_NewObject(js));
tmp = mat->name ? JS_NewString(js, mat->name) : JS_NULL;
JS_SetPropertyStr(js, m.val, "name", tmp);
JS_ROOT(pbr, JS_NewObject(js));
if (mat->has_pbr_metallic_roughness) {
cgltf_pbr_metallic_roughness *pmr = &mat->pbr_metallic_roughness;
tmp = make_float_array(js, pmr->base_color_factor, 4);
JS_SetPropertyStr(js, pbr.val, "base_color_factor", tmp);
tmp = make_texture_info(js, &pmr->base_color_texture, data);
JS_SetPropertyStr(js, pbr.val, "base_color_texture", tmp);
JS_SetPropertyStr(js, pbr.val, "metallic_factor", JS_NewFloat64(js, pmr->metallic_factor));
JS_SetPropertyStr(js, pbr.val, "roughness_factor", JS_NewFloat64(js, pmr->roughness_factor));
tmp = make_texture_info(js, &pmr->metallic_roughness_texture, data);
JS_SetPropertyStr(js, pbr.val, "metallic_roughness_texture", tmp);
}
tmp = make_texture_info(js, &mat->normal_texture, data);
JS_SetPropertyStr(js, pbr.val, "normal_texture", tmp);
tmp = make_texture_info(js, &mat->occlusion_texture, data);
JS_SetPropertyStr(js, pbr.val, "occlusion_texture", tmp);
tmp = make_float_array(js, mat->emissive_factor, 3);
JS_SetPropertyStr(js, pbr.val, "emissive_factor", tmp);
tmp = make_texture_info(js, &mat->emissive_texture, data);
JS_SetPropertyStr(js, pbr.val, "emissive_texture", tmp);
JS_SetPropertyStr(js, m.val, "pbr", pbr.val);
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";
tmp = JS_NewString(js, alpha_mode);
JS_SetPropertyStr(js, m.val, "alpha_mode", tmp);
JS_SetPropertyStr(js, m.val, "alpha_cutoff", JS_NewFloat64(js, mat->alpha_cutoff));
JS_SetPropertyStr(js, m.val, "double_sided", JS_NewBool(js, mat->double_sided));
JS_SetPropertyStr(js, m.val, "unlit", JS_NewBool(js, mat->unlit));
JS_SetPropertyNumber(js, materials_arr.val, i, m.val);
}
JS_SetPropertyStr(js, obj.val, "materials", materials_arr.val);
// Nodes
JS_ROOT(nodes_arr, JS_NewArray(js));
for (cgltf_size i = 0; i < data->nodes_count; i++) {
cgltf_node *node = &data->nodes[i];
JS_ROOT(n, JS_NewObject(js));
tmp = node->name ? JS_NewString(js, node->name) : JS_NULL;
JS_SetPropertyStr(js, n.val, "name", tmp);
JS_SetPropertyStr(js, n.val, "mesh", node->mesh ? JS_NewInt32(js, (int)(node->mesh - data->meshes)) : JS_NULL);
JS_ROOT(children, JS_NewArray(js));
for (cgltf_size ci = 0; ci < node->children_count; ci++)
JS_SetPropertyNumber(js, children.val, ci, JS_NewInt32(js, (int)(node->children[ci] - data->nodes)));
JS_SetPropertyStr(js, n.val, "children", children.val);
if (node->has_matrix) {
tmp = make_float_array(js, node->matrix, 16);
JS_SetPropertyStr(js, n.val, "matrix", tmp);
JS_SetPropertyStr(js, n.val, "translation", JS_NULL);
JS_SetPropertyStr(js, n.val, "rotation", JS_NULL);
JS_SetPropertyStr(js, n.val, "scale", JS_NULL);
} else {
JS_SetPropertyStr(js, n.val, "matrix", JS_NULL);
tmp = make_float_array(js, node->translation, 3);
JS_SetPropertyStr(js, n.val, "translation", tmp);
tmp = make_float_array(js, node->rotation, 4);
JS_SetPropertyStr(js, n.val, "rotation", tmp);
tmp = make_float_array(js, node->scale, 3);
JS_SetPropertyStr(js, n.val, "scale", tmp);
}
JS_SetPropertyStr(js, n.val, "skin", node->skin ? JS_NewInt32(js, (int)(node->skin - data->skins)) : JS_NULL);
JS_SetPropertyNumber(js, nodes_arr.val, i, n.val);
}
JS_SetPropertyStr(js, obj.val, "nodes", nodes_arr.val);
// Scenes
JS_ROOT(scenes_arr, JS_NewArray(js));
for (cgltf_size i = 0; i < data->scenes_count; i++) {
cgltf_scene *scene = &data->scenes[i];
JS_ROOT(s, JS_NewObject(js));
JS_ROOT(snodes, JS_NewArray(js));
for (cgltf_size ni = 0; ni < scene->nodes_count; ni++)
JS_SetPropertyNumber(js, snodes.val, ni, JS_NewInt32(js, (int)(scene->nodes[ni] - data->nodes)));
JS_SetPropertyStr(js, s.val, "nodes", snodes.val);
JS_SetPropertyNumber(js, scenes_arr.val, i, s.val);
}
JS_SetPropertyStr(js, obj.val, "scenes", scenes_arr.val);
JS_SetPropertyStr(js, obj.val, "scene", data->scene ? JS_NewInt32(js, (int)(data->scene - data->scenes)) : JS_NULL);
// Animations
JS_ROOT(anims_arr, JS_NewArray(js));
for (cgltf_size ai = 0; ai < data->animations_count; ai++) {
cgltf_animation *anim = &data->animations[ai];
JS_ROOT(a, JS_NewObject(js));
tmp = anim->name ? JS_NewString(js, anim->name) : JS_NULL;
JS_SetPropertyStr(js, a.val, "name", tmp);
JS_ROOT(samps, JS_NewArray(js));
for (cgltf_size si = 0; si < anim->samplers_count; si++) {
cgltf_animation_sampler *samp = &anim->samplers[si];
JS_ROOT(s, JS_NewObject(js));
JS_SetPropertyStr(js, s.val, "input", samp->input ? JS_NewInt32(js, (int)(samp->input - data->accessors)) : JS_NULL);
JS_SetPropertyStr(js, s.val, "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";
tmp = JS_NewString(js, interp);
JS_SetPropertyStr(js, s.val, "interpolation", tmp);
JS_SetPropertyNumber(js, samps.val, si, s.val);
}
JS_SetPropertyStr(js, a.val, "samplers", samps.val);
JS_ROOT(channels, JS_NewArray(js));
for (cgltf_size ci = 0; ci < anim->channels_count; ci++) {
cgltf_animation_channel *chan = &anim->channels[ci];
JS_ROOT(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.val, "sampler", JS_NewInt32(js, samp_idx));
JS_ROOT(target, JS_NewObject(js));
JS_SetPropertyStr(js, target.val, "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;
}
tmp = JS_NewString(js, path);
JS_SetPropertyStr(js, target.val, "path", tmp);
JS_SetPropertyStr(js, c.val, "target", target.val);
JS_SetPropertyNumber(js, channels.val, ci, c.val);
}
JS_SetPropertyStr(js, a.val, "channels", channels.val);
JS_SetPropertyNumber(js, anims_arr.val, ai, a.val);
}
JS_SetPropertyStr(js, obj.val, "animations", anims_arr.val);
// Skins
JS_ROOT(skins_arr, JS_NewArray(js));
for (cgltf_size i = 0; i < data->skins_count; i++) {
cgltf_skin *skin = &data->skins[i];
JS_ROOT(s, JS_NewObject(js));
tmp = skin->name ? JS_NewString(js, skin->name) : JS_NULL;
JS_SetPropertyStr(js, s.val, "name", tmp);
JS_ROOT(joints, JS_NewArray(js));
for (cgltf_size ji = 0; ji < skin->joints_count; ji++)
JS_SetPropertyNumber(js, joints.val, ji, JS_NewInt32(js, (int)(skin->joints[ji] - data->nodes)));
JS_SetPropertyStr(js, s.val, "joints", joints.val);
JS_SetPropertyStr(js, s.val, "inverse_bind_matrices", skin->inverse_bind_matrices ? JS_NewInt32(js, (int)(skin->inverse_bind_matrices - data->accessors)) : JS_NULL);
JS_SetPropertyStr(js, s.val, "skeleton", skin->skeleton ? JS_NewInt32(js, (int)(skin->skeleton - data->nodes)) : JS_NULL);
JS_SetPropertyNumber(js, skins_arr.val, i, s.val);
}
JS_SetPropertyStr(js, obj.val, "skins", skins_arr.val);
// Extensions
JS_ROOT(exts, JS_NewObject(js));
JS_ROOT(used, JS_NewArray(js));
for (cgltf_size i = 0; i < data->extensions_used_count; i++) {
tmp = JS_NewString(js, data->extensions_used[i]);
JS_SetPropertyNumber(js, used.val, i, tmp);
}
JS_SetPropertyStr(js, exts.val, "used", used.val);
JS_ROOT(required, JS_NewArray(js));
for (cgltf_size i = 0; i < data->extensions_required_count; i++) {
tmp = JS_NewString(js, data->extensions_required[i]);
JS_SetPropertyNumber(js, required.val, i, tmp);
}
JS_SetPropertyStr(js, exts.val, "required", required.val);
JS_SetPropertyStr(js, obj.val, "extensions", exts.val);
cgltf_free(data);
JS_RETURN(obj.val);
}
static const JSCFunctionListEntry js_gltf_funcs[] = {
MIST_FUNC_DEF(gltf, decode, 1),
};
CELL_USE_FUNCS(js_gltf_funcs)