// retro3d fantasy game console var io = use('fd') var time_mod = use('time') var blob_mod = use('blob') var video = use('sdl3/video') var gpu_mod = use('sdl3/gpu') var events = use('sdl3/events') 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 = { style: null, style_id: 0, // 0=ps1, 1=n64, 2=saturn resolution_w: 320, resolution_h: 240, boot_time: 0, dt: 1/60, frame_count: 0, draw_calls: 0, triangles: 0, // GPU resources window: null, gpu: null, pipeline_lit: null, pipeline_unlit: null, sampler_nearest: null, sampler_linear: null, depth_texture: null, white_texture: null, // Camera state camera: { x: 0, y: 0, z: 5, target_x: 0, target_y: 0, target_z: 0, up_x: 0, up_y: 1, up_z: 0, projection: "perspective", fov: 60, near: 0.1, far: 1000, left: -1, right: 1, bottom: -1, top: 1 }, // Lighting state ambient: [0.2, 0.2, 0.2], light_dir: [0.5, 1.0, 0.3], light_color: [1, 1, 1], light_intensity: 1.0, // Fog state fog_near: 10, fog_far: 100, fog_color: null, // Current material current_material: null, // State stack state_stack: [], // Immediate mode im_mode: null, im_vertices: [], im_color: [1, 1, 1, 1], // Input state keys_held: {}, keys_pressed: {}, axes: [0, 0, 0, 0], // RNG state rng_seed: 12345 } // Style configurations var _styles = { ps1: { id: 0, resolution: [320, 240], vertex_snap: true, affine_texturing: true, filtering: "nearest", color_depth: 15, dither: false }, n64: { id: 1, resolution: [320, 240], vertex_snap: false, affine_texturing: false, filtering: "linear", color_depth: 16, dither: false }, saturn: { id: 2, resolution: [320, 224], vertex_snap: false, affine_texturing: true, filtering: "nearest", color_depth: 15, dither: true } } // ============================================================================ // 1) System / Style / Time / Logging // ============================================================================ function set_style(style_name) { if (_state.style != null) return // Already set def style = _styles[style_name] if (!style) { log.console("retro3d: unknown style: " + style_name) return } _state.style = style_name _state.style_id = style.id _state.resolution_w = style.resolution[0] _state.resolution_h = style.resolution[1] _state.boot_time = time_mod.number() _init_gpu() } function get_style() { return _state.style } function set_resolution(w, h) { _state.resolution_w = w _state.resolution_h = h } function time() { return time_mod.number() - _state.boot_time } function dt() { return _state.dt } function stat(name) { if (name == "draw_calls") return _state.draw_calls if (name == "triangles") return _state.triangles if (name == "fps") return 1 / _state.dt if (name == "memory_bytes") return 0 return 0 } function log_msg() { var args = [] for (var i = 0; i < arguments.length; i++) { args.push(text(arguments[i])) } log.console("[retro3d] " + args.join(" ")) } // ============================================================================ // 2) Assets - Models, Textures, Audio // ============================================================================ function load_model(path) { var data = io.slurp(path) if (!data) return null var ext = path.slice(path.lastIndexOf('.') + 1).toLowerCase() 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 } // 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: [], 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) 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 || "uint16", vertex_count: mesh.vertex_count, 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 } function make_cube(w, h, d) { var hw = w / 2, hh = h / 2, hd = d / 2 // 8 vertices, 36 indices (12 triangles) var positions = [ // Front face -hw, -hh, hd, hw, -hh, hd, hw, hh, hd, -hw, hh, hd, // Back face -hw, -hh, -hd, -hw, hh, -hd, hw, hh, -hd, hw, -hh, -hd, // Top face -hw, hh, -hd, -hw, hh, hd, hw, hh, hd, hw, hh, -hd, // Bottom face -hw, -hh, -hd, hw, -hh, -hd, hw, -hh, hd, -hw, -hh, hd, // Right face hw, -hh, -hd, hw, hh, -hd, hw, hh, hd, hw, -hh, hd, // Left face -hw, -hh, -hd, -hw, -hh, hd, -hw, hh, hd, -hw, hh, -hd ] var normals = [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0,-1, 0, 0,-1, 0, 0,-1, 0, 0,-1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0,-1, 0, 0,-1, 0, 0,-1, 0, 0,-1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0 ] var uvs = [ 0,1, 1,1, 1,0, 0,0, 1,1, 1,0, 0,0, 0,1, 0,0, 0,1, 1,1, 1,0, 0,1, 1,1, 1,0, 0,0, 1,1, 1,0, 0,0, 0,1, 0,1, 1,1, 1,0, 0,0 ] var indices = [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9,10, 8,10,11, 12,13,14, 12,14,15, 16,17,18, 16,18,19, 20,21,22, 20,22,23 ] return _make_model_from_arrays(positions, normals, uvs, indices) } function make_sphere(r, segments) { if (!segments) segments = 12 var positions = [] var normals = [] var uvs = [] var indices = [] for (var y = 0; y <= segments; y++) { var v = y / segments var theta = v * 3.14159265 for (var x = 0; x <= segments; x++) { var u = x / segments var phi = u * 2 * 3.14159265 var nx = Math.sin(theta) * Math.cos(phi) var ny = Math.cos(theta) var nz = Math.sin(theta) * Math.sin(phi) positions.push(nx * r, ny * r, nz * r) normals.push(nx, ny, nz) uvs.push(u, v) } } for (var y = 0; y < segments; y++) { for (var x = 0; x < segments; x++) { var i = y * (segments + 1) + x indices.push(i, i + 1, i + segments + 1) indices.push(i + 1, i + segments + 2, i + segments + 1) } } return _make_model_from_arrays(positions, normals, uvs, indices) } function make_cylinder(r, h, segments) { if (!segments) segments = 12 var positions = [] var normals = [] var uvs = [] var indices = [] var hh = h / 2 // Side vertices for (var i = 0; i <= segments; i++) { var u = i / segments var angle = u * 2 * 3.14159265 var nx = Math.cos(angle) var nz = Math.sin(angle) positions.push(nx * r, -hh, nz * r) normals.push(nx, 0, nz) uvs.push(u, 1) positions.push(nx * r, hh, nz * r) normals.push(nx, 0, nz) uvs.push(u, 0) } // Side indices for (var i = 0; i < segments; i++) { var base = i * 2 indices.push(base, base + 1, base + 2) indices.push(base + 1, base + 3, base + 2) } return _make_model_from_arrays(positions, normals, uvs, indices) } function make_plane(w, h) { var hw = w / 2, hh = h / 2 var positions = [-hw, 0, -hh, hw, 0, -hh, hw, 0, hh, -hw, 0, hh] var normals = [0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0] var uvs = [0, 0, 1, 0, 1, 1, 0, 1] var indices = [0, 1, 2, 0, 2, 3] return _make_model_from_arrays(positions, normals, uvs, indices) } function make_model(vertices, indices, uvs_in, colors_in) { var vertex_count = vertices.length / 3 var positions = vertices var normals = [] var uvs = uvs_in || [] var colors = colors_in || [] // Generate flat normals for (var i = 0; i < vertex_count; i++) { normals.push(0, 1, 0) } return _make_model_from_arrays(positions, normals, uvs, indices, colors) } function load_texture(path) { var png = use('cell-image/png') var data = io.slurp(path) if (!data) return null var img = png.decode(data) if (!img) return null return _create_texture(img.width, img.height, img.pixels) } function make_texture(w, h, rgba_u8) { return _create_texture(w, h, rgba_u8) } // Audio stubs (to be implemented with audio backend) function load_sound(path) { return null } function play_sound(sound, opts) { return null } function stop_sound(voice_id) {} function load_music(path) { return null } function play_music(music, loop) {} function stop_music() {} function set_music_volume(v) {} function set_sfx_volume(v) {} // ============================================================================ // 3) Transforms - Hierarchical with dirty flags // ============================================================================ function make_transform(opts) { opts = opts || {} return { 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 // ============================================================================ function camera_look_at(ex, ey, ez, tx, ty, tz, upx, upy, upz) { _state.camera.x = ex _state.camera.y = ey _state.camera.z = ez _state.camera.target_x = tx _state.camera.target_y = ty _state.camera.target_z = tz _state.camera.up_x = upx != null ? upx : 0 _state.camera.up_y = upy != null ? upy : 1 _state.camera.up_z = upz != null ? upz : 0 } function camera_perspective(fov_deg, near, far) { _state.camera.projection = "perspective" _state.camera.fov = fov_deg || 60 _state.camera.near = near || 0.1 _state.camera.far = far || 1000 } function camera_ortho(left, right, bottom, top, near, far) { _state.camera.projection = "ortho" _state.camera.left = left _state.camera.right = right _state.camera.bottom = bottom _state.camera.top = top _state.camera.near = near || -1 _state.camera.far = far || 1 } function camera_get() { return Object.assign({}, _state.camera) } // ============================================================================ // 5) Render State Stack // ============================================================================ function push_state() { _state.state_stack.push({ camera: Object.assign({}, _state.camera), ambient: _state.ambient.slice(), light_dir: _state.light_dir.slice(), light_color: _state.light_color.slice(), light_intensity: _state.light_intensity, fog_near: _state.fog_near, fog_far: _state.fog_far, fog_color: _state.fog_color ? _state.fog_color.slice() : null, current_material: _state.current_material, im_color: _state.im_color.slice() }) } function pop_state() { if (_state.state_stack.length == 0) return var s = _state.state_stack.pop() _state.camera = s.camera _state.ambient = s.ambient _state.light_dir = s.light_dir _state.light_color = s.light_color _state.light_intensity = s.light_intensity _state.fog_near = s.fog_near _state.fog_far = s.fog_far _state.fog_color = s.fog_color _state.current_material = s.current_material _state.im_color = s.im_color } // ============================================================================ // 6) Materials, Lighting, Fog // ============================================================================ function make_material(kind, opts) { opts = opts || {} return { kind: kind, texture: opts.texture || null, color: opts.color || [1, 1, 1, 1] } } function set_material(material) { _state.current_material = material } function set_ambient(r, g, b) { _state.ambient = [r, g, b] } function set_light_dir(dx, dy, dz, r, g, b, intensity) { var len = Math.sqrt(dx*dx + dy*dy + dz*dz) _state.light_dir = [dx/len, dy/len, dz/len] _state.light_color = [r, g, b] _state.light_intensity = intensity != null ? intensity : 1.0 } function set_fog(near, far, color_or_null) { _state.fog_near = near _state.fog_far = far _state.fog_color = color_or_null } // ============================================================================ // 7) Drawing - Models & Immediate Mode // ============================================================================ function clear(r, g, b, a, clear_depth) { if (a == null) a = 1.0 if (clear_depth == null) clear_depth = true _state._clear_color = [r, g, b, a] _state._clear_depth = clear_depth } function clear_depth() { _state._clear_depth_only = true } function draw_model(model, transform, anim_instance) { if (!model || !model.meshes) return // Get view and projection matrices var view_matrix = _compute_view_matrix() var proj_matrix = _compute_projection_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, light_intensity: _state.light_intensity, fog_near: _state.fog_near, fog_far: _state.fog_far, fog_color: _state.fog_color, tint: tint, style_id: _state.style_id, resolution_w: _state.resolution_w, resolution_h: _state.resolution_h }) } // Immediate mode function begin_triangles() { _state.im_mode = "triangles"; _state.im_vertices = [] } function begin_lines() { _state.im_mode = "lines"; _state.im_vertices = [] } function begin_points() { _state.im_mode = "points"; _state.im_vertices = [] } function color(r, g, b, a) { _state.im_color = [r, g, b, a != null ? a : 1.0] } function vertex(x, y, z, u, v) { _state.im_vertices.push({ x: x, y: y, z: z, u: u || 0, v: v || 0, color: _state.im_color.slice() }) } function end() { if (_state.im_vertices.length == 0) return // Flush immediate mode geometry _flush_immediate() _state.im_mode = null _state.im_vertices = [] } // ============================================================================ // 8) Animation // ============================================================================ function anim_instance(model) { return { model: model, clip_index: 0, time: 0, speed: 1, loop: true, playing: false } } function anim_clip_count(model) { return model.animation_count || 0 } function anim_clip_duration(model, clip_index) { if (!model.animations || clip_index >= model.animations.length) return 0 return model.animations[clip_index].duration || 0 } function anim_play(anim, clip_index, loop) { anim.clip_index = clip_index anim.loop = loop != null ? loop : true anim.playing = true anim.time = 0 } function anim_stop(anim) { anim.playing = false } function anim_set_time(anim, seconds) { anim.time = seconds } function anim_set_speed(anim, speed) { anim.speed = speed } function anim_update(anim, dt_val) { if (!anim.playing) return dt_val = dt_val != null ? dt_val : _state.dt anim.time += dt_val * anim.speed var duration = anim_clip_duration(anim.model, anim.clip_index) if (duration > 0 && anim.time > duration) { if (anim.loop) { anim.time = anim.time % duration } else { anim.time = duration anim.playing = false } } } function anim_pop_events(anim) { return [] } // ============================================================================ // 9) Collision (stubs for v1) // ============================================================================ var _colliders = [] var _collider_id = 0 function add_collider_sphere(transform, radius, layer_mask, user) { var c = { id: _collider_id++, type: "sphere", transform: transform, radius: radius, layer_mask: layer_mask || 1, user: user } _colliders.push(c) return c } function add_collider_box(transform, sx, sy, sz, layer_mask, user) { var c = { id: _collider_id++, type: "box", transform: transform, sx: sx, sy: sy, sz: sz, layer_mask: layer_mask || 1, user: user } _colliders.push(c) return c } function remove_collider(collider) { for (var i = 0; i < _colliders.length; i++) { if (_colliders[i].id == collider.id) { _colliders.splice(i, 1) return } } } function overlaps(layer_mask_a, layer_mask_b) { // Simple O(n^2) collision check var results = [] for (var i = 0; i < _colliders.length; i++) { for (var j = i + 1; j < _colliders.length; j++) { var a = _colliders[i] var b = _colliders[j] if (layer_mask_a != null && !(a.layer_mask & layer_mask_a)) continue if (layer_mask_b != null && !(b.layer_mask & layer_mask_b)) continue if (_check_collision(a, b)) { results.push({a: a, b: b}) } } } return results } function raycast(ox, oy, oz, dx, dy, dz, max_dist, layer_mask) { // Stub - returns null for now return null } function _check_collision(a, b) { // Simple sphere-sphere for now var ax = a.transform.x, ay = a.transform.y, az = a.transform.z var bx = b.transform.x, by = b.transform.y, bz = b.transform.z var dx = bx - ax, dy = by - ay, dz = bz - az var dist = Math.sqrt(dx*dx + dy*dy + dz*dz) var ra = a.radius || Math.max(a.sx || 0, a.sy || 0, a.sz || 0) var rb = b.radius || Math.max(b.sx || 0, b.sy || 0, b.sz || 0) return dist < ra + rb } // ============================================================================ // 10) Input // ============================================================================ function btn(id, player) { // Map button IDs to keyboard keys for now var key_map = ['z', 'x', 'c', 'v', 'a', 's', 'return', 'escape'] var key = key_map[id] return _state.keys_held[key] || false } function btnp(id, player) { var key_map = ['z', 'x', 'c', 'v', 'a', 's', 'return', 'escape'] var key = key_map[id] return _state.keys_pressed[key] || false } function axis(id, player) { return _state.axes[id] || 0 } // ============================================================================ // 11) RNG & Math Helpers // ============================================================================ function seed(seed_int) { _state.rng_seed = seed_int } function rand() { // Simple LCG _state.rng_seed = (_state.rng_seed * 1103515245 + 12345) & 0x7fffffff return _state.rng_seed / 0x7fffffff } function irand(min_inclusive, max_inclusive) { return Math.floor(rand() * (max_inclusive - min_inclusive + 1)) + min_inclusive } // ============================================================================ // Internal GPU functions // ============================================================================ function _init_gpu() { // Create window _state.window = new video.window({ title: "retro3d", width: _state.resolution_w * 2, height: _state.resolution_h * 2 }) // Create GPU device _state.gpu = new gpu_mod.gpu({ debug: true, shaders_msl: true }) _state.gpu.claim_window(_state.window) // Load shaders var vert_code = io.slurp("shaders/retro3d.vert.msl") var frag_code = io.slurp("shaders/retro3d.frag.msl") if (!vert_code || !frag_code) { log.console("retro3d: failed to load shaders") return } var vert_shader = new gpu_mod.shader(_state.gpu, { code: vert_code, stage: "vertex", format: "msl", entrypoint: "vertex_main", num_uniform_buffers: 2 }) var frag_shader = new gpu_mod.shader(_state.gpu, { code: frag_code, stage: "fragment", format: "msl", entrypoint: "fragment_main", num_uniform_buffers: 2, num_samplers: 1 }) // Create pipeline var swapchain_format = _state.gpu.swapchain_format(_state.window) _state.pipeline_lit = new gpu_mod.graphics_pipeline(_state.gpu, { vertex: vert_shader, fragment: frag_shader, primitive: "triangle", cull: "back", face: "counter_clockwise", fill: "fill", vertex_buffer_descriptions: [{ slot: 0, pitch: 48, input_rate: "vertex" }], vertex_attributes: [ { location: 0, buffer_slot: 0, format: "float3", offset: 0 }, { location: 1, buffer_slot: 0, format: "float3", offset: 12 }, { location: 2, buffer_slot: 0, format: "float2", offset: 24 }, { location: 3, buffer_slot: 0, format: "float4", offset: 32 } ], target: { color_targets: [{ format: swapchain_format, blend: { enabled: false } }], depth: "d32 float s8" }, depth: { test: true, write: true, compare: "less" } }) // Create samplers var style = _styles[_state.style] _state.sampler_nearest = new gpu_mod.sampler(_state.gpu, { min_filter: "nearest", mag_filter: "nearest", u: "repeat", v: "repeat" }) _state.sampler_linear = new gpu_mod.sampler(_state.gpu, { min_filter: "linear", mag_filter: "linear", u: "repeat", v: "repeat" }) // Create depth texture _state.depth_texture = new gpu_mod.texture(_state.gpu, { width: _state.resolution_w * 2, height: _state.resolution_h * 2, format: "d32 float s8", type: "2d", layers: 1, mip_levels: 1, depth_target: true }) // Create white texture (1x1) var white_pixels = new blob_mod(32, true) _state.white_texture = _create_texture(1, 1, stone(white_pixels)) } function _create_vertex_buffer(data) { var size = data.length / 8 var buffer = new gpu_mod.buffer(_state.gpu, { size: size, vertex: true }) var transfer = new gpu_mod.transfer_buffer(_state.gpu, { size: size, usage: "upload" }) transfer.copy_blob(_state.gpu, data) var cmd = _state.gpu.acquire_cmd_buffer() var copy = cmd.copy_pass() copy.upload_to_buffer( { transfer_buffer: transfer, offset: 0 }, { buffer: buffer, offset: 0, size: size }, false ) copy.end() cmd.submit() return buffer } function _create_index_buffer(data) { var size = data.length / 8 var buffer = new gpu_mod.buffer(_state.gpu, { size: size, index: true }) var transfer = new gpu_mod.transfer_buffer(_state.gpu, { size: size, usage: "upload" }) transfer.copy_blob(_state.gpu, data) var cmd = _state.gpu.acquire_cmd_buffer() var copy = cmd.copy_pass() copy.upload_to_buffer( { transfer_buffer: transfer, offset: 0 }, { buffer: buffer, offset: 0, size: size }, false ) copy.end() cmd.submit() return buffer } function _create_texture(w, h, pixels) { var tex = new gpu_mod.texture(_state.gpu, { width: w, height: h, format: "rgba8", type: "2d", layers: 1, mip_levels: 1, sampler: true }) var size = w * h * 4 var transfer = new gpu_mod.transfer_buffer(_state.gpu, { size: size, usage: "upload" }) transfer.copy_blob(_state.gpu, pixels) var cmd = _state.gpu.acquire_cmd_buffer() var copy = cmd.copy_pass() copy.upload_to_texture( { transfer_buffer: transfer, offset: 0, pixels_per_row: w, rows_per_layer: h }, { texture: tex, x: 0, y: 0, z: 0, w: w, h: h, d: 1 }, false ) copy.end() cmd.submit() tex.width = w tex.height = h return tex } 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, index_count: indices.length } mesh.positions = model_c.f32_blob(positions) mesh.normals = normals && normals.length > 0 ? model_c.f32_blob(normals) : null mesh.uvs = uvs && uvs.length > 0 ? model_c.f32_blob(uvs) : null mesh.colors = colors && colors.length > 0 ? model_c.f32_blob(colors) : null 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, texture: null, mesh_index: 0 }], nodes: [], root_nodes: [], textures: [], materials: [], animations: [], animation_count: 0 } } function _compute_view_matrix() { var c = _state.camera return model_c.compute_view_matrix( c.x, c.y, c.z, c.target_x, c.target_y, c.target_z, c.up_x, c.up_y, c.up_z ) } function _compute_projection_matrix() { var c = _state.camera var aspect = _state.resolution_w / _state.resolution_h if (c.projection == "perspective") { return model_c.compute_perspective(c.fov, aspect, c.near, c.far) } else { return model_c.compute_ortho(c.left, c.right, c.bottom, c.top, c.near, c.far) } } function _draw_mesh(mesh, uniforms, texture, kind) { // This will be called during render pass _state._pending_draws = _state._pending_draws || [] _state._pending_draws.push({ mesh: mesh, uniforms: uniforms, texture: texture, kind: kind }) } function _flush_immediate() { // Convert immediate mode vertices to a temporary mesh and draw if (_state.im_vertices.length == 0) return var verts = _state.im_vertices var positions = [] var normals = [] var uvs = [] var colors = [] var indices = [] for (var i = 0; i < verts.length; i++) { var v = verts[i] positions.push(v.x, v.y, v.z) normals.push(0, 1, 0) uvs.push(v.u, v.v) colors.push(v.color[0], v.color[1], v.color[2], v.color[3]) indices.push(i) } var model = _make_model_from_arrays(positions, normals, uvs, indices, colors) var transform = make_transform() draw_model(model, transform) } // ============================================================================ // Frame rendering (called by cart runner) // ============================================================================ function _begin_frame() { _state.draw_calls = 0 _state.triangles = 0 _state.keys_pressed = {} _state._pending_draws = [] _state._clear_color = [0, 0, 0, 1] _state._clear_depth = true } function _process_events() { var ev while ((ev = events.poll()) != null) { if (ev.type == "quit" || ev.type == "window_close_requested") { return false } if (ev.type == "key_down" || ev.type == "key_up") { var key_name = keyboard.get_key_name(ev.key).toLowerCase() var pressed = ev.type == "key_down" if (pressed && !_state.keys_held[key_name]) { _state.keys_pressed[key_name] = true } _state.keys_held[key_name] = pressed // Map WASD to axes if (key_name == "w") _state.axes[1] = pressed ? -1 : 0 if (key_name == "s") _state.axes[1] = pressed ? 1 : 0 if (key_name == "a") _state.axes[0] = pressed ? -1 : 0 if (key_name == "d") _state.axes[0] = pressed ? 1 : 0 } } return true } function _end_frame() { if (!_state.gpu) return var cmd = _state.gpu.acquire_cmd_buffer() // Get swapchain pass instead var pass_desc = { color_targets: [{ texture: null, // Will use swapchain load: "clear", store: "store", clear_color: { r: _state._clear_color[0], g: _state._clear_color[1], b: _state._clear_color[2], a: _state._clear_color[3] } }] } if (_state.depth_texture) { pass_desc.depth_stencil = { texture: _state.depth_texture, load: _state._clear_depth ? "clear" : "load", store: "dont_care", stencil_load: "clear", stencil_store: "dont_care", 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 || [] for (var i = 0; i < draws.length; i++) { var d = draws[i] swap_pass.bind_pipeline(_state.pipeline_lit) 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) // 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 }]) swap_pass.draw_indexed(d.mesh.index_count, 1, 0, 0, 0) } swap_pass.end() cmd.submit() _state.frame_count++ } // Export the module return { set_style: set_style, get_style: get_style, set_resolution: set_resolution, time: time, dt: dt, stat: stat, log: log_msg, load_model: load_model, make_cube: make_cube, make_sphere: make_sphere, make_cylinder: make_cylinder, make_plane: make_plane, make_model: make_model, load_texture: load_texture, make_texture: make_texture, load_sound: load_sound, play_sound: play_sound, stop_sound: stop_sound, load_music: load_music, play_music: play_music, stop_music: stop_music, set_music_volume: set_music_volume, 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, camera_ortho: camera_ortho, camera_get: camera_get, push_state: push_state, pop_state: pop_state, make_material: make_material, set_material: set_material, set_ambient: set_ambient, set_light_dir: set_light_dir, set_fog: set_fog, clear: clear, clear_depth: clear_depth, draw_model: draw_model, begin_triangles: begin_triangles, begin_lines: begin_lines, begin_points: begin_points, color: color, vertex: vertex, end: end, anim_instance: anim_instance, anim_clip_count: anim_clip_count, anim_clip_duration: anim_clip_duration, anim_play: anim_play, anim_stop: anim_stop, anim_set_time: anim_set_time, anim_set_speed: anim_set_speed, anim_update: anim_update, anim_pop_events: anim_pop_events, add_collider_sphere: add_collider_sphere, add_collider_box: add_collider_box, remove_collider: remove_collider, overlaps: overlaps, raycast: raycast, btn: btn, btnp: btnp, axis: axis, seed: seed, rand: rand, irand: irand, // Internal functions for cart runner _state: _state, _begin_frame: _begin_frame, _process_events: _process_events, _end_frame: _end_frame }