model viewer

This commit is contained in:
2025-12-12 17:27:12 -06:00
parent d5f1cff090
commit 42f7048a56
7 changed files with 2421 additions and 1 deletions

View File

@@ -1,3 +1,3 @@
[dependencies] [dependencies]
mload = "gitea.pockle.world/john/cell-model" mload = "/Users/john/work/cell-model"
sdl3 = "gitea.pockle.world/john/cell-sdl3" sdl3 = "gitea.pockle.world/john/cell-sdl3"

1211
core.cm Normal file

File diff suppressed because it is too large Load Diff

163
examples/cube.ce Normal file
View File

@@ -0,0 +1,163 @@
// Simple Cube Demo for retro3d
// Usage: cell run examples/cube.ce [style]
// style: ps1, n64, or saturn (default: ps1)
var time_mod = use('time')
var retro3d = use('core')
// Parse command line arguments
var args = $_.args || []
var style = args[0] || "ps1"
// Camera orbit state
var cam_distance = 5
var cam_yaw = 0
var cam_pitch = 0.4
var auto_rotate = true
// Model and transform
var cube = null
var transform = null
// Timing
var last_time = 0
function _init() {
log.console("retro3d Cube Demo")
log.console("Style: " + style)
// Initialize retro3d with selected style
retro3d.set_style(style)
// Create a cube
cube = retro3d.make_cube(1, 1, 1)
// Create transform for the cube
transform = retro3d.make_transform()
transform.y = 0.5
// Set up lighting
retro3d.set_ambient(0.3, 0.3, 0.35)
retro3d.set_light_dir(0.5, 1.0, 0.3, 1.0, 0.95, 0.9, 1.0)
// Set up a default material
var mat = retro3d.make_material("lit", {
color: [0.8, 0.3, 0.2, 1]
})
retro3d.set_material(mat)
last_time = time_mod.number()
log.console("")
log.console("Controls:")
log.console(" WASD - Orbit camera")
log.console(" Space - Toggle auto-rotate")
log.console(" ESC - Exit")
// Start the main loop
frame()
}
function _update(dt) {
// Auto rotate
if (auto_rotate) {
cam_yaw += 0.5 * dt
}
// Handle input for camera orbit
if (retro3d._state.keys_held['a']) {
cam_yaw -= 2.0 * dt
auto_rotate = false
}
if (retro3d._state.keys_held['d']) {
cam_yaw += 2.0 * dt
auto_rotate = false
}
if (retro3d._state.keys_held['w']) {
cam_pitch += 2.0 * dt
if (cam_pitch > 1.5) cam_pitch = 1.5
}
if (retro3d._state.keys_held['s']) {
cam_pitch -= 2.0 * dt
if (cam_pitch < -1.5) cam_pitch = -1.5
}
// Toggle auto-rotate
if (retro3d._state.keys_pressed[' ']) {
auto_rotate = !auto_rotate
}
// Exit on escape
if (retro3d._state.keys_held['escape']) {
$_.stop()
}
}
function _draw() {
// Clear with style-appropriate color
if (style == "ps1") {
retro3d.clear(0.1, 0.05, 0.15, 1.0)
} else if (style == "n64") {
retro3d.clear(0.0, 0.1, 0.2, 1.0)
} else {
retro3d.clear(0.05, 0.05, 0.1, 1.0)
}
// Set up camera
retro3d.camera_perspective(60, 0.1, 100)
// Calculate camera position from orbit
var cam_x = Math.sin(cam_yaw) * Math.cos(cam_pitch) * cam_distance
var cam_y = Math.sin(cam_pitch) * cam_distance + 0.5
var cam_z = Math.cos(cam_yaw) * Math.cos(cam_pitch) * cam_distance
retro3d.camera_look_at(cam_x, cam_y, cam_z, 0, 0.5, 0)
// Draw the cube
retro3d.draw_model(cube, transform)
// Draw ground plane
retro3d.push_state()
var ground_mat = retro3d.make_material("lit", {
color: [0.3, 0.5, 0.3, 1]
})
retro3d.set_material(ground_mat)
var ground = retro3d.make_plane(10, 10)
var ground_transform = retro3d.make_transform()
retro3d.draw_model(ground, ground_transform)
retro3d.pop_state()
}
function frame() {
// Begin frame
retro3d._begin_frame()
// Process events
if (!retro3d._process_events()) {
log.console("Exiting...")
$_.stop()
return
}
// Calculate delta time
var now = time_mod.number()
var dt = now - last_time
last_time = now
// Update
_update(dt)
// Draw
_draw()
// End frame (submit GPU commands)
retro3d._end_frame()
// Schedule next frame
$_.delay(frame, 1/60)
}
// Start
_init()

