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

446 lines
16 KiB
C

#include "cell.h"
#include <string.h>
#include <stdlib.h>
#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)
{
JS_FRAME(js);
JS_ROOT(a, JS_NewArray(js));
for (int i = 0; i < count; i++)
JS_SetPropertyNumber(js, a.val, i, JS_NewFloat64(js, arr[i]));
JS_RETURN(a.val);
}
JSValue js_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");
JS_FRAME(js);
JS_ROOT(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_buf = 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_buf) {
int vn_idx = idx.vn_idx;
if (vn_idx >= 0 && vn_idx < (int)(attrib.num_normals)) {
normals_buf[v * 3 + 0] = attrib.normals[vn_idx * 3 + 0];
normals_buf[v * 3 + 1] = attrib.normals[vn_idx * 3 + 1];
normals_buf[v * 3 + 2] = attrib.normals[vn_idx * 3 + 2];
} else {
normals_buf[v * 3 + 0] = 0;
normals_buf[v * 3 + 1] = 1;
normals_buf[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 tmp;
JS_ROOT(buffers_arr, JS_NewArray(js));
{
JS_ROOT(buf, JS_NewObject(js));
tmp = js_new_blob_stoned_copy(js, buffer_data, total_buffer_size);
JS_SetPropertyStr(js, buf.val, "blob", tmp);
JS_SetPropertyStr(js, buf.val, "byte_length", JS_NewInt64(js, total_buffer_size));
JS_SetPropertyNumber(js, buffers_arr.val, 0, buf.val);
}
JS_SetPropertyStr(js, obj.val, "buffers", buffers_arr.val);
// Create views
JS_ROOT(views_arr, JS_NewArray(js));
int view_idx = 0;
// Position view
{
JS_ROOT(pos_view, JS_NewObject(js));
JS_SetPropertyStr(js, pos_view.val, "buffer", JS_NewInt32(js, 0));
JS_SetPropertyStr(js, pos_view.val, "byte_offset", JS_NewInt64(js, 0));
JS_SetPropertyStr(js, pos_view.val, "byte_length", JS_NewInt64(js, pos_size));
JS_SetPropertyStr(js, pos_view.val, "byte_stride", JS_NULL);
tmp = JS_NewString(js, "vertex");
JS_SetPropertyStr(js, pos_view.val, "usage", tmp);
JS_SetPropertyNumber(js, views_arr.val, view_idx++, pos_view.val);
}
int pos_view_idx = 0;
int norm_view_idx = -1;
if (global_has_normals) {
JS_ROOT(norm_view, JS_NewObject(js));
JS_SetPropertyStr(js, norm_view.val, "buffer", JS_NewInt32(js, 0));
JS_SetPropertyStr(js, norm_view.val, "byte_offset", JS_NewInt64(js, pos_size));
JS_SetPropertyStr(js, norm_view.val, "byte_length", JS_NewInt64(js, norm_size));
JS_SetPropertyStr(js, norm_view.val, "byte_stride", JS_NULL);
tmp = JS_NewString(js, "vertex");
JS_SetPropertyStr(js, norm_view.val, "usage", tmp);
norm_view_idx = view_idx;
JS_SetPropertyNumber(js, views_arr.val, view_idx++, norm_view.val);
}
int uv_view_idx = -1;
if (global_has_uvs) {
JS_ROOT(uv_view, JS_NewObject(js));
JS_SetPropertyStr(js, uv_view.val, "buffer", JS_NewInt32(js, 0));
JS_SetPropertyStr(js, uv_view.val, "byte_offset", JS_NewInt64(js, pos_size + norm_size));
JS_SetPropertyStr(js, uv_view.val, "byte_length", JS_NewInt64(js, uv_size));
JS_SetPropertyStr(js, uv_view.val, "byte_stride", JS_NULL);
tmp = JS_NewString(js, "vertex");
JS_SetPropertyStr(js, uv_view.val, "usage", tmp);
uv_view_idx = view_idx;
JS_SetPropertyNumber(js, views_arr.val, view_idx++, uv_view.val);
}
// Index view
int idx_view_idx;
{
JS_ROOT(idx_view, JS_NewObject(js));
JS_SetPropertyStr(js, idx_view.val, "buffer", JS_NewInt32(js, 0));
JS_SetPropertyStr(js, idx_view.val, "byte_offset", JS_NewInt64(js, pos_size + norm_size + uv_size));
JS_SetPropertyStr(js, idx_view.val, "byte_length", JS_NewInt64(js, idx_size));
JS_SetPropertyStr(js, idx_view.val, "byte_stride", JS_NULL);
tmp = JS_NewString(js, "index");
JS_SetPropertyStr(js, idx_view.val, "usage", tmp);
idx_view_idx = view_idx;
JS_SetPropertyNumber(js, views_arr.val, view_idx++, idx_view.val);
}
JS_SetPropertyStr(js, obj.val, "views", views_arr.val);
// Build per-shape accessors and meshes
JS_ROOT(accessors_arr, JS_NewArray(js));
int acc_idx = 0;
JS_ROOT(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;
{
JS_ROOT(spa, JS_NewObject(js));
JS_SetPropertyStr(js, spa.val, "view", JS_NewInt32(js, pos_view_idx));
JS_SetPropertyStr(js, spa.val, "byte_offset", JS_NewInt64(js, vertex_offset * 3 * sizeof(float)));
JS_SetPropertyStr(js, spa.val, "count", JS_NewInt64(js, shape_vertices));
tmp = JS_NewString(js, "f32");
JS_SetPropertyStr(js, spa.val, "component_type", tmp);
tmp = JS_NewString(js, "vec3");
JS_SetPropertyStr(js, spa.val, "type", tmp);
JS_SetPropertyStr(js, spa.val, "normalized", JS_FALSE);
JS_SetPropertyStr(js, spa.val, "min", JS_NULL);
JS_SetPropertyStr(js, spa.val, "max", JS_NULL);
JS_SetPropertyNumber(js, accessors_arr.val, acc_idx++, spa.val);
}
int shape_norm_acc = -1;
if (global_has_normals) {
shape_norm_acc = acc_idx;
JS_ROOT(sna, JS_NewObject(js));
JS_SetPropertyStr(js, sna.val, "view", JS_NewInt32(js, norm_view_idx));
JS_SetPropertyStr(js, sna.val, "byte_offset", JS_NewInt64(js, vertex_offset * 3 * sizeof(float)));
JS_SetPropertyStr(js, sna.val, "count", JS_NewInt64(js, shape_vertices));
tmp = JS_NewString(js, "f32");
JS_SetPropertyStr(js, sna.val, "component_type", tmp);
tmp = JS_NewString(js, "vec3");
JS_SetPropertyStr(js, sna.val, "type", tmp);
JS_SetPropertyStr(js, sna.val, "normalized", JS_FALSE);
JS_SetPropertyStr(js, sna.val, "min", JS_NULL);
JS_SetPropertyStr(js, sna.val, "max", JS_NULL);
JS_SetPropertyNumber(js, accessors_arr.val, acc_idx++, sna.val);
}
int shape_uv_acc = -1;
if (global_has_uvs) {
shape_uv_acc = acc_idx;
JS_ROOT(sua, JS_NewObject(js));
JS_SetPropertyStr(js, sua.val, "view", JS_NewInt32(js, uv_view_idx));
JS_SetPropertyStr(js, sua.val, "byte_offset", JS_NewInt64(js, vertex_offset * 2 * sizeof(float)));
JS_SetPropertyStr(js, sua.val, "count", JS_NewInt64(js, shape_vertices));
tmp = JS_NewString(js, "f32");
JS_SetPropertyStr(js, sua.val, "component_type", tmp);
tmp = JS_NewString(js, "vec2");
JS_SetPropertyStr(js, sua.val, "type", tmp);
JS_SetPropertyStr(js, sua.val, "normalized", JS_FALSE);
JS_SetPropertyStr(js, sua.val, "min", JS_NULL);
JS_SetPropertyStr(js, sua.val, "max", JS_NULL);
JS_SetPropertyNumber(js, accessors_arr.val, acc_idx++, sua.val);
}
int shape_idx_acc = acc_idx;
{
JS_ROOT(sia, JS_NewObject(js));
JS_SetPropertyStr(js, sia.val, "view", JS_NewInt32(js, idx_view_idx));
JS_SetPropertyStr(js, sia.val, "byte_offset", JS_NewInt64(js, vertex_offset * (use_32bit ? sizeof(uint32_t) : sizeof(uint16_t))));
JS_SetPropertyStr(js, sia.val, "count", JS_NewInt64(js, shape_vertices));
tmp = JS_NewString(js, use_32bit ? "u32" : "u16");
JS_SetPropertyStr(js, sia.val, "component_type", tmp);
tmp = JS_NewString(js, "scalar");
JS_SetPropertyStr(js, sia.val, "type", tmp);
JS_SetPropertyStr(js, sia.val, "normalized", JS_FALSE);
JS_SetPropertyStr(js, sia.val, "min", JS_NULL);
JS_SetPropertyStr(js, sia.val, "max", JS_NULL);
JS_SetPropertyNumber(js, accessors_arr.val, acc_idx++, sia.val);
}
// Create mesh
JS_ROOT(mesh, JS_NewObject(js));
tmp = shape->name ? JS_NewString(js, shape->name) : JS_NULL;
JS_SetPropertyStr(js, mesh.val, "name", tmp);
JS_ROOT(prims_arr, JS_NewArray(js));
{
JS_ROOT(prim, JS_NewObject(js));
tmp = JS_NewString(js, "triangles");
JS_SetPropertyStr(js, prim.val, "topology", tmp);
JS_ROOT(attrs, JS_NewObject(js));
JS_SetPropertyStr(js, attrs.val, "POSITION", JS_NewInt32(js, shape_pos_acc));
if (shape_norm_acc >= 0)
JS_SetPropertyStr(js, attrs.val, "NORMAL", JS_NewInt32(js, shape_norm_acc));
if (shape_uv_acc >= 0)
JS_SetPropertyStr(js, attrs.val, "TEXCOORD_0", JS_NewInt32(js, shape_uv_acc));
JS_SetPropertyStr(js, prim.val, "attributes", attrs.val);
JS_SetPropertyStr(js, prim.val, "indices", JS_NewInt32(js, shape_idx_acc));
JS_SetPropertyStr(js, prim.val, "material", JS_NULL);
JS_SetPropertyNumber(js, prims_arr.val, 0, prim.val);
}
JS_SetPropertyStr(js, mesh.val, "primitives", prims_arr.val);
JS_SetPropertyNumber(js, meshes_arr.val, si, mesh.val);
vertex_offset += shape_vertices;
}
JS_SetPropertyStr(js, obj.val, "accessors", accessors_arr.val);
JS_SetPropertyStr(js, obj.val, "meshes", meshes_arr.val);
// Materials from OBJ
JS_ROOT(materials_arr, JS_NewArray(js));
for (size_t i = 0; i < num_materials; i++) {
tinyobj_material_t *mat = &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));
float bc[4] = {mat->diffuse[0], mat->diffuse[1], mat->diffuse[2], 1.0f};
tmp = make_float_array(js, bc, 4);
JS_SetPropertyStr(js, pbr.val, "base_color_factor", tmp);
JS_SetPropertyStr(js, pbr.val, "base_color_texture", JS_NULL);
JS_SetPropertyStr(js, pbr.val, "metallic_factor", JS_NewFloat64(js, 0.0));
JS_SetPropertyStr(js, pbr.val, "roughness_factor", JS_NewFloat64(js, 1.0));
JS_SetPropertyStr(js, pbr.val, "metallic_roughness_texture", JS_NULL);
JS_SetPropertyStr(js, pbr.val, "normal_texture", JS_NULL);
JS_SetPropertyStr(js, pbr.val, "occlusion_texture", JS_NULL);
float ef[3] = {mat->emission[0], mat->emission[1], mat->emission[2]};
tmp = make_float_array(js, ef, 3);
JS_SetPropertyStr(js, pbr.val, "emissive_factor", tmp);
JS_SetPropertyStr(js, pbr.val, "emissive_texture", JS_NULL);
JS_SetPropertyStr(js, m.val, "pbr", pbr.val);
tmp = JS_NewString(js, "OPAQUE");
JS_SetPropertyStr(js, m.val, "alpha_mode", tmp);
JS_SetPropertyStr(js, m.val, "alpha_cutoff", JS_NewFloat64(js, 0.5));
JS_SetPropertyStr(js, m.val, "double_sided", JS_FALSE);
JS_SetPropertyNumber(js, materials_arr.val, i, m.val);
}
JS_SetPropertyStr(js, obj.val, "materials", materials_arr.val);
// Empty arrays for unsupported features
tmp = JS_NewArray(js);
JS_SetPropertyStr(js, obj.val, "images", tmp);
tmp = JS_NewArray(js);
JS_SetPropertyStr(js, obj.val, "textures", tmp);
tmp = JS_NewArray(js);
JS_SetPropertyStr(js, obj.val, "samplers", tmp);
// Nodes - one node per mesh
JS_ROOT(nodes_arr, JS_NewArray(js));
JS_ROOT(scene_nodes, JS_NewArray(js));
for (size_t i = 0; i < num_shapes; i++) {
JS_ROOT(n, JS_NewObject(js));
tmp = shapes[i].name ? JS_NewString(js, shapes[i].name) : JS_NULL;
JS_SetPropertyStr(js, n.val, "name", tmp);
JS_SetPropertyStr(js, n.val, "mesh", JS_NewInt32(js, i));
tmp = JS_NewArray(js);
JS_SetPropertyStr(js, n.val, "children", tmp);
JS_SetPropertyStr(js, n.val, "matrix", JS_NULL);
float t[3] = {0, 0, 0};
float r[4] = {0, 0, 0, 1};
float s[3] = {1, 1, 1};
tmp = make_float_array(js, t, 3);
JS_SetPropertyStr(js, n.val, "translation", tmp);
tmp = make_float_array(js, r, 4);
JS_SetPropertyStr(js, n.val, "rotation", tmp);
tmp = make_float_array(js, s, 3);
JS_SetPropertyStr(js, n.val, "scale", tmp);
JS_SetPropertyStr(js, n.val, "skin", JS_NULL);
JS_SetPropertyNumber(js, nodes_arr.val, i, n.val);
JS_SetPropertyNumber(js, scene_nodes.val, i, JS_NewInt32(js, i));
}
JS_SetPropertyStr(js, obj.val, "nodes", nodes_arr.val);
// Scenes
JS_ROOT(scenes_arr, JS_NewArray(js));
{
JS_ROOT(sc, JS_NewObject(js));
JS_SetPropertyStr(js, sc.val, "nodes", scene_nodes.val);
JS_SetPropertyNumber(js, scenes_arr.val, 0, sc.val);
}
JS_SetPropertyStr(js, obj.val, "scenes", scenes_arr.val);
JS_SetPropertyStr(js, obj.val, "scene", JS_NewInt32(js, 0));
// Empty arrays for animations/skins
tmp = JS_NewArray(js);
JS_SetPropertyStr(js, obj.val, "animations", tmp);
tmp = JS_NewArray(js);
JS_SetPropertyStr(js, obj.val, "skins", tmp);
// Extensions
JS_ROOT(exts, JS_NewObject(js));
tmp = JS_NewArray(js);
JS_SetPropertyStr(js, exts.val, "used", tmp);
tmp = JS_NewArray(js);
JS_SetPropertyStr(js, exts.val, "required", tmp);
JS_SetPropertyStr(js, obj.val, "extensions", exts.val);
free(buffer_data);
tinyobj_attrib_free(&attrib);
tinyobj_shapes_free(shapes, num_shapes);
tinyobj_materials_free(materials, num_materials);
JS_RETURN(obj.val);
}
static const JSCFunctionListEntry js_obj_funcs[] = {
MIST_FUNC_DEF(obj, decode, 1),
};
CELL_USE_FUNCS(js_obj_funcs)