From 863bc7fe9b3bc53851f2977d45d0dd3b10005343 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Sat, 13 Dec 2025 00:46:00 -0600 Subject: [PATCH] fixed render --- cell.toml | 3 +- core.cm | 517 +++++++++++++++++++++++++++++++++++++----- examples/modelview.ce | 39 ++-- model.c | 375 +++++++++++++++++++++--------- 4 files changed, 742 insertions(+), 192 deletions(-) diff --git a/cell.toml b/cell.toml index 7015388..70e2703 100644 --- a/cell.toml +++ b/cell.toml @@ -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" diff --git a/core.cm b/core.cm index f686cb1..3caf433 100644 --- a/core.cm +++ b/core.cm @@ -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, diff --git a/examples/modelview.ce b/examples/modelview.ce index fa5e6e1..916ac36 100644 --- a/examples/modelview.ce +++ b/examples/modelview.ce @@ -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 [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 diff --git a/model.c b/model.c index 9b2c2b8..cfc9104 100644 --- a/model.c +++ b/model.c @@ -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),