200
examples/modelview.ce Normal file
View File

@@ -0,0 +1,200 @@
// Model Viewer for retro3d
// Usage: cell run examples/modelview.ce <model_path> [style]
// style: ps1, n64, or saturn (default: ps1)
var io = use('fd')
var time_mod = use('time')
var retro3d = use('core')
// Parse command line arguments
var model_path = args[0]
var style = args[1] || "ps1"
if (!model_path) {
log.console("Usage: cell run examples/modelview.ce <model_path> [style]")
log.console(" style: ps1, n64, or saturn (default: ps1)")
log.console("Example: cell run examples/modelview.ce mymodel.glb ps1")
$_.stop()
}
// Camera orbit state
var cam_distance = 5
var cam_yaw = 0
var cam_pitch = 0.3
var cam_target_y = 0
var orbit_speed = 2.0
var zoom_speed = 0.5
// Model and transform
var model = null
var transform = null
// Timing
var last_time = 0
function _init() {
log.console("retro3d Model Viewer")
log.console("Style: " + style)
log.console("Loading: " + model_path)
// Initialize retro3d with selected style
retro3d.set_style(style)
// Load the model
model = retro3d.load_model(model_path)
if (!model) {
log.console("Error: Could not load model: " + model_path)
$_.stop()
return
}
log.console("Model loaded with " + text(model.meshes.length) + " mesh(es)")
// Create transform for the model
transform = retro3d.make_transform()
// Set up lighting
retro3d.set_ambient(0.3, 0.3, 0.35)
retro3d.set_light_dir(0.5, 1.0, 0.3, 1.0, 0.95, 0.9, 1.0)
// Set up a default material
var mat = retro3d.make_material("lit", {
color: [1, 1, 1, 1]
})
retro3d.set_material(mat)
last_time = time_mod.number()
log.console("")
log.console("Controls:")
log.console(" WASD - Orbit camera")
log.console(" Q/E - Zoom in/out")
log.console(" R/F - Move target up/down")
log.console(" ESC - Exit")
// Start the main loop
frame()
}
function _update(dt) {
// Handle input for camera orbit
if (retro3d._state.keys_held['a']) {
cam_yaw -= orbit_speed * dt
}
if (retro3d._state.keys_held['d']) {
cam_yaw += orbit_speed * dt
}
if (retro3d._state.keys_held['w']) {
cam_pitch += orbit_speed * dt
if (cam_pitch > 1.5) cam_pitch = 1.5
}
if (retro3d._state.keys_held['s']) {
cam_pitch -= orbit_speed * dt
if (cam_pitch < -1.5) cam_pitch = -1.5
}
// Zoom
if (retro3d._state.keys_held['q']) {
cam_distance -= zoom_speed * dt * cam_distance
if (cam_distance < 0.5) cam_distance = 0.5
}
if (retro3d._state.keys_held['e']) {
cam_distance += zoom_speed * dt * cam_distance
if (cam_distance > 100) cam_distance = 100
}
// Move target up/down
if (retro3d._state.keys_held['r']) {
cam_target_y += zoom_speed * dt
}
if (retro3d._state.keys_held['f']) {
cam_target_y -= zoom_speed * dt
}
// Exit on escape
if (retro3d._state.keys_held['escape']) {
$_.stop()
}
}
function _draw() {
// Clear with a nice gradient-ish color based on style
if (style == "ps1") {
retro3d.clear(0.1, 0.05, 0.15, 1.0)
} else if (style == "n64") {
retro3d.clear(0.0, 0.1, 0.2, 1.0)
} else {
retro3d.clear(0.05, 0.05, 0.1, 1.0)
}
// Set up camera
retro3d.camera_perspective(60, 0.1, 100)
// Calculate camera position from orbit
var cam_x = Math.sin(cam_yaw) * Math.cos(cam_pitch) * cam_distance
var cam_y = Math.sin(cam_pitch) * cam_distance + cam_target_y
var cam_z = Math.cos(cam_yaw) * Math.cos(cam_pitch) * cam_distance
retro3d.camera_look_at(cam_x, cam_y, cam_z, 0, cam_target_y, 0)
// Draw the model
if (model) {
retro3d.draw_model(model, transform)
}
// Draw a ground grid using immediate mode
retro3d.push_state()
var grid_mat = retro3d.make_material("unlit", {
color: [0.3, 0.3, 0.3, 1]
})
retro3d.set_material(grid_mat)
retro3d.begin_lines()
retro3d.color(0.3, 0.3, 0.3, 1)
var grid_size = 10
var grid_step = 1
for (var i = -grid_size; i <= grid_size; i += grid_step) {
// X lines
retro3d.vertex(i, 0, -grid_size)
retro3d.vertex(i, 0, grid_size)
// Z lines
retro3d.vertex(-grid_size, 0, i)
retro3d.vertex(grid_size, 0, i)
}
retro3d.end()
retro3d.pop_state()
}
function frame() {
// Begin frame
retro3d._begin_frame()
// Process events
if (!retro3d._process_events()) {
log.console("Exiting...")
$_.stop()
return
}
// Calculate delta time
var now = time_mod.number()
var dt = now - last_time
last_time = now
// Update
_update(dt)
// Draw
_draw()
// End frame (submit GPU commands)
retro3d._end_frame()
// Schedule next frame
$_.delay(frame, 1/60)
}
// Start
_init()

