model viewer
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
[dependencies]
|
||||
mload = "gitea.pockle.world/john/cell-model"
|
||||
mload = "/Users/john/work/cell-model"
|
||||
sdl3 = "gitea.pockle.world/john/cell-sdl3"
|
||||
|
||||
163
examples/cube.ce
Normal file
163
examples/cube.ce
Normal 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
200
examples/modelview.ce
Normal 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
606
model.c
@@ -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
139
shaders/retro3d.frag.msl
Normal 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
101
shaders/retro3d.vert.msl
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user