Files
retro3d/model.c
2025-12-13 19:37:30 -06:00

1176 lines
38 KiB
C

#include "cell.h"
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <stdint.h>
// 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, &times_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, &times_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)