606
model.c
View File

@@ -0,0 +1,606 @@
#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;
// 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
static mat4 mat4_mul(mat4 a, mat4 b) {
mat4 r = {0};
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
for (int k = 0; k < 4; k++) {
r.m[i * 4 + j] += a.m[i * 4 + k] * b.m[k * 4 + j];
}
}
}
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;
}
// 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 world matrix from transform object
// transform: { x, y, z, rot_x, rot_y, rot_z, scale_x, scale_y, scale_z, parent }
JSValue js_model_compute_world_matrix(JSContext *js, JSValue this_val, int argc, JSValueConst *argv)
{
if (argc < 1) return JS_ThrowTypeError(js, "compute_world_matrix requires a transform");
// Walk up parent chain, collecting transforms
mat4 matrices[32];
int depth = 0;
JSValue current = JS_DupValue(js, argv[0]);
while (!JS_IsNull(current) && depth < 32) {
JSValue x_v = JS_GetPropertyStr(js, current, "x");
JSValue y_v = JS_GetPropertyStr(js, current, "y");
JSValue z_v = JS_GetPropertyStr(js, current, "z");
JSValue rx_v = JS_GetPropertyStr(js, current, "rot_x");
JSValue ry_v = JS_GetPropertyStr(js, current, "rot_y");
JSValue rz_v = JS_GetPropertyStr(js, current, "rot_z");
JSValue sx_v = JS_GetPropertyStr(js, current, "scale_x");
JSValue sy_v = JS_GetPropertyStr(js, current, "scale_y");
JSValue sz_v = JS_GetPropertyStr(js, current, "scale_z");
double x = 0, y = 0, z = 0;
double rx = 0, ry = 0, rz = 0;
double sx = 1, sy = 1, sz = 1;
JS_ToFloat64(js, &x, x_v);
JS_ToFloat64(js, &y, y_v);
JS_ToFloat64(js, &z, z_v);
JS_ToFloat64(js, &rx, rx_v);
JS_ToFloat64(js, &ry, ry_v);
JS_ToFloat64(js, &rz, rz_v);
JS_ToFloat64(js, &sx, sx_v);
JS_ToFloat64(js, &sy, sy_v);
JS_ToFloat64(js, &sz, sz_v);
JS_FreeValue(js, x_v);
JS_FreeValue(js, y_v);
JS_FreeValue(js, z_v);
JS_FreeValue(js, rx_v);
JS_FreeValue(js, ry_v);
JS_FreeValue(js, rz_v);
JS_FreeValue(js, sx_v);
JS_FreeValue(js, sy_v);
JS_FreeValue(js, sz_v);
// Build local matrix: T * Rz * Ry * Rx * S
mat4 T = mat4_translate(x, y, z);
mat4 Rx = mat4_rotate_x(rx);
mat4 Ry = mat4_rotate_y(ry);
mat4 Rz = mat4_rotate_z(rz);
mat4 S = mat4_scale(sx, sy, sz);
mat4 local = mat4_mul(T, mat4_mul(Rz, mat4_mul(Ry, mat4_mul(Rx, S))));
matrices[depth++] = local;
JSValue parent = JS_GetPropertyStr(js, current, "parent");
JS_FreeValue(js, current);
current = parent;
}
JS_FreeValue(js, current);
// Multiply from root to leaf
mat4 world = mat4_identity();
for (int i = depth - 1; i >= 0; i--) {
world = mat4_mul(world, matrices[i]);
}
// Return as blob (64 bytes = 16 floats)
return js_new_blob_stoned_copy(js, world.m, sizeof(world.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));
}
// Pack interleaved vertex data for GPU
// Takes separate position, normal, uv, color blobs and packs into interleaved format
// Returns: { data: blob, stride: number }
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");
size_t pos_size, norm_size, uv_size, color_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);
if (!positions) {
JS_FreeValue(js, pos_v);
JS_FreeValue(js, norm_v);
JS_FreeValue(js, uv_v);
JS_FreeValue(js, color_v);
return JS_ThrowTypeError(js, "positions required");
}
// Interleaved format: pos(3) + normal(3) + uv(2) + color(4) = 12 floats = 48 bytes
int stride = 48;
size_t total_size = vertex_count * stride;
float *packed = malloc(total_size);
for (int i = 0; i < vertex_count; i++) {
float *v = &packed[i * 12];
// 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
if (colors) {
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] = 1; v[9] = 1; v[10] = 1; v[11] = 1;
}
}
JS_FreeValue(js, pos_v);
JS_FreeValue(js, norm_v);
JS_FreeValue(js, uv_v);
JS_FreeValue(js, color_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));
free(packed);
return result;
}
// Build uniform buffer for retro3d rendering
// Contains: MVP matrix (64), model matrix (64), view matrix (64), projection matrix (64)
// ambient (16), light_dir (16), light_color (16), fog params (16), tint (16)
// style params (16) = 352 bytes total, padded to 368 for alignment
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 (384 bytes for good alignment)
float uniforms[96] = {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
memcpy(&uniforms[0], mvp.m, 64);
// Model at offset 16
memcpy(&uniforms[16], model.m, 64);
// View at offset 32
memcpy(&uniforms[32], view.m, 64);
// Projection at offset 48
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
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] = 1.0f;
JS_FreeValue(js, ambient_v);
// Light direction at offset 68
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;
JS_FreeValue(js, light_dir_v);
// Light color at offset 72
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
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: near, far, r, g, b, 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;
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[78 + i] = val;
JS_FreeValue(js, c);
}
uniforms[81] = 1.0f; // fog enabled
} else {
uniforms[78] = 0; uniforms[79] = 0; uniforms[80] = 0;
uniforms[81] = 0.0f; // fog disabled
}
JS_FreeValue(js, fog_near_v);
JS_FreeValue(js, fog_far_v);
JS_FreeValue(js, fog_color_v);
// Tint color at offset 82
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[82 + i] = val;
JS_FreeValue(js, c);
}
} else {
uniforms[82] = 1; uniforms[83] = 1; uniforms[84] = 1; uniforms[85] = 1;
}
JS_FreeValue(js, tint_v);
// Style params at offset 86: style_id, vertex_snap, affine_amount, dither
JSValue style_v = JS_GetPropertyStr(js, params, "style_id");
double style_id = 0;
JS_ToFloat64(js, &style_id, style_v);
uniforms[86] = style_id;
JS_FreeValue(js, style_v);
// Style-specific params
uniforms[87] = (style_id == 0) ? 1.0f : 0.0f; // vertex_snap for PS1
uniforms[88] = (style_id == 0) ? 1.0f : 0.0f; // affine texturing for PS1
uniforms[89] = (style_id == 2) ? 1.0f : 0.0f; // dither for Saturn
// Resolution for vertex snapping
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[90] = res_w;
uniforms[91] = res_h;
JS_FreeValue(js, res_w_v);
JS_FreeValue(js, res_h_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;
}
// 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_world_matrix, 1),
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, pack_vertices, 1),
MIST_FUNC_DEF(model, build_uniforms, 1),
MIST_FUNC_DEF(model, f32_blob, 1),
MIST_FUNC_DEF(model, u16_blob, 1),
};
CELL_USE_FUNCS(js_model_funcs)

