#include "cell.h" #include #include #include #include // 4x4 matrix type (column-major for GPU compatibility) typedef struct { float m[16]; } mat4; // Vector types typedef struct { float x, y, z; } vec3; typedef struct { float x, y, z, w; } vec4; typedef struct { float x, y, z, w; } quat; // Identity matrix static mat4 mat4_identity(void) { mat4 m = {0}; m.m[0] = m.m[5] = m.m[10] = m.m[15] = 1.0f; return m; } // Matrix multiplication (column-major: result = a * b) // For column-major matrices: r[col][row] = sum(a[k][row] * b[col][k]) // Index: col * 4 + row static mat4 mat4_mul(mat4 a, mat4 b) { mat4 r = {0}; for (int col = 0; col < 4; col++) { for (int row = 0; row < 4; row++) { for (int k = 0; k < 4; k++) { r.m[col * 4 + row] += a.m[k * 4 + row] * b.m[col * 4 + k]; } } } return r; } // Translation matrix static mat4 mat4_translate(float x, float y, float z) { mat4 m = mat4_identity(); m.m[12] = x; m.m[13] = y; m.m[14] = z; return m; } // Scale matrix static mat4 mat4_scale(float x, float y, float z) { mat4 m = mat4_identity(); m.m[0] = x; m.m[5] = y; m.m[10] = z; return m; } // Rotation matrices (XYZ order) static mat4 mat4_rotate_x(float rad) { mat4 m = mat4_identity(); float c = cosf(rad), s = sinf(rad); m.m[5] = c; m.m[6] = s; m.m[9] = -s; m.m[10] = c; return m; } static mat4 mat4_rotate_y(float rad) { mat4 m = mat4_identity(); float c = cosf(rad), s = sinf(rad); m.m[0] = c; m.m[2] = -s; m.m[8] = s; m.m[10] = c; return m; } static mat4 mat4_rotate_z(float rad) { mat4 m = mat4_identity(); float c = cosf(rad), s = sinf(rad); m.m[0] = c; m.m[1] = s; m.m[4] = -s; m.m[5] = c; return m; } // Matrix from quaternion (column-major) static mat4 mat4_from_quat(quat q) { mat4 m = mat4_identity(); float x = q.x, y = q.y, z = q.z, w = q.w; float x2 = x + x, y2 = y + y, z2 = z + z; float xx = x * x2, xy = x * y2, xz = x * z2; float yy = y * y2, yz = y * z2, zz = z * z2; float wx = w * x2, wy = w * y2, wz = w * z2; m.m[0] = 1.0f - (yy + zz); m.m[1] = xy + wz; m.m[2] = xz - wy; m.m[3] = 0.0f; m.m[4] = xy - wz; m.m[5] = 1.0f - (xx + zz); m.m[6] = yz + wx; m.m[7] = 0.0f; m.m[8] = xz + wy; m.m[9] = yz - wx; m.m[10] = 1.0f - (xx + yy); m.m[11] = 0.0f; m.m[12] = 0.0f; m.m[13] = 0.0f; m.m[14] = 0.0f; m.m[15] = 1.0f; return m; } // Build TRS matrix from translation, quaternion rotation, scale (column-major) static mat4 mat4_trs(vec3 t, quat r, vec3 s) { mat4 rot = mat4_from_quat(r); // Scale the rotation matrix columns rot.m[0] *= s.x; rot.m[1] *= s.x; rot.m[2] *= s.x; rot.m[4] *= s.y; rot.m[5] *= s.y; rot.m[6] *= s.y; rot.m[8] *= s.z; rot.m[9] *= s.z; rot.m[10] *= s.z; // Set translation rot.m[12] = t.x; rot.m[13] = t.y; rot.m[14] = t.z; return rot; } // Perspective projection static mat4 mat4_perspective(float fov_deg, float aspect, float near, float far) { mat4 m = {0}; float f = 1.0f / tanf(fov_deg * 0.5f * 3.14159265f / 180.0f); m.m[0] = f / aspect; m.m[5] = f; m.m[10] = (far + near) / (near - far); m.m[11] = -1.0f; m.m[14] = (2.0f * far * near) / (near - far); return m; } // Orthographic projection static mat4 mat4_ortho(float left, float right, float bottom, float top, float near, float far) { mat4 m = {0}; m.m[0] = 2.0f / (right - left); m.m[5] = 2.0f / (top - bottom); m.m[10] = -2.0f / (far - near); m.m[12] = -(right + left) / (right - left); m.m[13] = -(top + bottom) / (top - bottom); m.m[14] = -(far + near) / (far - near); m.m[15] = 1.0f; return m; } // Look-at view matrix static mat4 mat4_look_at(vec3 eye, vec3 target, vec3 up) { vec3 f = {target.x - eye.x, target.y - eye.y, target.z - eye.z}; float len = sqrtf(f.x*f.x + f.y*f.y + f.z*f.z); f.x /= len; f.y /= len; f.z /= len; vec3 s = {f.y*up.z - f.z*up.y, f.z*up.x - f.x*up.z, f.x*up.y - f.y*up.x}; len = sqrtf(s.x*s.x + s.y*s.y + s.z*s.z); s.x /= len; s.y /= len; s.z /= len; vec3 u = {s.y*f.z - s.z*f.y, s.z*f.x - s.x*f.z, s.x*f.y - s.y*f.x}; mat4 m = mat4_identity(); m.m[0] = s.x; m.m[4] = s.y; m.m[8] = s.z; m.m[1] = u.x; m.m[5] = u.y; m.m[9] = u.z; m.m[2] = -f.x; m.m[6] = -f.y; m.m[10] = -f.z; m.m[12] = -(s.x*eye.x + s.y*eye.y + s.z*eye.z); m.m[13] = -(u.x*eye.x + u.y*eye.y + u.z*eye.z); m.m[14] = (f.x*eye.x + f.y*eye.y + f.z*eye.z); return m; } // Compute view matrix from look-at parameters JSValue js_model_compute_view_matrix(JSContext *js, JSValue this_val, int argc, JSValueConst *argv) { if (argc < 9) return JS_ThrowTypeError(js, "compute_view_matrix requires 9 arguments"); double ex, ey, ez, tx, ty, tz, ux, uy, uz; JS_ToFloat64(js, &ex, argv[0]); JS_ToFloat64(js, &ey, argv[1]); JS_ToFloat64(js, &ez, argv[2]); JS_ToFloat64(js, &tx, argv[3]); JS_ToFloat64(js, &ty, argv[4]); JS_ToFloat64(js, &tz, argv[5]); JS_ToFloat64(js, &ux, argv[6]); JS_ToFloat64(js, &uy, argv[7]); JS_ToFloat64(js, &uz, argv[8]); vec3 eye = {ex, ey, ez}; vec3 target = {tx, ty, tz}; vec3 up = {ux, uy, uz}; mat4 view = mat4_look_at(eye, target, up); return js_new_blob_stoned_copy(js, view.m, sizeof(view.m)); } // Compute perspective projection matrix JSValue js_model_compute_perspective(JSContext *js, JSValue this_val, int argc, JSValueConst *argv) { if (argc < 4) return JS_ThrowTypeError(js, "compute_perspective requires 4 arguments"); double fov, aspect, near, far; JS_ToFloat64(js, &fov, argv[0]); JS_ToFloat64(js, &aspect, argv[1]); JS_ToFloat64(js, &near, argv[2]); JS_ToFloat64(js, &far, argv[3]); mat4 proj = mat4_perspective(fov, aspect, near, far); return js_new_blob_stoned_copy(js, proj.m, sizeof(proj.m)); } // Compute orthographic projection matrix JSValue js_model_compute_ortho(JSContext *js, JSValue this_val, int argc, JSValueConst *argv) { if (argc < 6) return JS_ThrowTypeError(js, "compute_ortho requires 6 arguments"); double left, right, bottom, top, near, far; JS_ToFloat64(js, &left, argv[0]); JS_ToFloat64(js, &right, argv[1]); JS_ToFloat64(js, &bottom, argv[2]); JS_ToFloat64(js, &top, argv[3]); JS_ToFloat64(js, &near, argv[4]); JS_ToFloat64(js, &far, argv[5]); mat4 proj = mat4_ortho(left, right, bottom, top, near, far); return js_new_blob_stoned_copy(js, proj.m, sizeof(proj.m)); } // Multiply two matrices (both as blobs) JSValue js_model_mat4_mul(JSContext *js, JSValue this_val, int argc, JSValueConst *argv) { if (argc < 2) return JS_ThrowTypeError(js, "mat4_mul requires 2 matrices"); size_t size_a, size_b; float *a = js_get_blob_data(js, &size_a, argv[0]); float *b = js_get_blob_data(js, &size_b, argv[1]); if (!a || !b || size_a < 64 || size_b < 64) { return JS_ThrowTypeError(js, "mat4_mul requires two 4x4 matrix blobs"); } mat4 ma, mb; memcpy(ma.m, a, 64); memcpy(mb.m, b, 64); mat4 result = mat4_mul(ma, mb); return js_new_blob_stoned_copy(js, result.m, sizeof(result.m)); } // Create identity matrix JSValue js_model_mat4_identity(JSContext *js, JSValue this_val, int argc, JSValueConst *argv) { mat4 m = mat4_identity(); return js_new_blob_stoned_copy(js, m.m, sizeof(m.m)); } // Create matrix from TRS (translation, quaternion rotation, scale) // Args: tx, ty, tz, qx, qy, qz, qw, sx, sy, sz JSValue js_model_mat4_from_trs(JSContext *js, JSValue this_val, int argc, JSValueConst *argv) { if (argc < 10) return JS_ThrowTypeError(js, "mat4_from_trs requires 10 arguments"); double tx, ty, tz, qx, qy, qz, qw, sx, sy, sz; JS_ToFloat64(js, &tx, argv[0]); JS_ToFloat64(js, &ty, argv[1]); JS_ToFloat64(js, &tz, argv[2]); JS_ToFloat64(js, &qx, argv[3]); JS_ToFloat64(js, &qy, argv[4]); JS_ToFloat64(js, &qz, argv[5]); JS_ToFloat64(js, &qw, argv[6]); JS_ToFloat64(js, &sx, argv[7]); JS_ToFloat64(js, &sy, argv[8]); JS_ToFloat64(js, &sz, argv[9]); vec3 t = {tx, ty, tz}; quat r = {qx, qy, qz, qw}; vec3 s = {sx, sy, sz}; mat4 m = mat4_trs(t, r, s); return js_new_blob_stoned_copy(js, m.m, sizeof(m.m)); } // Create matrix from 16-element array (column-major) JSValue js_model_mat4_from_array(JSContext *js, JSValue this_val, int argc, JSValueConst *argv) { if (argc < 1 || !JS_IsArray(js, argv[0])) return JS_ThrowTypeError(js, "mat4_from_array requires an array of 16 numbers"); int len = JS_ArrayLength(js, argv[0]); if (len < 16) return JS_ThrowTypeError(js, "mat4_from_array requires 16 elements"); float m[16]; for (int i = 0; i < 16; i++) { JSValue v = JS_GetPropertyUint32(js, argv[0], i); double d = 0.0; JS_ToFloat64(js, &d, v); JS_FreeValue(js, v); m[i] = (float)d; } return js_new_blob_stoned_copy(js, m, sizeof(m)); } // Extract accessor data from a gltf buffer // Args: buffer_blob, view_byte_offset, view_byte_stride (or 0), accessor_byte_offset, count, component_type, type // Returns: blob of floats (always converts to f32) JSValue js_model_extract_accessor(JSContext *js, JSValue this_val, int argc, JSValueConst *argv) { if (argc < 7) return JS_ThrowTypeError(js, "extract_accessor requires 7 arguments"); size_t buf_size; uint8_t *buf = js_get_blob_data(js, &buf_size, argv[0]); if (!buf) return JS_ThrowTypeError(js, "invalid buffer blob"); int view_offset, view_stride, acc_offset, count; JS_ToInt32(js, &view_offset, argv[1]); JS_ToInt32(js, &view_stride, argv[2]); JS_ToInt32(js, &acc_offset, argv[3]); JS_ToInt32(js, &count, argv[4]); const char *comp_type = JS_ToCString(js, argv[5]); const char *type_str = JS_ToCString(js, argv[6]); if (!comp_type || !type_str) { if (comp_type) JS_FreeCString(js, comp_type); if (type_str) JS_FreeCString(js, type_str); return JS_ThrowTypeError(js, "invalid component_type or type"); } // Determine component count int comp_count = 1; if (strcmp(type_str, "vec2") == 0) comp_count = 2; else if (strcmp(type_str, "vec3") == 0) comp_count = 3; else if (strcmp(type_str, "vec4") == 0) comp_count = 4; else if (strcmp(type_str, "mat2") == 0) comp_count = 4; else if (strcmp(type_str, "mat3") == 0) comp_count = 9; else if (strcmp(type_str, "mat4") == 0) comp_count = 16; // Determine component size and type int comp_size = 4; int is_float = 1; int is_signed = 0; if (strcmp(comp_type, "f32") == 0) { comp_size = 4; is_float = 1; } else if (strcmp(comp_type, "u8") == 0) { comp_size = 1; is_float = 0; is_signed = 0; } else if (strcmp(comp_type, "i8") == 0) { comp_size = 1; is_float = 0; is_signed = 1; } else if (strcmp(comp_type, "u16") == 0) { comp_size = 2; is_float = 0; is_signed = 0; } else if (strcmp(comp_type, "i16") == 0) { comp_size = 2; is_float = 0; is_signed = 1; } else if (strcmp(comp_type, "u32") == 0) { comp_size = 4; is_float = 0; is_signed = 0; } int element_size = comp_size * comp_count; int stride = view_stride > 0 ? view_stride : element_size; JS_FreeCString(js, comp_type); JS_FreeCString(js, type_str); // Allocate output (always f32) size_t out_size = count * comp_count * sizeof(float); float *out = malloc(out_size); if (!out) return JS_ThrowOutOfMemory(js); uint8_t *src = buf + view_offset + acc_offset; for (int i = 0; i < count; i++) { uint8_t *elem = src + i * stride; for (int c = 0; c < comp_count; c++) { float val = 0.0f; if (is_float) { val = *(float*)(elem + c * comp_size); } else if (comp_size == 1) { val = is_signed ? (float)*(int8_t*)(elem + c) : (float)*(uint8_t*)(elem + c); } else if (comp_size == 2) { val = is_signed ? (float)*(int16_t*)(elem + c * 2) : (float)*(uint16_t*)(elem + c * 2); } else if (comp_size == 4) { val = (float)*(uint32_t*)(elem + c * 4); } out[i * comp_count + c] = val; } } JSValue ret = js_new_blob_stoned_copy(js, out, out_size); free(out); return ret; } // Extract index data from a gltf buffer // Args: buffer_blob, view_byte_offset, accessor_byte_offset, count, component_type // Returns: blob of u16 or u32 indices JSValue js_model_extract_indices(JSContext *js, JSValue this_val, int argc, JSValueConst *argv) { if (argc < 5) return JS_ThrowTypeError(js, "extract_indices requires 5 arguments"); size_t buf_size; uint8_t *buf = js_get_blob_data(js, &buf_size, argv[0]); if (!buf) return JS_ThrowTypeError(js, "invalid buffer blob"); int view_offset, acc_offset, count; JS_ToInt32(js, &view_offset, argv[1]); JS_ToInt32(js, &acc_offset, argv[2]); JS_ToInt32(js, &count, argv[3]); const char *comp_type = JS_ToCString(js, argv[4]); if (!comp_type) return JS_ThrowTypeError(js, "invalid component_type"); uint8_t *src = buf + view_offset + acc_offset; JSValue ret; if (strcmp(comp_type, "u32") == 0) { ret = js_new_blob_stoned_copy(js, src, count * sizeof(uint32_t)); } else if (strcmp(comp_type, "u16") == 0) { ret = js_new_blob_stoned_copy(js, src, count * sizeof(uint16_t)); } else if (strcmp(comp_type, "u8") == 0) { // Convert u8 to u16 uint16_t *out = malloc(count * sizeof(uint16_t)); for (int i = 0; i < count; i++) out[i] = src[i]; ret = js_new_blob_stoned_copy(js, out, count * sizeof(uint16_t)); free(out); } else { JS_FreeCString(js, comp_type); return JS_ThrowTypeError(js, "unsupported index type"); } JS_FreeCString(js, comp_type); return ret; } // Pack interleaved vertex data for GPU // Takes separate position, normal, uv, color, joints, weights blobs and packs into interleaved format // Returns: { data: blob, stride: number, skinned: bool } // Non-skinned: pos(3) + normal(3) + uv(2) + color(4) = 12 floats = 48 bytes // Skinned: pos(3) + normal(3) + uv(2) + color(4) + joints(4 as float) + weights(4) = 20 floats = 80 bytes JSValue js_model_pack_vertices(JSContext *js, JSValue this_val, int argc, JSValueConst *argv) { if (argc < 1) return JS_ThrowTypeError(js, "pack_vertices requires mesh data"); JSValue mesh = argv[0]; // Get vertex count JSValue vc_v = JS_GetPropertyStr(js, mesh, "vertex_count"); int vertex_count = 0; JS_ToInt32(js, &vertex_count, vc_v); JS_FreeValue(js, vc_v); if (vertex_count <= 0) return JS_ThrowTypeError(js, "invalid vertex count"); // Get data blobs JSValue pos_v = JS_GetPropertyStr(js, mesh, "positions"); JSValue norm_v = JS_GetPropertyStr(js, mesh, "normals"); JSValue uv_v = JS_GetPropertyStr(js, mesh, "uvs"); JSValue color_v = JS_GetPropertyStr(js, mesh, "colors"); JSValue joints_v = JS_GetPropertyStr(js, mesh, "joints"); JSValue weights_v = JS_GetPropertyStr(js, mesh, "weights"); size_t pos_size, norm_size, uv_size, color_size, joints_size, weights_size; float *positions = js_get_blob_data(js, &pos_size, pos_v); float *normals = JS_IsNull(norm_v) ? NULL : js_get_blob_data(js, &norm_size, norm_v); float *uvs = JS_IsNull(uv_v) ? NULL : js_get_blob_data(js, &uv_size, uv_v); float *colors = JS_IsNull(color_v) ? NULL : js_get_blob_data(js, &color_size, color_v); float *joints = JS_IsNull(joints_v) ? NULL : js_get_blob_data(js, &joints_size, joints_v); float *weights = JS_IsNull(weights_v) ? NULL : js_get_blob_data(js, &weights_size, weights_v); if (!positions) { JS_FreeValue(js, pos_v); JS_FreeValue(js, norm_v); JS_FreeValue(js, uv_v); JS_FreeValue(js, color_v); JS_FreeValue(js, joints_v); JS_FreeValue(js, weights_v); return JS_ThrowTypeError(js, "positions required"); } int skinned = (joints != NULL && weights != NULL) ? 1 : 0; int floats_per_vertex = skinned ? 20 : 12; int stride = floats_per_vertex * 4; size_t total_size = vertex_count * stride; float *packed = malloc(total_size); // Detect if colors are vec3 (RGB) or vec4 (RGBA) // vec3: color_size = vertex_count * 3 * sizeof(float) // vec4: color_size = vertex_count * 4 * sizeof(float) int color_components = 4; if (colors && color_size > 0) { size_t expected_vec4 = (size_t)vertex_count * 4 * sizeof(float); size_t expected_vec3 = (size_t)vertex_count * 3 * sizeof(float); if (color_size == expected_vec3) { color_components = 3; } } for (int i = 0; i < vertex_count; i++) { float *v = &packed[i * floats_per_vertex]; // Position v[0] = positions[i * 3 + 0]; v[1] = positions[i * 3 + 1]; v[2] = positions[i * 3 + 2]; // Normal if (normals) { v[3] = normals[i * 3 + 0]; v[4] = normals[i * 3 + 1]; v[5] = normals[i * 3 + 2]; } else { v[3] = 0; v[4] = 1; v[5] = 0; } // UV if (uvs) { v[6] = uvs[i * 2 + 0]; v[7] = uvs[i * 2 + 1]; } else { v[6] = 0; v[7] = 0; } // Color (handle both vec3 and vec4) if (colors) { if (color_components == 4) { v[8] = colors[i * 4 + 0]; v[9] = colors[i * 4 + 1]; v[10] = colors[i * 4 + 2]; v[11] = colors[i * 4 + 3]; } else { v[8] = colors[i * 3 + 0]; v[9] = colors[i * 3 + 1]; v[10] = colors[i * 3 + 2]; v[11] = 1.0f; // Default alpha for vec3 colors } } else { v[8] = 1; v[9] = 1; v[10] = 1; v[11] = 1; } // Joints and weights (for skinned meshes) if (skinned) { // Joints (stored as floats for shader compatibility) v[12] = joints[i * 4 + 0]; v[13] = joints[i * 4 + 1]; v[14] = joints[i * 4 + 2]; v[15] = joints[i * 4 + 3]; // Weights v[16] = weights[i * 4 + 0]; v[17] = weights[i * 4 + 1]; v[18] = weights[i * 4 + 2]; v[19] = weights[i * 4 + 3]; } } JS_FreeValue(js, pos_v); JS_FreeValue(js, norm_v); JS_FreeValue(js, uv_v); JS_FreeValue(js, color_v); JS_FreeValue(js, joints_v); JS_FreeValue(js, weights_v); JSValue result = JS_NewObject(js); JS_SetPropertyStr(js, result, "data", js_new_blob_stoned_copy(js, packed, total_size)); JS_SetPropertyStr(js, result, "stride", JS_NewInt32(js, stride)); JS_SetPropertyStr(js, result, "vertex_count", JS_NewInt32(js, vertex_count)); JS_SetPropertyStr(js, result, "skinned", JS_NewBool(js, skinned)); free(packed); return result; } // Build uniform buffer for retro3d rendering // Layout matches shader struct Uniforms (400 bytes = 100 floats): // float4x4 mvp [0-15] (64 bytes) // float4x4 model [16-31] (64 bytes) // float4x4 view [32-47] (64 bytes) // float4x4 projection [48-63] (64 bytes) // float4 ambient [64-67] (16 bytes) - rgb, unused // float4 light_dir [68-71] (16 bytes) - xyz, unused // float4 light_color [72-75] (16 bytes) - rgb, intensity // float4 fog_params [76-79] (16 bytes) - near, far, unused, enabled // float4 fog_color [80-83] (16 bytes) - rgb, unused // float4 tint [84-87] (16 bytes) - rgba (base_color_factor) // float4 style_params [88-91] (16 bytes) - style_id, vertex_snap, affine, dither // float4 resolution [92-95] (16 bytes) - w, h, unused, unused // float4 material_params [96-99] (16 bytes) - alpha_mode, alpha_cutoff, unlit, unused JSValue js_model_build_uniforms(JSContext *js, JSValue this_val, int argc, JSValueConst *argv) { if (argc < 1) return JS_ThrowTypeError(js, "build_uniforms requires params object"); JSValue params = argv[0]; // Allocate uniform buffer (400 bytes = 100 floats) float uniforms[100] = {0}; // Get matrices JSValue model_v = JS_GetPropertyStr(js, params, "model"); JSValue view_v = JS_GetPropertyStr(js, params, "view"); JSValue proj_v = JS_GetPropertyStr(js, params, "projection"); size_t size; float *model_m = JS_IsNull(model_v) ? NULL : js_get_blob_data(js, &size, model_v); float *view_m = JS_IsNull(view_v) ? NULL : js_get_blob_data(js, &size, view_v); float *proj_m = JS_IsNull(proj_v) ? NULL : js_get_blob_data(js, &size, proj_v); // Compute MVP mat4 model = model_m ? *(mat4*)model_m : mat4_identity(); mat4 view = view_m ? *(mat4*)view_m : mat4_identity(); mat4 proj = proj_m ? *(mat4*)proj_m : mat4_identity(); mat4 mvp = mat4_mul(proj, mat4_mul(view, model)); // MVP at offset 0-15 memcpy(&uniforms[0], mvp.m, 64); // Model at offset 16-31 memcpy(&uniforms[16], model.m, 64); // View at offset 32-47 memcpy(&uniforms[32], view.m, 64); // Projection at offset 48-63 memcpy(&uniforms[48], proj.m, 64); JS_FreeValue(js, model_v); JS_FreeValue(js, view_v); JS_FreeValue(js, proj_v); // Ambient color at offset 64-67 (rgb, unused) JSValue ambient_v = JS_GetPropertyStr(js, params, "ambient"); if (!JS_IsNull(ambient_v)) { for (int i = 0; i < 3; i++) { JSValue c = JS_GetPropertyUint32(js, ambient_v, i); double val = 0; JS_ToFloat64(js, &val, c); uniforms[64 + i] = val; JS_FreeValue(js, c); } } else { uniforms[64] = 0.2f; uniforms[65] = 0.2f; uniforms[66] = 0.2f; } uniforms[67] = 0.0f; // unused JS_FreeValue(js, ambient_v); // Light direction at offset 68-71 (xyz, unused) JSValue light_dir_v = JS_GetPropertyStr(js, params, "light_dir"); if (!JS_IsNull(light_dir_v)) { for (int i = 0; i < 3; i++) { JSValue c = JS_GetPropertyUint32(js, light_dir_v, i); double val = 0; JS_ToFloat64(js, &val, c); uniforms[68 + i] = val; JS_FreeValue(js, c); } } else { uniforms[68] = 0.5f; uniforms[69] = 1.0f; uniforms[70] = 0.3f; } uniforms[71] = 0.0f; // unused JS_FreeValue(js, light_dir_v); // Light color at offset 72-75 (rgb, intensity) JSValue light_color_v = JS_GetPropertyStr(js, params, "light_color"); if (!JS_IsNull(light_color_v)) { for (int i = 0; i < 3; i++) { JSValue c = JS_GetPropertyUint32(js, light_color_v, i); double val = 0; JS_ToFloat64(js, &val, c); uniforms[72 + i] = val; JS_FreeValue(js, c); } } else { uniforms[72] = 1.0f; uniforms[73] = 1.0f; uniforms[74] = 1.0f; } JS_FreeValue(js, light_color_v); // Light intensity at offset 75 JSValue light_int_v = JS_GetPropertyStr(js, params, "light_intensity"); double light_int = 1.0; JS_ToFloat64(js, &light_int, light_int_v); uniforms[75] = light_int; JS_FreeValue(js, light_int_v); // Fog params at offset 76-79 (near, far, unused, enabled) JSValue fog_near_v = JS_GetPropertyStr(js, params, "fog_near"); JSValue fog_far_v = JS_GetPropertyStr(js, params, "fog_far"); JSValue fog_color_v = JS_GetPropertyStr(js, params, "fog_color"); double fog_near = 10.0, fog_far = 100.0; JS_ToFloat64(js, &fog_near, fog_near_v); JS_ToFloat64(js, &fog_far, fog_far_v); uniforms[76] = fog_near; uniforms[77] = fog_far; uniforms[78] = 0.0f; // unused uniforms[79] = JS_IsNull(fog_color_v) ? 0.0f : 1.0f; // enabled flag // Fog color at offset 80-83 (rgb, unused) if (!JS_IsNull(fog_color_v)) { for (int i = 0; i < 3; i++) { JSValue c = JS_GetPropertyUint32(js, fog_color_v, i); double val = 0; JS_ToFloat64(js, &val, c); uniforms[80 + i] = val; JS_FreeValue(js, c); } } else { uniforms[80] = 0; uniforms[81] = 0; uniforms[82] = 0; } uniforms[83] = 0.0f; // unused JS_FreeValue(js, fog_near_v); JS_FreeValue(js, fog_far_v); JS_FreeValue(js, fog_color_v); // Tint color at offset 84-87 (rgba) JSValue tint_v = JS_GetPropertyStr(js, params, "tint"); if (!JS_IsNull(tint_v)) { for (int i = 0; i < 4; i++) { JSValue c = JS_GetPropertyUint32(js, tint_v, i); double val = 1.0; JS_ToFloat64(js, &val, c); uniforms[84 + i] = val; JS_FreeValue(js, c); } } else { uniforms[84] = 1; uniforms[85] = 1; uniforms[86] = 1; uniforms[87] = 1; } JS_FreeValue(js, tint_v); // Style params at offset 88-91 (style_id, vertex_snap, affine, dither) JSValue style_v = JS_GetPropertyStr(js, params, "style_id"); double style_id = 0; JS_ToFloat64(js, &style_id, style_v); uniforms[88] = style_id; JS_FreeValue(js, style_v); uniforms[89] = (style_id == 0) ? 1.0f : 0.0f; // vertex_snap for PS1 uniforms[90] = (style_id == -1) ? 1.0f : 0.0f; // affine texturing for PS1 uniforms[91] = (style_id == 2) ? 1.0f : 0.0f; // dither for Saturn // Resolution at offset 92-95 (w, h, unused, unused) JSValue res_w_v = JS_GetPropertyStr(js, params, "resolution_w"); JSValue res_h_v = JS_GetPropertyStr(js, params, "resolution_h"); double res_w = 320, res_h = 240; JS_ToFloat64(js, &res_w, res_w_v); JS_ToFloat64(js, &res_h, res_h_v); uniforms[92] = res_w; uniforms[93] = res_h; uniforms[94] = 0.0f; // unused uniforms[95] = 0.0f; // unused JS_FreeValue(js, res_w_v); JS_FreeValue(js, res_h_v); // Material params at offset 96-99 (alpha_mode, alpha_cutoff, unlit, unused) // alpha_mode: 0=OPAQUE, 1=MASK, 2=BLEND JSValue alpha_mode_v = JS_GetPropertyStr(js, params, "alpha_mode"); JSValue alpha_cutoff_v = JS_GetPropertyStr(js, params, "alpha_cutoff"); JSValue unlit_v = JS_GetPropertyStr(js, params, "unlit"); double alpha_mode = 0.0; // default OPAQUE double alpha_cutoff = 0.5; // glTF default double unlit_d = 0.0; if (!JS_IsNull(alpha_mode_v)) { JS_ToFloat64(js, &alpha_mode, alpha_mode_v); } if (!JS_IsNull(alpha_cutoff_v)) { JS_ToFloat64(js, &alpha_cutoff, alpha_cutoff_v); } if (!JS_IsNull(unlit_v)) { JS_ToFloat64(js, &unlit_d, unlit_v); } uniforms[96] = (float)alpha_mode; uniforms[97] = (float)alpha_cutoff; uniforms[98] = (float)unlit_d; uniforms[99] = 0.0f; // unused JS_FreeValue(js, alpha_mode_v); JS_FreeValue(js, alpha_cutoff_v); JS_FreeValue(js, unlit_v); return js_new_blob_stoned_copy(js, uniforms, sizeof(uniforms)); } // Pack JS array of numbers into a float32 blob JSValue js_model_f32_blob(JSContext *js, JSValue this_val, int argc, JSValueConst *argv) { if (argc < 1 || !JS_IsArray(js, argv[0])) return JS_ThrowTypeError(js, "f32_blob requires an array"); JSValue arr = argv[0]; int len = JS_ArrayLength(js, arr); if (len < 0) len = 0; float *data = malloc(sizeof(float) * (size_t)len); if (!data) return JS_ThrowOutOfMemory(js); for (int i = 0; i < len; i++) { JSValue v = JS_GetPropertyUint32(js, arr, (uint32_t)i); double d = 0.0; JS_ToFloat64(js, &d, v); JS_FreeValue(js, v); data[i] = (float)d; } JSValue ret = js_new_blob_stoned_copy(js, data, sizeof(float) * (size_t)len); free(data); return ret; } // Quaternion normalize static quat quat_normalize(quat q) { float len = sqrtf(q.x*q.x + q.y*q.y + q.z*q.z + q.w*q.w); if (len > 0.0001f) { q.x /= len; q.y /= len; q.z /= len; q.w /= len; } return q; } // Quaternion slerp static quat quat_slerp(quat a, quat b, float t) { // Normalize inputs a = quat_normalize(a); b = quat_normalize(b); // Compute dot product float dot = a.x*b.x + a.y*b.y + a.z*b.z + a.w*b.w; // If dot < 0, negate one quaternion to take shorter path if (dot < 0.0f) { b.x = -b.x; b.y = -b.y; b.z = -b.z; b.w = -b.w; dot = -dot; } // If very close, use linear interpolation if (dot > 0.9995f) { quat r; r.x = a.x + t * (b.x - a.x); r.y = a.y + t * (b.y - a.y); r.z = a.z + t * (b.z - a.z); r.w = a.w + t * (b.w - a.w); return quat_normalize(r); } float theta_0 = acosf(dot); float theta = theta_0 * t; float sin_theta = sinf(theta); float sin_theta_0 = sinf(theta_0); float s0 = cosf(theta) - dot * sin_theta / sin_theta_0; float s1 = sin_theta / sin_theta_0; quat r; r.x = s0 * a.x + s1 * b.x; r.y = s0 * a.y + s1 * b.y; r.z = s0 * a.z + s1 * b.z; r.w = s0 * a.w + s1 * b.w; return r; } // Binary search for keyframe index static int find_keyframe(const float *times, int count, float t) { if (count == 0) return 0; if (t <= times[0]) return 0; if (t >= times[count - 1]) return count - 1; int lo = 0, hi = count - 1; while (lo < hi - 1) { int mid = (lo + hi) / 2; if (times[mid] <= t) lo = mid; else hi = mid; } return lo; } // Sample vec3 animation track // Args: times_blob, values_blob, count, t, interpolation ("LINEAR", "STEP") // Returns: [x, y, z] JSValue js_model_sample_vec3(JSContext *js, JSValue this_val, int argc, JSValueConst *argv) { if (argc < 5) return JS_ThrowTypeError(js, "sample_vec3 requires 5 arguments"); size_t times_size, values_size; float *times = js_get_blob_data(js, ×_size, argv[0]); float *values = js_get_blob_data(js, &values_size, argv[1]); if (!times || !values) return JS_ThrowTypeError(js, "invalid blobs"); int count; double t_d; JS_ToInt32(js, &count, argv[2]); JS_ToFloat64(js, &t_d, argv[3]); float t = (float)t_d; const char *interp = JS_ToCString(js, argv[4]); int is_step = interp && strcmp(interp, "STEP") == 0; JS_FreeCString(js, interp); if (count <= 0) { JSValue arr = JS_NewArray(js); JS_SetPropertyUint32(js, arr, 0, JS_NewFloat64(js, 0)); JS_SetPropertyUint32(js, arr, 1, JS_NewFloat64(js, 0)); JS_SetPropertyUint32(js, arr, 2, JS_NewFloat64(js, 0)); return arr; } int idx = find_keyframe(times, count, t); float x, y, z; if (is_step || idx >= count - 1) { // Use value at idx directly x = values[idx * 3 + 0]; y = values[idx * 3 + 1]; z = values[idx * 3 + 2]; } else { // Linear interpolation float t0 = times[idx]; float t1 = times[idx + 1]; float factor = (t1 > t0) ? (t - t0) / (t1 - t0) : 0.0f; if (factor < 0) factor = 0; if (factor > 1) factor = 1; float *v0 = &values[idx * 3]; float *v1 = &values[(idx + 1) * 3]; x = v0[0] + factor * (v1[0] - v0[0]); y = v0[1] + factor * (v1[1] - v0[1]); z = v0[2] + factor * (v1[2] - v0[2]); } JSValue arr = JS_NewArray(js); JS_SetPropertyUint32(js, arr, 0, JS_NewFloat64(js, x)); JS_SetPropertyUint32(js, arr, 1, JS_NewFloat64(js, y)); JS_SetPropertyUint32(js, arr, 2, JS_NewFloat64(js, z)); return arr; } // Sample quaternion animation track (with slerp for LINEAR) // Args: times_blob, values_blob, count, t, interpolation ("LINEAR", "STEP") // Returns: [x, y, z, w] JSValue js_model_sample_quat(JSContext *js, JSValue this_val, int argc, JSValueConst *argv) { if (argc < 5) return JS_ThrowTypeError(js, "sample_quat requires 5 arguments"); size_t times_size, values_size; float *times = js_get_blob_data(js, ×_size, argv[0]); float *values = js_get_blob_data(js, &values_size, argv[1]); if (!times || !values) return JS_ThrowTypeError(js, "invalid blobs"); int count; double t_d; JS_ToInt32(js, &count, argv[2]); JS_ToFloat64(js, &t_d, argv[3]); float t = (float)t_d; const char *interp = JS_ToCString(js, argv[4]); int is_step = interp && strcmp(interp, "STEP") == 0; JS_FreeCString(js, interp); if (count <= 0) { JSValue arr = JS_NewArray(js); JS_SetPropertyUint32(js, arr, 0, JS_NewFloat64(js, 0)); JS_SetPropertyUint32(js, arr, 1, JS_NewFloat64(js, 0)); JS_SetPropertyUint32(js, arr, 2, JS_NewFloat64(js, 0)); JS_SetPropertyUint32(js, arr, 3, JS_NewFloat64(js, 1)); return arr; } int idx = find_keyframe(times, count, t); quat result; if (is_step || idx >= count - 1) { result.x = values[idx * 4 + 0]; result.y = values[idx * 4 + 1]; result.z = values[idx * 4 + 2]; result.w = values[idx * 4 + 3]; } else { // Slerp interpolation float t0 = times[idx]; float t1 = times[idx + 1]; float factor = (t1 > t0) ? (t - t0) / (t1 - t0) : 0.0f; if (factor < 0) factor = 0; if (factor > 1) factor = 1; quat q0 = { values[idx * 4 + 0], values[idx * 4 + 1], values[idx * 4 + 2], values[idx * 4 + 3] }; quat q1 = { values[(idx + 1) * 4 + 0], values[(idx + 1) * 4 + 1], values[(idx + 1) * 4 + 2], values[(idx + 1) * 4 + 3] }; result = quat_slerp(q0, q1, factor); } JSValue arr = JS_NewArray(js); JS_SetPropertyUint32(js, arr, 0, JS_NewFloat64(js, result.x)); JS_SetPropertyUint32(js, arr, 1, JS_NewFloat64(js, result.y)); JS_SetPropertyUint32(js, arr, 2, JS_NewFloat64(js, result.z)); JS_SetPropertyUint32(js, arr, 3, JS_NewFloat64(js, result.w)); return arr; } // Build joint palette for skinning // Args: joint_world_matrices (array of mat4 blobs), inv_bind_blob, joint_count // Returns: palette blob (joint_count * 64 bytes) JSValue js_model_build_joint_palette(JSContext *js, JSValue this_val, int argc, JSValueConst *argv) { if (argc < 3) return JS_ThrowTypeError(js, "build_joint_palette requires 3 arguments"); JSValue worlds_arr = argv[0]; if (!JS_IsArray(js, worlds_arr)) return JS_ThrowTypeError(js, "first arg must be array of world matrices"); size_t inv_bind_size; float *inv_bind = js_get_blob_data(js, &inv_bind_size, argv[1]); if (!inv_bind) return JS_ThrowTypeError(js, "invalid inv_bind blob"); int joint_count; JS_ToInt32(js, &joint_count, argv[2]); if (joint_count <= 0) return JS_ThrowTypeError(js, "invalid joint_count"); // Allocate palette size_t palette_size = joint_count * 64; float *palette = malloc(palette_size); if (!palette) return JS_ThrowOutOfMemory(js); for (int j = 0; j < joint_count; j++) { JSValue world_v = JS_GetPropertyUint32(js, worlds_arr, j); size_t world_size; float *world_m = js_get_blob_data(js, &world_size, world_v); JS_FreeValue(js, world_v); mat4 world, inv; if (world_m && world_size >= 64) { memcpy(world.m, world_m, 64); } else { world = mat4_identity(); } memcpy(inv.m, &inv_bind[j * 16], 64); // S[j] = world * inv_bind mat4 skin_mat = mat4_mul(world, inv); memcpy(&palette[j * 16], skin_mat.m, 64); } JSValue ret = js_new_blob_stoned_copy(js, palette, palette_size); free(palette); return ret; } // Get node world matrix from model (helper for attachments) // Args: world_matrices_array, node_index // Returns: mat4 blob JSValue js_model_get_node_world(JSContext *js, JSValue this_val, int argc, JSValueConst *argv) { if (argc < 2) return JS_ThrowTypeError(js, "get_node_world requires 2 arguments"); if (!JS_IsArray(js, argv[0])) return JS_ThrowTypeError(js, "first arg must be array"); int node_idx; JS_ToInt32(js, &node_idx, argv[1]); JSValue mat_v = JS_GetPropertyUint32(js, argv[0], node_idx); if (JS_IsNull(mat_v)) { JS_FreeValue(js, mat_v); mat4 id = mat4_identity(); return js_new_blob_stoned_copy(js, id.m, 64); } // Return a copy size_t size; float *data = js_get_blob_data(js, &size, mat_v); JS_FreeValue(js, mat_v); if (!data || size < 64) { mat4 id = mat4_identity(); return js_new_blob_stoned_copy(js, id.m, 64); } return js_new_blob_stoned_copy(js, data, 64); } // Matrix inversion (for computing inverse bind matrices if needed) static mat4 mat4_invert(mat4 m) { float *a = m.m; mat4 out; float *o = out.m; float a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3]; float a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7]; float a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11]; float a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15]; float b00 = a00 * a11 - a01 * a10; float b01 = a00 * a12 - a02 * a10; float b02 = a00 * a13 - a03 * a10; float b03 = a01 * a12 - a02 * a11; float b04 = a01 * a13 - a03 * a11; float b05 = a02 * a13 - a03 * a12; float b06 = a20 * a31 - a21 * a30; float b07 = a20 * a32 - a22 * a30; float b08 = a20 * a33 - a23 * a30; float b09 = a21 * a32 - a22 * a31; float b10 = a21 * a33 - a23 * a31; float b11 = a22 * a33 - a23 * a32; float det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; if (fabsf(det) < 0.00001f) { return mat4_identity(); } det = 1.0f / det; o[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det; o[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det; o[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det; o[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det; o[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det; o[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det; o[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det; o[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det; o[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det; o[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det; o[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det; o[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det; o[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det; o[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det; o[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det; o[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det; return out; } // Invert a mat4 blob JSValue js_model_mat4_invert(JSContext *js, JSValue this_val, int argc, JSValueConst *argv) { if (argc < 1) return JS_ThrowTypeError(js, "mat4_invert requires 1 argument"); size_t size; float *data = js_get_blob_data(js, &size, argv[0]); if (!data || size < 64) return JS_ThrowTypeError(js, "invalid mat4 blob"); mat4 m; memcpy(m.m, data, 64); mat4 inv = mat4_invert(m); return js_new_blob_stoned_copy(js, inv.m, 64); } // Pack JS array of numbers into a uint16 blob (little-endian) JSValue js_model_u16_blob(JSContext *js, JSValue this_val, int argc, JSValueConst *argv) { if (argc < 1 || !JS_IsArray(js, argv[0])) return JS_ThrowTypeError(js, "u16_blob requires an array"); JSValue arr = argv[0]; int len = JS_ArrayLength(js, arr); if (len < 0) len = 0; uint16_t *data = malloc(sizeof(uint16_t) * (size_t)len); if (!data) return JS_ThrowOutOfMemory(js); for (int i = 0; i < len; i++) { JSValue v = JS_GetPropertyUint32(js, arr, (uint32_t)i); uint32_t u = 0; JS_ToUint32(js, &u, v); JS_FreeValue(js, v); if (u > 0xFFFF) u = 0xFFFF; data[i] = (uint16_t)u; } JSValue ret = js_new_blob_stoned_copy(js, data, sizeof(uint16_t) * (size_t)len); free(data); return ret; } static const JSCFunctionListEntry js_model_funcs[] = { MIST_FUNC_DEF(model, compute_view_matrix, 9), MIST_FUNC_DEF(model, compute_perspective, 4), MIST_FUNC_DEF(model, compute_ortho, 6), MIST_FUNC_DEF(model, mat4_mul, 2), MIST_FUNC_DEF(model, mat4_identity, 0), MIST_FUNC_DEF(model, mat4_from_trs, 10), MIST_FUNC_DEF(model, mat4_from_array, 1), MIST_FUNC_DEF(model, mat4_invert, 1), MIST_FUNC_DEF(model, extract_accessor, 7), MIST_FUNC_DEF(model, extract_indices, 5), MIST_FUNC_DEF(model, pack_vertices, 1), MIST_FUNC_DEF(model, build_uniforms, 1), MIST_FUNC_DEF(model, f32_blob, 1), MIST_FUNC_DEF(model, u16_blob, 1), MIST_FUNC_DEF(model, sample_vec3, 5), MIST_FUNC_DEF(model, sample_quat, 5), MIST_FUNC_DEF(model, build_joint_palette, 3), MIST_FUNC_DEF(model, get_node_world, 2), }; CELL_USE_FUNCS(js_model_funcs)