1176 lines
38 KiB
C
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, ×_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)
|