139
shaders/retro3d.frag.msl Normal file
View File

@@ -0,0 +1,139 @@
#include <metal_stdlib>
using namespace metal;
struct Uniforms {
float4x4 mvp;
float4x4 model;
float4x4 view;
float4x4 projection;
float4 ambient; // rgb, unused
float4 light_dir; // xyz normalized, unused
float4 light_color; // rgb, intensity
float4 fog_params; // near, far, unused, enabled
float4 fog_color; // rgb, unused
float4 tint; // rgba
float4 style_params; // style_id, vertex_snap, affine, dither
float4 resolution; // w, h, unused, unused
};
struct FragmentIn {
float4 position [[position]];
float3 world_normal;
float2 uv;
float4 color;
float fog_factor;
float3 noperspective_uv;
};
// Dither pattern for Saturn style (4x4 Bayer matrix)
constant float dither_matrix[16] = {
0.0/16.0, 8.0/16.0, 2.0/16.0, 10.0/16.0,
12.0/16.0, 4.0/16.0, 14.0/16.0, 6.0/16.0,
3.0/16.0, 11.0/16.0, 1.0/16.0, 9.0/16.0,
15.0/16.0, 7.0/16.0, 13.0/16.0, 5.0/16.0
};
float get_dither(float2 pos) {
int x = int(pos.x) % 4;
int y = int(pos.y) % 4;
return dither_matrix[y * 4 + x];
}
// Quantize color to lower bit depth
float3 quantize_color(float3 color, float bits) {
float levels = pow(2.0, bits) - 1.0;
return floor(color * levels + 0.5) / levels;
}
fragment float4 fragment_main(
FragmentIn in [[stage_in]],
constant Uniforms &uniforms [[buffer(1)]],
texture2d<float> tex [[texture(0)]],
sampler samp [[sampler(0)]]
) {
float style_id = uniforms.style_params.x;
float affine = uniforms.style_params.z;
float dither = uniforms.style_params.w;
// Get UV coordinates
float2 uv;
if (affine > 0.5) {
// Affine texturing (PS1 style) - divide by interpolated w
uv = in.noperspective_uv.xy / in.noperspective_uv.z;
} else {
uv = in.uv;
}
// Sample texture
float4 tex_color = tex.sample(samp, uv);
// Start with vertex color * texture
float4 base_color = in.color * tex_color;
// Lighting calculation
float3 normal = normalize(in.world_normal);
float3 light_dir = normalize(uniforms.light_dir.xyz);
float ndotl = max(dot(normal, light_dir), 0.0);
float3 ambient = uniforms.ambient.rgb;
float3 diffuse = uniforms.light_color.rgb * uniforms.light_color.w * ndotl;
float3 lighting = ambient + diffuse;
// Apply lighting
float3 lit_color = base_color.rgb * lighting;
// Apply tint
lit_color *= uniforms.tint.rgb;
float alpha = base_color.a * uniforms.tint.a;
// Style-specific processing
if (style_id < 0.5) {
// PS1 style: 15-bit color (5 bits per channel)
lit_color = quantize_color(lit_color, 5.0);
} else if (style_id < 1.5) {
// N64 style: smoother, 16-bit color with bilinear filtering
// (filtering is handled by sampler, just quantize slightly)
lit_color = quantize_color(lit_color, 5.0);
} else {
// Saturn style: dithered, flat shaded look
if (dither > 0.5) {
float d = get_dither(in.position.xy);
// Add dither before quantization
lit_color += (d - 0.5) * 0.1;
}
lit_color = quantize_color(lit_color, 5.0);
}
// Apply fog
float3 fog_color = uniforms.fog_color.rgb;
lit_color = mix(fog_color, lit_color, in.fog_factor);
return float4(lit_color, alpha);
}
// Unlit fragment shader for sprites/UI
fragment float4 fragment_unlit(
FragmentIn in [[stage_in]],
constant Uniforms &uniforms [[buffer(1)]],
texture2d<float> tex [[texture(0)]],
sampler samp [[sampler(0)]]
) {
float affine = uniforms.style_params.z;
float2 uv;
if (affine > 0.5) {
uv = in.noperspective_uv.xy / in.noperspective_uv.z;
} else {
uv = in.uv;
}
float4 tex_color = tex.sample(samp, uv);
float4 color = in.color * tex_color * uniforms.tint;
// Alpha test for sprites
if (color.a < 0.1) {
discard_fragment();
}
return color;
}

