fixed render

This commit is contained in:
2025-12-13 00:46:00 -06:00
parent 42f7048a56
commit 863bc7fe9b
4 changed files with 742 additions and 192 deletions

View File

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

517
core.cm
View File

@@ -1,7 +1,7 @@
// retro3d fantasy game console
var io = use('fd')
var time_mod = use('time')
var blob = use('blob')
var blob_mod = use('blob')
var video = use('sdl3/video')
var gpu_mod = use('sdl3/gpu')
var events = use('sdl3/events')
@@ -9,6 +9,7 @@ var keyboard = use('sdl3/keyboard')
var gltf = use('mload/gltf')
var obj_loader = use('mload/obj')
var model_c = use('model')
var png = use('cell-image/png')
// Internal state
var _state = {
@@ -166,47 +167,274 @@ function log_msg() {
function load_model(path) {
var data = io.slurp(path)
if (!data) return null
var ext = path.slice(path.lastIndexOf('.') + 1).toLowerCase()
var parsed = null
if (ext == "gltf" || ext == "glb") {
parsed = gltf.decode(data)
} else if (ext == "obj") {
parsed = obj_loader.decode(data)
} else {
if (ext == "obj") {
var parsed = obj_loader.decode(data)
if (!parsed) return null
return _load_obj_model(parsed)
}
if (ext != "gltf" && ext != "glb") {
log.console("retro3d: unsupported model format: " + ext)
return null
}
if (!parsed || parsed.mesh_count == 0) return null
// Process meshes into GPU-ready format
// Parse gltf
var g = gltf.decode(data)
if (!g) return null
// Get the main buffer blob
var buffer_blob = g.buffers[0] ? g.buffers[0].blob : null
if (!buffer_blob) {
log.console("retro3d: gltf has no buffer data")
return null
}
// Build model structure
var model = {
meshes: [],
animations: parsed.animations || [],
animation_count: parsed.animation_count || 0
nodes: [],
root_nodes: [],
textures: [],
materials: g.materials || [],
animations: g.animations || [],
animation_count: g.animations ? g.animations.length : 0,
_gltf: g
}
// Load textures from embedded images
for (var ti = 0; ti < g.images.length; ti++) {
var img = g.images[ti]
var tex = null
if (img.kind == "buffer_view" && img.view != null) {
var view = g.views[img.view]
var img_data = _extract_buffer_view(buffer_blob, view)
if (img.mime == "image/png") {
var decoded = png.decode(img_data)
if (decoded) {
tex = _create_texture(decoded.width, decoded.height, decoded.pixels)
}
}
}
model.textures.push(tex)
}
// Build node transforms (preserving hierarchy)
for (var ni = 0; ni < g.nodes.length; ni++) {
var node = g.nodes[ni]
var t = null
if (node.matrix) {
t = make_transform({
local_mat: model_c.mat4_from_array(node.matrix),
has_local_mat: true
})
} else {
var trans = node.translation || [0, 0, 0]
var rot = node.rotation || [0, 0, 0, 1]
var scale = node.scale || [1, 1, 1]
t = make_transform({
x: trans[0], y: trans[1], z: trans[2],
qx: rot[0], qy: rot[1], qz: rot[2], qw: rot[3],
sx: scale[0], sy: scale[1], sz: scale[2]
})
}
t.mesh_index = node.mesh
t.name = node.name
t.gltf_children = node.children || []
model.nodes.push(t)
}
// Set up parent-child relationships
for (var ni = 0; ni < model.nodes.length; ni++) {
var t = model.nodes[ni]
for (var ci = 0; ci < t.gltf_children.length; ci++) {
var child_idx = t.gltf_children[ci]
if (child_idx < model.nodes.length) {
transform_set_parent(model.nodes[child_idx], t)
}
}
}
// Find root nodes (those without parents)
for (var ni = 0; ni < model.nodes.length; ni++) {
if (!model.nodes[ni].parent) {
model.root_nodes.push(model.nodes[ni])
}
}
// Process meshes
for (var mi = 0; mi < g.meshes.length; mi++) {
var mesh = g.meshes[mi]
for (var pi = 0; pi < mesh.primitives.length; pi++) {
var prim = mesh.primitives[pi]
var gpu_mesh = _process_gltf_primitive(g, buffer_blob, prim, model.textures)
if (gpu_mesh) {
gpu_mesh.name = mesh.name
gpu_mesh.mesh_index = mi
gpu_mesh.primitive_index = pi
model.meshes.push(gpu_mesh)
}
}
}
return model
}
function _extract_buffer_view(buffer_blob, view) {
// Extract a portion of the buffer as a new blob
var offset = view.byte_offset || 0
var length = view.byte_length
var newblob = buffer_blob.read_blob(offset*8, (offset + length)*8)
return stone(newblob)
}
function _process_gltf_primitive(g, buffer_blob, prim, textures) {
var attrs = prim.attributes
if (!attrs.POSITION) return null
// Get accessors
var pos_acc = g.accessors[attrs.POSITION]
var norm_acc = attrs.NORMAL != null ? g.accessors[attrs.NORMAL] : null
var uv_acc = attrs.TEXCOORD_0 != null ? g.accessors[attrs.TEXCOORD_0] : null
var idx_acc = prim.indices != null ? g.accessors[prim.indices] : null
var vertex_count = pos_acc.count
// Extract position data
var pos_view = g.views[pos_acc.view]
var positions = model_c.extract_accessor(
buffer_blob,
pos_view.byte_offset || 0,
pos_view.byte_stride || 0,
pos_acc.byte_offset || 0,
pos_acc.count,
pos_acc.component_type,
pos_acc.type
)
// Extract normals
var normals = null
if (norm_acc) {
var norm_view = g.views[norm_acc.view]
normals = model_c.extract_accessor(
buffer_blob,
norm_view.byte_offset || 0,
norm_view.byte_stride || 0,
norm_acc.byte_offset || 0,
norm_acc.count,
norm_acc.component_type,
norm_acc.type
)
}
// Extract UVs
var uvs = null
if (uv_acc) {
var uv_view = g.views[uv_acc.view]
uvs = model_c.extract_accessor(
buffer_blob,
uv_view.byte_offset || 0,
uv_view.byte_stride || 0,
uv_acc.byte_offset || 0,
uv_acc.count,
uv_acc.component_type,
uv_acc.type
)
}
// Extract indices
var indices = null
var index_count = 0
var index_type = "uint16"
if (idx_acc) {
var idx_view = g.views[idx_acc.view]
indices = model_c.extract_indices(
buffer_blob,
idx_view.byte_offset || 0,
idx_acc.byte_offset || 0,
idx_acc.count,
idx_acc.component_type
)
index_count = idx_acc.count
index_type = idx_acc.component_type == "u32" ? "uint32" : "uint16"
}
// Pack vertices
var mesh_data = {
vertex_count: vertex_count,
positions: positions,
normals: normals,
uvs: uvs,
colors: null
}
var packed = model_c.pack_vertices(mesh_data)
// Create GPU buffers
var vertex_buffer = _create_vertex_buffer(packed.data)
var index_buffer = indices ? _create_index_buffer(indices) : null
// Get material texture
var texture = null
if (prim.material != null && g.materials[prim.material]) {
var mat = g.materials[prim.material]
if (mat.pbr && mat.pbr.base_color_texture) {
var tex_info = mat.pbr.base_color_texture
var tex_obj = g.textures[tex_info.texture]
if (tex_obj && tex_obj.image != null && textures[tex_obj.image]) {
texture = textures[tex_obj.image]
}
}
}
return {
vertex_buffer: vertex_buffer,
index_buffer: index_buffer,
index_count: index_count,
index_type: index_type,
vertex_count: vertex_count,
material_index: prim.material,
texture: texture
}
}
function _load_obj_model(parsed) {
if (!parsed || !parsed.meshes || parsed.meshes.length == 0) return null
var model = {
meshes: [],
nodes: [],
root_nodes: [],
textures: [],
materials: [],
animations: [],
animation_count: 0
}
for (var i = 0; i < parsed.meshes.length; i++) {
var mesh = parsed.meshes[i]
var packed = model_c.pack_vertices(mesh)
// Create GPU buffers
var vertex_buffer = _create_vertex_buffer(packed.data)
var index_buffer = _create_index_buffer(mesh.indices)
model.meshes.push({
vertex_buffer: vertex_buffer,
index_buffer: index_buffer,
index_count: mesh.index_count,
index_type: mesh.index_type,
index_type: mesh.index_type || "uint16",
vertex_count: mesh.vertex_count,
material: mesh.material,
name: mesh.name
name: mesh.name,
texture: null
})
}
// Create a single root transform
var root = make_transform()
model.nodes.push(root)
model.root_nodes.push(root)
return model
}
@@ -379,18 +607,123 @@ function set_music_volume(v) {}
function set_sfx_volume(v) {}
// ============================================================================
// 3) Transforms
// 3) Transforms - Hierarchical with dirty flags
// ============================================================================
function make_transform() {
function make_transform(opts) {
opts = opts || {}
return {
x: 0, y: 0, z: 0,
rot_x: 0, rot_y: 0, rot_z: 0,
scale_x: 1, scale_y: 1, scale_z: 1,
parent: null
parent: opts.parent || null,
children: [],
// TRS (authoring) - translation
x: opts.x || 0,
y: opts.y || 0,
z: opts.z || 0,
// Quaternion rotation
qx: opts.qx || 0,
qy: opts.qy || 0,
qz: opts.qz || 0,
qw: opts.qw != null ? opts.qw : 1,
// Scale
sx: opts.sx != null ? opts.sx : 1,
sy: opts.sy != null ? opts.sy : 1,
sz: opts.sz != null ? opts.sz : 1,
// Matrices (opaque blobs)
local_mat: opts.local_mat || null,
world_mat: null,
// Authority: if true, local_mat is used directly, TRS ignored
has_local_mat: opts.has_local_mat || false,
// Cache invalidation
dirty_local: true,
dirty_world: true
}
}
function transform_set_parent(child, parent) {
if (child.parent) {
var idx = child.parent.children.indexOf(child)
if (idx >= 0) child.parent.children.splice(idx, 1)
}
child.parent = parent
if (parent) parent.children.push(child)
transform_mark_dirty(child)
}
function transform_mark_dirty(t) {
t.dirty_local = true
t.dirty_world = true
for (var i = 0; i < t.children.length; i++) {
transform_mark_world_dirty(t.children[i])
}
}
function transform_mark_world_dirty(t) {
t.dirty_world = true
for (var i = 0; i < t.children.length; i++) {
transform_mark_world_dirty(t.children[i])
}
}
function transform_get_local_matrix(t) {
if (!t.dirty_local && t.local_mat) return t.local_mat
if (t.has_local_mat && t.local_mat) {
t.dirty_local = false
return t.local_mat
}
// Build from TRS
t.local_mat = model_c.mat4_from_trs(
t.x, t.y, t.z,
t.qx, t.qy, t.qz, t.qw,
t.sx, t.sy, t.sz
)
t.dirty_local = false
return t.local_mat
}
function transform_get_world_matrix(t) {
if (!t.dirty_world && t.world_mat) return t.world_mat
var local = transform_get_local_matrix(t)
if (t.parent) {
var parent_world = transform_get_world_matrix(t.parent)
t.world_mat = model_c.mat4_mul(parent_world, local)
} else {
t.world_mat = local
}
t.dirty_world = false
return t.world_mat
}
function transform_set_position(t, x, y, z) {
t.x = x; t.y = y; t.z = z
t.has_local_mat = false
transform_mark_dirty(t)
}
function transform_set_rotation_quat(t, qx, qy, qz, qw) {
t.qx = qx; t.qy = qy; t.qz = qz; t.qw = qw
t.has_local_mat = false
transform_mark_dirty(t)
}
function transform_set_scale(t, sx, sy, sz) {
t.sx = sx; t.sy = sy; t.sz = sz
t.has_local_mat = false
transform_mark_dirty(t)
}
function transform_set_matrix(t, mat) {
t.local_mat = mat
t.has_local_mat = true
transform_mark_dirty(t)
}
// ============================================================================
// 4) Camera & Projection
// ============================================================================
@@ -513,19 +846,69 @@ function clear_depth() {
function draw_model(model, transform, anim_instance) {
if (!model || !model.meshes) return
var world_matrix = model_c.compute_world_matrix(transform)
// Get view and projection matrices
var view_matrix = _compute_view_matrix()
var proj_matrix = _compute_projection_matrix()
var mat = _state.current_material
var tint = mat ? mat.color : [1, 1, 1, 1]
var tex = mat && mat.texture ? mat.texture : _state.white_texture
var uniforms = model_c.build_uniforms({
model: world_matrix,
view: view_matrix,
projection: proj_matrix,
// If transform is provided, use it as an additional parent for the model's root nodes
var extra_transform = transform ? transform_get_world_matrix(transform) : null
// Draw each node that has a mesh
for (var ni = 0; ni < model.nodes.length; ni++) {
var node = model.nodes[ni]
if (node.mesh_index == null) continue
// Find meshes for this node
for (var mi = 0; mi < model.meshes.length; mi++) {
var mesh = model.meshes[mi]
if (mesh.mesh_index != node.mesh_index) continue
// Get world matrix for this node
var node_world = transform_get_world_matrix(node)
// Apply extra transform if provided
var world_matrix = extra_transform
? model_c.mat4_mul(extra_transform, node_world)
: node_world
// Get material/texture
var mat = _state.current_material
var tint = mat ? mat.color : [1, 1, 1, 1]
var tex = mesh.texture || (mat && mat.texture ? mat.texture : _state.white_texture)
// Build uniforms in cell script
var uniforms = _build_uniforms(world_matrix, view_matrix, proj_matrix, tint)
_draw_mesh(mesh, uniforms, tex, mat ? mat.kind : "lit")
_state.draw_calls++
_state.triangles += mesh.index_count / 3
}
}
// If model has no nodes with meshes, draw meshes directly (fallback)
if (model.nodes.length == 0) {
var world_matrix = transform ? transform_get_world_matrix(transform) : model_c.mat4_identity()
var mat = _state.current_material
var tint = mat ? mat.color : [1, 1, 1, 1]
for (var i = 0; i < model.meshes.length; i++) {
var mesh = model.meshes[i]
var tex = mesh.texture || (mat && mat.texture ? mat.texture : _state.white_texture)
var uniforms = _build_uniforms(world_matrix, view_matrix, proj_matrix, tint)
_draw_mesh(mesh, uniforms, tex, mat ? mat.kind : "lit")
_state.draw_calls++
_state.triangles += mesh.index_count / 3
}
}
}
// Build uniform buffer using C helper (blob API doesn't have write_f32)
function _build_uniforms(model_mat, view_mat, proj_mat, tint) {
return model_c.build_uniforms({
model: model_mat,
view: view_mat,
projection: proj_mat,
ambient: _state.ambient,
light_dir: _state.light_dir,
light_color: _state.light_color,
@@ -538,13 +921,6 @@ function draw_model(model, transform, anim_instance) {
resolution_w: _state.resolution_w,
resolution_h: _state.resolution_h
})
for (var i = 0; i < model.meshes.length; i++) {
var mesh = model.meshes[i]
_draw_mesh(mesh, uniforms, tex, mat ? mat.kind : "lit")
_state.draw_calls++
_state.triangles += mesh.index_count / 3
}
}
// Immediate mode
@@ -764,7 +1140,7 @@ function _init_gpu() {
})
// Create GPU device
_state.gpu = new gpu_mod.gpu({ debug: false, shaders_msl: true })
_state.gpu = new gpu_mod.gpu({ debug: true, shaders_msl: true })
_state.gpu.claim_window(_state.window)
// Load shaders
@@ -781,7 +1157,7 @@ function _init_gpu() {
stage: "vertex",
format: "msl",
entrypoint: "vertex_main",
num_uniform_buffers: 1
num_uniform_buffers: 2
})
var frag_shader = new gpu_mod.shader(_state.gpu, {
@@ -789,7 +1165,7 @@ function _init_gpu() {
stage: "fragment",
format: "msl",
entrypoint: "fragment_main",
num_uniform_buffers: 1,
num_uniform_buffers: 2,
num_samplers: 1
})
@@ -853,7 +1229,7 @@ function _init_gpu() {
})
// Create white texture (1x1)
var white_pixels = new blob(32, true)
var white_pixels = new blob_mod(32, true)
_state.white_texture = _create_texture(1, 1, stone(white_pixels))
}
@@ -948,7 +1324,7 @@ function _create_texture(w, h, pixels) {
function _make_model_from_arrays(positions, normals, uvs, indices, colors) {
var vertex_count = positions.length / 3
// Build mesh object
var mesh = {
vertex_count: vertex_count,
@@ -962,18 +1338,24 @@ function _make_model_from_arrays(positions, normals, uvs, indices, colors) {
mesh.indices = model_c.u16_blob(indices)
mesh.index_type = "uint16"
// Pack and create GPU buffers
var packed = model_c.pack_vertices(mesh)
return {
meshes: [{
vertex_buffer: _create_vertex_buffer(packed.data),
index_buffer: _create_index_buffer(mesh.indices),
index_count: mesh.index_count,
index_type: "uint16",
vertex_count: vertex_count
vertex_count: vertex_count,
texture: null,
mesh_index: 0
}],
nodes: [],
root_nodes: [],
textures: [],
materials: [],
animations: [],
animation_count: 0
}
@@ -1079,7 +1461,7 @@ function _end_frame() {
var cmd = _state.gpu.acquire_cmd_buffer()
// Get swapchain pass instead
var swap_pass = cmd.swapchain_pass(_state.window, {
var pass_desc = {
color_targets: [{
texture: null, // Will use swapchain
load: "clear",
@@ -1090,8 +1472,11 @@ function _end_frame() {
b: _state._clear_color[2],
a: _state._clear_color[3]
}
}],
depth_stencil: {
}]
}
if (_state.depth_texture) {
pass_desc.depth_stencil = {
texture: _state.depth_texture,
load: _state._clear_depth ? "clear" : "load",
store: "dont_care",
@@ -1100,7 +1485,9 @@ function _end_frame() {
clear: 1.0,
clear_stencil: 0
}
})
}
var swap_pass = cmd.swapchain_pass(_state.window, pass_desc)
// Draw all pending meshes
var draws = _state._pending_draws || []
@@ -1111,8 +1498,9 @@ function _end_frame() {
swap_pass.bind_vertex_buffers(0, [{ buffer: d.mesh.vertex_buffer, offset: 0 }])
swap_pass.bind_index_buffer({ buffer: d.mesh.index_buffer, offset: 0 }, d.mesh.index_type == "uint32" ? 32 : 16)
cmd.push_vertex_uniform_data(0, d.uniforms)
cmd.push_fragment_uniform_data(0, d.uniforms)
// Shaders use [[buffer(1)]] for uniforms (buffer(0) is vertex data)
cmd.push_vertex_uniform_data(1, d.uniforms)
cmd.push_fragment_uniform_data(1, d.uniforms)
var sampler = _state.style_id == 1 ? _state.sampler_linear : _state.sampler_nearest
swap_pass.bind_fragment_samplers(0, [{ texture: d.texture, sampler: sampler }])
@@ -1154,6 +1542,13 @@ return {
set_sfx_volume: set_sfx_volume,
make_transform: make_transform,
transform_set_parent: transform_set_parent,
transform_set_position: transform_set_position,
transform_set_rotation_quat: transform_set_rotation_quat,
transform_set_scale: transform_set_scale,
transform_set_matrix: transform_set_matrix,
transform_get_local_matrix: transform_get_local_matrix,
transform_get_world_matrix: transform_get_world_matrix,
camera_look_at: camera_look_at,
camera_perspective: camera_perspective,

View File

@@ -7,16 +7,9 @@ var time_mod = use('time')
var retro3d = use('core')
// Parse command line arguments
var model_path = args[0]
var model_path = args[0] || "Duck.glb"
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
@@ -36,10 +29,10 @@ 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) {
@@ -47,31 +40,33 @@ function _init() {
$_.stop()
return
}
log.console("Model loaded with " + text(model.meshes.length) + " mesh(es)")
// Create transform for the model
log.console(" Nodes: " + text(model.nodes.length))
log.console(" Textures: " + text(model.textures.length))
// Create transform for the model (this will be an extra parent transform)
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()
}
@@ -92,7 +87,7 @@ function _update(dt) {
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
@@ -102,7 +97,7 @@ function _update(dt) {
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
@@ -110,7 +105,7 @@ function _update(dt) {
if (retro3d._state.keys_held['f']) {
cam_target_y -= zoom_speed * dt
}
// Exit on escape
if (retro3d._state.keys_held['escape']) {
$_.stop()
@@ -150,7 +145,7 @@ function _draw() {
retro3d.set_material(grid_mat)
retro3d.begin_lines()
retro3d.color(0.3, 0.3, 0.3, 1)
retro3d.color(0.3, 1, 0.3, 1)
var grid_size = 10
var grid_step = 1

375
model.c
View File

@@ -12,6 +12,7 @@ typedef struct {
// 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) {
@@ -20,13 +21,15 @@ static mat4 mat4_identity(void) {
return m;
}
// Matrix multiplication
// 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 i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
for (int col = 0; col < 4; col++) {
for (int row = 0; row < 4; row++) {
for (int k = 0; k < 4; k++) {
r.m[i * 4 + j] += a.m[i * 4 + k] * b.m[k * 4 + j];
r.m[col * 4 + row] += a.m[k * 4 + row] * b.m[col * 4 + k];
}
}
}
@@ -76,6 +79,51 @@ static mat4 mat4_rotate_z(float rad) {
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};
@@ -123,78 +171,6 @@ static mat4 mat4_look_at(vec3 eye, vec3 target, vec3 up) {
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)
{
@@ -279,6 +255,173 @@ JSValue js_model_mat4_identity(JSContext *js, JSValue this_val, int argc, JSValu
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 blobs and packs into interleaved format
// Returns: { data: blob, stride: number }
@@ -372,16 +515,26 @@ JSValue js_model_pack_vertices(JSContext *js, JSValue this_val, int argc, JSValu
}
// 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
// Layout matches shader struct Uniforms (384 bytes = 96 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
// float4 style_params [88-91] (16 bytes) - style_id, vertex_snap, affine, dither
// float4 resolution [92-95] (16 bytes) - w, h, unused, 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 (384 bytes for good alignment)
// Allocate uniform buffer (384 bytes = 96 floats)
float uniforms[96] = {0};
// Get matrices
@@ -400,20 +553,20 @@ JSValue js_model_build_uniforms(JSContext *js, JSValue this_val, int argc, JSVal
mat4 proj = proj_m ? *(mat4*)proj_m : mat4_identity();
mat4 mvp = mat4_mul(proj, mat4_mul(view, model));
// MVP at offset 0
// MVP at offset 0-15
memcpy(&uniforms[0], mvp.m, 64);
// Model at offset 16
// Model at offset 16-31
memcpy(&uniforms[16], model.m, 64);
// View at offset 32
// View at offset 32-47
memcpy(&uniforms[32], view.m, 64);
// Projection at offset 48
// 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
// 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++) {
@@ -426,10 +579,10 @@ JSValue js_model_build_uniforms(JSContext *js, JSValue this_val, int argc, JSVal
} else {
uniforms[64] = 0.2f; uniforms[65] = 0.2f; uniforms[66] = 0.2f;
}
uniforms[67] = 1.0f;
uniforms[67] = 0.0f; // unused
JS_FreeValue(js, ambient_v);
// Light direction at offset 68
// 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++) {
@@ -442,10 +595,10 @@ JSValue js_model_build_uniforms(JSContext *js, JSValue this_val, int argc, JSVal
} else {
uniforms[68] = 0.5f; uniforms[69] = 1.0f; uniforms[70] = 0.3f;
}
uniforms[71] = 0.0f;
uniforms[71] = 0.0f; // unused
JS_FreeValue(js, light_dir_v);
// Light color at offset 72
// 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++) {
@@ -460,14 +613,14 @@ JSValue js_model_build_uniforms(JSContext *js, JSValue this_val, int argc, JSVal
}
JS_FreeValue(js, light_color_v);
// Light intensity
// 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: near, far, r, g, b, enabled
// 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");
@@ -477,60 +630,63 @@ JSValue js_model_build_uniforms(JSContext *js, JSValue this_val, int argc, JSVal
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[78 + i] = val;
uniforms[80 + 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
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 82
// 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[82 + i] = val;
uniforms[84 + i] = val;
JS_FreeValue(js, c);
}
} else {
uniforms[82] = 1; uniforms[83] = 1; uniforms[84] = 1; uniforms[85] = 1;
uniforms[84] = 1; uniforms[85] = 1; uniforms[86] = 1; uniforms[87] = 1;
}
JS_FreeValue(js, tint_v);
// Style params at offset 86: style_id, vertex_snap, affine_amount, dither
// 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[86] = style_id;
uniforms[88] = 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
uniforms[89] = (style_id == 0) ? 1.0f : 0.0f; // vertex_snap for PS1
uniforms[90] = (style_id == 0) ? 1.0f : 0.0f; // affine texturing for PS1
uniforms[91] = (style_id == 2) ? 1.0f : 0.0f; // dither for Saturn
// Resolution for vertex snapping
// 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[90] = res_w;
uniforms[91] = res_h;
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);
@@ -591,12 +747,15 @@ JSValue js_model_u16_blob(JSContext *js, JSValue this_val, int argc, JSValueCons
}
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, mat4_from_trs, 10),
MIST_FUNC_DEF(model, mat4_from_array, 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),