101
shaders/retro3d.vert.msl Normal file
View File

@@ -0,0 +1,101 @@
#include <metal_stdlib>
using namespace metal;
struct Uniforms {
float4x4 mvp;
float4x4 model;
float4x4 view;
float4x4 projection;
float4 ambient; // rgb, unused
float4 light_dir; // xyz normalized, unused
float4 light_color; // rgb, intensity
float4 fog_params; // near, far, unused, enabled
float4 fog_color; // rgb, unused
float4 tint; // rgba
float4 style_params; // style_id, vertex_snap, affine, dither
float4 resolution; // w, h, unused, unused
};
struct VertexIn {
float3 position [[attribute(0)]];
float3 normal [[attribute(1)]];
float2 uv [[attribute(2)]];
float4 color [[attribute(3)]];
};
struct VertexOut {
float4 position [[position]];
float3 world_normal;
float2 uv;
float4 color;
float fog_factor;
float3 noperspective_uv; // For affine texturing (PS1 style)
};
vertex VertexOut vertex_main(
VertexIn in [[stage_in]],
constant Uniforms &uniforms [[buffer(1)]]
) {
VertexOut out;
float4 world_pos = uniforms.model * float4(in.position, 1.0);
float4 clip_pos = uniforms.mvp * float4(in.position, 1.0);
// PS1-style vertex snapping
float style_id = uniforms.style_params.x;
float vertex_snap = uniforms.style_params.y;
if (vertex_snap > 0.5 && style_id < 0.5) {
// PS1 style: snap vertices to grid
float2 res = uniforms.resolution.xy;
float2 snap_res = res * 0.5;
// Snap in clip space after perspective divide
float4 snapped = clip_pos;
if (snapped.w > 0.0) {
float2 ndc = snapped.xy / snapped.w;
ndc = floor(ndc * snap_res + 0.5) / snap_res;
snapped.xy = ndc * snapped.w;
}
clip_pos = snapped;
}
out.position = clip_pos;
// Transform normal to world space
float3x3 normal_matrix = float3x3(
uniforms.model[0].xyz,
uniforms.model[1].xyz,
uniforms.model[2].xyz
);
out.world_normal = normalize(normal_matrix * in.normal);
// UV coordinates
out.uv = in.uv;
// For affine texturing (PS1), we pass UV * w to fragment shader
// and divide by interpolated w there
float affine = uniforms.style_params.z;
if (affine > 0.5) {
out.noperspective_uv = float3(in.uv * clip_pos.w, clip_pos.w);
} else {
out.noperspective_uv = float3(in.uv, 1.0);
}
// Vertex color
out.color = in.color;
// Fog calculation (linear fog)
float fog_enabled = uniforms.fog_params.w;
if (fog_enabled > 0.5) {
float4 view_pos = uniforms.view * world_pos;
float dist = length(view_pos.xyz);
float fog_near = uniforms.fog_params.x;
float fog_far = uniforms.fog_params.y;
out.fog_factor = clamp((fog_far - dist) / (fog_far - fog_near), 0.0, 1.0);
} else {
out.fog_factor = 1.0;
}
return out;
}