From 828db06c74664dcc6769d8cddb512e1f2658d4de Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Sun, 14 Dec 2025 01:36:35 -0600 Subject: [PATCH] split apart modules --- camera.cm | 90 ++ collision.cm | 235 +++ core.cm | 2758 ++++++------------------------------ examples/billboard_test.ce | 156 -- examples/cube.ce | 109 +- examples/forest.ce | 263 ++-- examples/modelview.ce | 220 ++- examples/sprite_test.ce | 117 -- input.cm | 122 ++ lance3d.md | 22 +- math.cm | 133 ++ resources.cm | 552 ++++++++ sdl.cm | 376 +++++ 13 files changed, 2212 insertions(+), 2941 deletions(-) create mode 100644 camera.cm create mode 100644 collision.cm delete mode 100644 examples/billboard_test.ce delete mode 100644 examples/sprite_test.ce create mode 100644 input.cm create mode 100644 math.cm create mode 100644 resources.cm create mode 100644 sdl.cm diff --git a/camera.cm b/camera.cm new file mode 100644 index 0000000..98befea --- /dev/null +++ b/camera.cm @@ -0,0 +1,90 @@ +// Camera module for lance3d +var model_c = use('model') + +// Private camera state +var _view_matrix = null +var _proj_matrix = null +var _eye = {x: 0, y: 0, z: 5} +var _target = {x: 0, y: 0, z: 0} +var _up = {x: 0, y: 1, z: 0} +var _fov = 60 +var _near = 0.1 +var _far = 1000 +var _aspect = 4/3 +var _projection_type = "perspective" + +function set_aspect(aspect) { + _aspect = aspect +} + +function look_at(ex, ey, ez, tx, ty, tz, upx, upy, upz) { + upx = upx != null ? upx : 0 + upy = upy != null ? upy : 1 + upz = upz != null ? upz : 0 + + _eye = {x: ex, y: ey, z: ez} + _target = {x: tx, y: ty, z: tz} + _up = {x: upx, y: upy, z: upz} + + _view_matrix = model_c.compute_view_matrix(ex, ey, ez, tx, ty, tz, upx, upy, upz) +} + +function perspective(fov_deg, near, far) { + _fov = fov_deg || 60 + _near = near || 0.1 + _far = far || 1000 + _projection_type = "perspective" + + _proj_matrix = model_c.compute_perspective(_fov, _aspect, _near, _far) +} + +function ortho(left, right, bottom, top, near, far) { + near = near != null ? near : -1 + far = far != null ? far : 1 + _projection_type = "ortho" + + _proj_matrix = model_c.compute_ortho(left, right, bottom, top, near, far) +} + +function get_view_matrix() { + return _view_matrix || model_c.mat4_identity() +} + +function get_proj_matrix() { + return _proj_matrix || model_c.mat4_identity() +} + +function get_eye() { + return {x: _eye.x, y: _eye.y, z: _eye.z} +} + +function get_target() { + return {x: _target.x, y: _target.y, z: _target.z} +} + +function get_state() { + return { + view_matrix: _view_matrix, + proj_matrix: _proj_matrix, + eye: _eye, + target: _target, + up: _up, + fov: _fov, + near: _near, + far: _far, + aspect: _aspect, + projection_type: _projection_type + } +} + +return { + set_aspect: set_aspect, + look_at: look_at, + perspective: perspective, + ortho: ortho, + get_view_matrix: get_view_matrix, + get_proj_matrix: get_proj_matrix, + get_eye: get_eye, + get_target: get_target, + get_state: get_state +} diff --git a/collision.cm b/collision.cm new file mode 100644 index 0000000..7f3f746 --- /dev/null +++ b/collision.cm @@ -0,0 +1,235 @@ +// Collision module for lance3d + +// Private collider storage +var _colliders = [] +var _collider_id = 0 + +function clear() { + _colliders = [] +} + +function add_sphere(transform, radius, opts) { + opts = opts || {} + var c = { + id: _collider_id++, + type: "sphere", + transform: transform, + radius: radius, + layer_mask: opts.layer_mask || 1, + user: opts.user + } + _colliders.push(c) + return c +} + +function add_box(transform, sx, sy, sz, opts) { + opts = opts || {} + var c = { + id: _collider_id++, + type: "box", + transform: transform, + sx: sx, sy: sy, sz: sz, + layer_mask: opts.layer_mask || 1, + user: opts.user + } + _colliders.push(c) + return c +} + +function remove(collider) { + for (var i = 0; i < _colliders.length; i++) { + if (_colliders[i].id == collider.id) { + _colliders.splice(i, 1) + return true + } + } + return false +} + +function overlaps(layer_mask_a, layer_mask_b) { + 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, opts) { + opts = opts || {} + var max_dist = opts.max_dist || 1000000 + var layer_mask = opts.layer_mask || 0xFFFFFFFF + + // Normalize direction + var len = Math.sqrt(dx*dx + dy*dy + dz*dz) + if (len < 0.0001) return null + dx /= len + dy /= len + dz /= len + + var closest = null + var closest_dist = max_dist + + for (var i = 0; i < _colliders.length; i++) { + var c = _colliders[i] + if (!(c.layer_mask & layer_mask)) continue + + var hit = null + if (c.type == "sphere") { + hit = _ray_sphere(ox, oy, oz, dx, dy, dz, c) + } else if (c.type == "box") { + hit = _ray_box(ox, oy, oz, dx, dy, dz, c) + } + + if (hit && hit.distance < closest_dist) { + closest = hit + closest_dist = hit.distance + } + } + + return closest +} + +function _get_position(transform) { + if (!transform) return {x: 0, y: 0, z: 0} + // If transform is a matrix (array of 16), extract translation + if (transform.length == 16) { + return {x: transform[12], y: transform[13], z: transform[14]} + } + // If transform is an object with x,y,z + if (transform.x != null) { + return {x: transform.x, y: transform.y, z: transform.z} + } + return {x: 0, y: 0, z: 0} +} + +function _check_collision(a, b) { + var pa = _get_position(a.transform) + var pb = _get_position(b.transform) + + var dx = pb.x - pa.x + var dy = pb.y - pa.y + var dz = pb.z - pa.z + var dist = Math.sqrt(dx*dx + dy*dy + dz*dz) + + // Simple sphere-sphere approximation + 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 +} + +function _ray_sphere(ox, oy, oz, dx, dy, dz, sphere) { + var pos = _get_position(sphere.transform) + var r = sphere.radius + + // Vector from ray origin to sphere center + var lx = pos.x - ox + var ly = pos.y - oy + var lz = pos.z - oz + + // Project onto ray direction + var tca = lx*dx + ly*dy + lz*dz + if (tca < 0) return null + + var d2 = lx*lx + ly*ly + lz*lz - tca*tca + var r2 = r*r + if (d2 > r2) return null + + var thc = Math.sqrt(r2 - d2) + var t = tca - thc + if (t < 0) t = tca + thc + if (t < 0) return null + + var hx = ox + dx*t + var hy = oy + dy*t + var hz = oz + dz*t + + // Normal at hit point + var nx = (hx - pos.x) / r + var ny = (hy - pos.y) / r + var nz = (hz - pos.z) / r + + return { + x: hx, y: hy, z: hz, + nx: nx, ny: ny, nz: nz, + distance: t, + collider: sphere + } +} + +function _ray_box(ox, oy, oz, dx, dy, dz, box) { + var pos = _get_position(box.transform) + var hx = (box.sx || 1) / 2 + var hy = (box.sy || 1) / 2 + var hz = (box.sz || 1) / 2 + + var minx = pos.x - hx, maxx = pos.x + hx + var miny = pos.y - hy, maxy = pos.y + hy + var minz = pos.z - hz, maxz = pos.z + hz + + var tmin = -1000000, tmax = 1000000 + var nx = 0, ny = 0, nz = 0 + + // X slab + if (Math.abs(dx) > 0.0001) { + var t1 = (minx - ox) / dx + var t2 = (maxx - ox) / dx + if (t1 > t2) { var tmp = t1; t1 = t2; t2 = tmp } + if (t1 > tmin) { tmin = t1; nx = dx > 0 ? -1 : 1; ny = 0; nz = 0 } + if (t2 < tmax) tmax = t2 + } else if (ox < minx || ox > maxx) { + return null + } + + // Y slab + if (Math.abs(dy) > 0.0001) { + var t1 = (miny - oy) / dy + var t2 = (maxy - oy) / dy + if (t1 > t2) { var tmp = t1; t1 = t2; t2 = tmp } + if (t1 > tmin) { tmin = t1; nx = 0; ny = dy > 0 ? -1 : 1; nz = 0 } + if (t2 < tmax) tmax = t2 + } else if (oy < miny || oy > maxy) { + return null + } + + // Z slab + if (Math.abs(dz) > 0.0001) { + var t1 = (minz - oz) / dz + var t2 = (maxz - oz) / dz + if (t1 > t2) { var tmp = t1; t1 = t2; t2 = tmp } + if (t1 > tmin) { tmin = t1; nx = 0; ny = 0; nz = dz > 0 ? -1 : 1 } + if (t2 < tmax) tmax = t2 + } else if (oz < minz || oz > maxz) { + return null + } + + if (tmin > tmax || tmax < 0) return null + + var t = tmin > 0 ? tmin : tmax + if (t < 0) return null + + return { + x: ox + dx*t, y: oy + dy*t, z: oz + dz*t, + nx: nx, ny: ny, nz: nz, + distance: t, + collider: box + } +} + +return { + clear: clear, + add_sphere: add_sphere, + add_box: add_box, + remove: remove, + overlaps: overlaps, + raycast: raycast +} diff --git a/core.cm b/core.cm index 2e5dbc5..7cc1f90 100644 --- a/core.cm +++ b/core.cm @@ -1,101 +1,21 @@ -// retro3d fantasy game console -var io = use('fd') +// lance3d - retro 3D fantasy console 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 mouse = use('sdl3/mouse') -var gltf = use('mload/gltf') -var obj_loader = use('mload/obj') var model_c = use('model') -var png = use('cell-image/png') -var resize_mod = use('cell-image/resize') var anim_mod = use('animation') var skin_mod = use('skin') -// 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, +// Import sub-modules +var backend = use('sdl') +var input_mod = use('input') +var collision_mod = use('collision') +var resources_mod = use('resources') +var camera_mod = use('camera') +var math_mod = use('math') - // GPU resources - window: null, - gpu: null, - // Pipelines for different alpha modes and culling - // Key format: "skinned_alphamode_cull" e.g. "false_opaque_back", "true_blend_none" - pipelines: {}, - sampler_nearest: null, - sampler_linear: null, - depth_texture: null, - white_texture: null, - // Shader references for pipeline creation - vert_shader: null, - frag_shader: null, - skinned_vert_shader: null, - swapchain_format: 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], - mouse_x: 0, - mouse_y: 0, - mouse_dx: 0, - mouse_dy: 0, - - // RNG state - rng_seed: 12345, - - // Sprite state - sprite_manifest: null, // { base_path: extension } mapping - sprite_cache: {}, // { path: sprite_object } cache - sprite_quad: null // Shared quad mesh for sprites/billboards -} - -// Style configurations +// Style configurations (PS1, N64, Saturn) var _styles = { ps1: { + name: "ps1", id: 0, resolution: [320, 240], vertex_snap: true, @@ -107,6 +27,7 @@ var _styles = { tri_budget: 2000 }, n64: { + name: "n64", id: 1, resolution: [320, 240], vertex_snap: false, @@ -118,6 +39,7 @@ var _styles = { tri_budget: 3000 }, saturn: { + name: "saturn", id: 2, resolution: [320, 224], vertex_snap: false, @@ -130,27 +52,61 @@ var _styles = { } } -// Track original image data for textures (needed for re-resizing on style change) -// Key: texture object, Value: { width, height, pixels } -// Using symbol property since WeakMap not available -var TEX_ORIGINAL = Symbol("texture_original") - // Triangle budget warning state var _tri_warning_state = { - last_warn_time: 0, - warned_this_cycle: false + last_warn_time: 0 +} + +// Internal state +var _state = { + style: null, + style_id: 0, + resolution_w: 320, + resolution_h: 240, + boot_time: 0, + dt: 1/60, + frame_count: 0, + draw_calls: 0, + triangles: 0, + + // Environment (lighting/fog) + lighting: { + sun_dir: [0.3, -1, 0.2], + sun_color: [1, 1, 1], + ambient: [0.25, 0.25, 0.25] + }, + fog: { + enabled: false, + color: [0.5, 0.6, 0.7], + near: 10, + far: 80 + }, + + // Pending draws + _pending_draws: [], + _clear_color: [0, 0, 0, 1], + _clear_depth: true +} + +// Default material prototype +var _default_material = { + color_map: null, + paint: [1, 1, 1, 1], + coverage: "opaque", + face: "single", + lamp: "lit" } // ============================================================================ -// 1) System / Style / Time / Logging +// System / Style / Time / Logging // ============================================================================ function set_style(style_name) { if (_state.style != null) return // Already set - def style = _styles[style_name] + var style = _styles[style_name] if (!style) { - log.console("retro3d: unknown style: " + style_name) + log.console("lance3d: unknown style: " + style_name) return } @@ -160,16 +116,51 @@ function set_style(style_name) { _state.resolution_h = style.resolution[1] _state.boot_time = time_mod.number() - _init_gpu() + // Initialize backend + backend.init({ + title: "lance3d - " + style_name, + width: _state.resolution_w, + height: _state.resolution_h + }) + + // Set up resources module with backend + resources_mod.set_backend(backend) + + // Set camera aspect ratio + camera_mod.set_aspect(_state.resolution_w / _state.resolution_h) } function get_style() { return _state.style } +function get_style_config() { + return _styles[_state.style] +} + function set_resolution(w, h) { _state.resolution_w = w _state.resolution_h = h + camera_mod.set_aspect(w / h) +} + +// Switch platform style at runtime (re-resizes all cached textures) +function switch_style(style_name) { + var style = _styles[style_name] + if (!style) { + log.console("lance3d: unknown style: " + style_name) + return false + } + + _state.style = style_name + _state.style_id = style.id + _state.resolution_w = style.resolution[0] + _state.resolution_h = style.resolution[1] + + camera_mod.set_aspect(_state.resolution_w / _state.resolution_h) + + log.console("lance3d: switched to " + style_name + " style") + return true } function time() { @@ -193,506 +184,55 @@ function log_msg() { for (var i = 0; i < arguments.length; i++) { args.push(text(arguments[i])) } - log.console("[retro3d] " + args.join(" ")) + log.console("[lance3d] " + args.join(" ")) } -// Switch platform style at runtime (re-resizes all cached textures) -function switch_style(style_name) { - def style = _styles[style_name] - if (!style) { - log.console("retro3d: unknown style: " + style_name) - return false - } - - _state.style = style_name - _state.style_id = style.id - _state.resolution_w = style.resolution[0] - _state.resolution_h = style.resolution[1] - - // Invalidate texture cache - textures will be re-resized on next use - // WeakMap doesn't have clear(), so we just create a new one - // The old textures will be garbage collected - - log.console("retro3d: switched to " + style_name + " style") - return true -} - -// Get texture size for current platform and tier -function _get_tex_size(tier) { - def style = _styles[_state.style] - if (!style) return 64 - - def sizes = style.tex_sizes - if (tier == "hero" && sizes.hero) return sizes.hero - if (tier == "low" && sizes.low) return sizes.low - return sizes.normal || 64 -} - -// Resize an image to platform-appropriate size -function _resize_image_for_platform(img, tier) { - def target_size = _get_tex_size(tier) - def src_w = img.width - def src_h = img.height - - // If already at or below target size, return as-is - if (src_w <= target_size && src_h <= target_size) { - return img - } - - // Resize to fit within target_size x target_size (square) - def scale = target_size / Math.max(src_w, src_h) - def dst_w = Math.floor(src_w * scale) - def dst_h = Math.floor(src_h * scale) - if (dst_w < 1) dst_w = 1 - if (dst_h < 1) dst_h = 1 - - // Use nearest filter for retro look - return resize_mod.resize(img, dst_w, dst_h, { filter: "nearest" }) -} - -// Create a texture with platform-appropriate sizing, storing original for re-resize -function _create_texture_for_platform(w, h, pixels, tier) { - def original = { width: w, height: h, pixels: pixels } - def img = _resize_image_for_platform(original, tier) - def tex = _create_texture(img.width, img.height, img.pixels) - - // Tag texture with current style and tier for cache invalidation - tex._style_tag = _state.style - tex._tier = tier || "normal" - - // Store original for re-resizing on style switch - tex[TEX_ORIGINAL] = original - // _texture_originals.set(tex, original) - not using WeakMap anymore - - return tex -} - -// Get or create resized texture for current platform -function _get_platform_texture(tex, tier) { - if (!tex) return _state.white_texture - - // Check if texture needs re-resizing (style changed) - if (tex._style_tag != _state.style || tex._tier != tier) { - def original = tex[TEX_ORIGINAL] - if (original) { - def img = _resize_image_for_platform(original, tier) - // Create new GPU texture with resized data - def new_tex = _create_texture(img.width, img.height, img.pixels) - new_tex._style_tag = _state.style - new_tex._tier = tier - new_tex[TEX_ORIGINAL] = original - return new_tex +function set_lighting(opts) { + if (opts.sun_dir) { + var d = opts.sun_dir + var len = Math.sqrt(d[0]*d[0] + d[1]*d[1] + d[2]*d[2]) + if (len > 0) { + _state.lighting.sun_dir = [d[0]/len, d[1]/len, d[2]/len] } } - - return tex + if (opts.sun_color) _state.lighting.sun_color = opts.sun_color.slice() + if (opts.ambient) _state.lighting.ambient = opts.ambient.slice() +} + +function set_fog(opts) { + if (opts.enabled != null) _state.fog.enabled = opts.enabled + if (opts.color) _state.fog.color = opts.color.slice() + if (opts.near != null) _state.fog.near = opts.near + if (opts.far != null) _state.fog.far = opts.far } // ============================================================================ -// 2) Assets - Models, Textures, Audio +// Draw API - Models & Meshes // ============================================================================ -// load_model(path, [opts]) -// opts.type: "normal" (default) or "hero" - applies texture tier to all materials function load_model(path, opts) { opts = opts || {} var tex_tier = opts.type || "normal" - - var ext = path.slice(path.lastIndexOf('.') + 1).toLowerCase() - - if (ext == "obj") { - var data = io.slurp(path) - if (!data) return null - var parsed = obj_loader.decode(data) - if (!parsed) return null - return _load_obj_model(parsed, tex_tier) - } - - if (ext != "gltf" && ext != "glb") { - log.console("retro3d: unsupported model format: " + ext) - return null - } - - // Parse gltf + pull/decode used images - var g = gltf.load(path, {pull_images:true, decode_images:true, mode:"used"}) - 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: [], - animations: [], - animation_count: g.animations ? g.animations.length : 0, - skins: [], - _gltf: g, - _tex_tier: tex_tier, - _original_images: [] - } - - // Load textures from decoded gltf images with platform-appropriate sizing - for (var ti = 0; ti < g.images.length; ti++) { - var img = g.images[ti] - var tex = null - if (img && img.pixels) { - // Store original image data for re-resizing on style switch - model._original_images.push({ - width: img.pixels.width, - height: img.pixels.height, - pixels: img.pixels.pixels - }) - tex = _create_texture_for_platform(img.pixels.width, img.pixels.height, img.pixels.pixels, tex_tier) - } else { - model._original_images.push(null) - } - model.textures.push(tex) - } - - // Create materials from glTF material data - var gltf_mats = g.materials || [] - for (var mi = 0; mi < gltf_mats.length; mi++) { - var gmat = gltf_mats[mi] - - // Get base color factor (default white) - var base_color = [1, 1, 1, 1] - if (gmat.pbr && gmat.pbr.base_color_factor) { - base_color = gmat.pbr.base_color_factor.slice() - } - - // Get texture if present - var tex = null - if (gmat.pbr && gmat.pbr.base_color_texture) { - var tex_info = gmat.pbr.base_color_texture - var tex_obj = g.textures[tex_info.texture] - if (tex_obj && tex_obj.image != null && model.textures[tex_obj.image]) { - tex = model.textures[tex_obj.image] - } - } - - // Convert alpha_mode string to our format - var alpha_mode = "opaque" - if (gmat.alpha_mode == "MASK") alpha_mode = "mask" - else if (gmat.alpha_mode == "BLEND") alpha_mode = "blend" - - // Check for unlit extension (KHR_materials_unlit) - var is_unlit = gmat.unlit || false - - var mat = make_material(is_unlit ? "unlit" : "lit", { - texture: tex, - color: base_color, - alpha_mode: alpha_mode, - alpha_cutoff: gmat.alpha_cutoff != null ? gmat.alpha_cutoff : 0.5, - double_sided: gmat.double_sided || false, - unlit: is_unlit - }) - mat.name = gmat.name - model.materials.push(mat) - } - - // 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) - } - } - } - - // Prepare animations - model.animations = anim_mod.prepare_animations(model) - model.animation_count = model.animations.length - - // Prepare skins - model.skins = skin_mod.prepare_skins(model) - - return model + var style = _styles[_state.style] + return resources_mod.load_model(path, style, tex_tier) } -function _process_gltf_primitive(g, buffer_blob, prim, textures) { - var attrs = prim.attributes - if (attrs.POSITION == null) 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 color_acc = attrs.COLOR_0 != null ? g.accessors[attrs.COLOR_0] : null - var joints_acc = attrs.JOINTS_0 != null ? g.accessors[attrs.JOINTS_0] : null - var weights_acc = attrs.WEIGHTS_0 != null ? g.accessors[attrs.WEIGHTS_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 vertex colors (COLOR_0) - var colors = null - if (color_acc) { - var color_view = g.views[color_acc.view] - colors = model_c.extract_accessor( - buffer_blob, - color_view.byte_offset || 0, - color_view.byte_stride || 0, - color_acc.byte_offset || 0, - color_acc.count, - color_acc.component_type, - color_acc.type - ) - } - - // Extract joints (for skinned meshes) - var joints = null - if (joints_acc) { - var joints_view = g.views[joints_acc.view] - joints = model_c.extract_accessor( - buffer_blob, - joints_view.byte_offset || 0, - joints_view.byte_stride || 0, - joints_acc.byte_offset || 0, - joints_acc.count, - joints_acc.component_type, - joints_acc.type - ) - } - - // Extract weights (for skinned meshes) - var weights = null - if (weights_acc) { - var weights_view = g.views[weights_acc.view] - weights = model_c.extract_accessor( - buffer_blob, - weights_view.byte_offset || 0, - weights_view.byte_stride || 0, - weights_acc.byte_offset || 0, - weights_acc.count, - weights_acc.component_type, - weights_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: colors, - joints: joints, - weights: weights - } - 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, - skinned: packed.skinned || false, - stride: packed.stride - } -} - -function _load_obj_model(parsed, tex_tier) { - if (!parsed || !parsed.meshes || parsed.meshes.length == 0) return null - - var model = { - meshes: [], - nodes: [], - root_nodes: [], - textures: [], - materials: [], - animations: [], - animation_count: 0, - _tex_tier: tex_tier || "normal", - _original_images: [] - } - - 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 _flip_tri_winding(indices) { - for (var i = 0; i < indices.length; i += 3) { - var t = indices[i + 1] - indices[i + 1] = indices[i + 2] - indices[i + 2] = t - } - return indices +// Recalculate model textures for current style (call after switch_style) +function recalc_model_textures(model) { + var style = _styles[_state.style] + var tier = model._internal ? model._internal._tex_tier : "normal" + resources_mod.recalc_model_textures(model, style, tier) } 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 ] @@ -723,7 +263,7 @@ function make_cube(w, h, d) { 20,21,22, 20,22,23 ] - return _make_model_from_arrays(positions, normals, uvs, indices) + return _make_mesh_from_arrays(positions, normals, uvs, indices) } function make_sphere(r, segments) { @@ -759,7 +299,7 @@ function make_sphere(r, segments) { } } - return _make_model_from_arrays(positions, normals, uvs, indices) + return _make_mesh_from_arrays(positions, normals, uvs, indices) } function make_cylinder(r, h, segments) { @@ -770,7 +310,6 @@ function make_cylinder(r, h, segments) { 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 @@ -786,14 +325,13 @@ function make_cylinder(r, h, segments) { 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) + return _make_mesh_from_arrays(positions, normals, uvs, indices) } function make_plane(w, h) { @@ -802,608 +340,129 @@ function make_plane(w, h) { 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) + return _make_mesh_from_arrays(positions, normals, uvs, indices) } -function make_cone(r, h, segments) { - if (!segments) segments = 12 - var positions = [] - var normals = [] - var uvs = [] - var indices = [] - - // Apex vertex - var apex_y = h / 2 - positions.push(0, apex_y, 0) - normals.push(0, 1, 0) // Pointing up for apex - uvs.push(0.5, 1) - - // Base vertices - for (var i = 0; i <= segments; i++) { - var u = i / segments - var angle = u * 2 * 3.14159265 - var x = Math.cos(angle) * r - var z = Math.sin(angle) * r - positions.push(x, -apex_y, z) - - // Normals for base - pointing outward - normals.push(x / r, 0, z / r) - uvs.push(u, 0) - } - - // Side triangles - for (var i = 0; i < segments; i++) { - var base_idx = i + 1 - var next_base_idx = (i + 1) % segments + 1 - indices.push(0, base_idx, next_base_idx) - } - - // Base triangles (fan) - for (var i = 1; i < segments; i++) { - indices.push(1, i + 1, i + 2) - } - - _flip_tri_winding(indices) - return _make_model_from_arrays(positions, normals, uvs, indices) -} - -function make_capsule(r, h, segments) { - if (!segments) segments = 12 - var positions = [] - var normals = [] - var uvs = [] - var indices = [] - var vertex_offset = 0 - - // Cylinder body height (total height minus two radius caps) - var body_height = Math.max(0, h - 2 * r) - var half_body = body_height / 2 - - // Top hemisphere - for (var lat = 0; lat <= segments / 2; lat++) { - var v = lat / (segments / 2) - var phi = v * 3.14159265 / 2 // 0 to π/2 - - for (var lon = 0; lon <= segments; lon++) { - var u = lon / segments - var theta = u * 2 * 3.14159265 - - var x = Math.sin(phi) * Math.cos(theta) * r - var y = Math.cos(phi) * r + half_body - var z = Math.sin(phi) * Math.sin(theta) * r - - positions.push(x, y, z) - normals.push(x / r, y / r, z / r) - uvs.push(u, v) - } - } - - // Bottom hemisphere - vertex_offset += (segments / 2 + 1) * (segments + 1) - for (var lat = 0; lat <= segments / 2; lat++) { - var v = lat / (segments / 2) - var phi = v * 3.14159265 / 2 + 3.14159265 / 2 // π/2 to π - - for (var lon = 0; lon <= segments; lon++) { - var u = lon / segments - var theta = u * 2 * 3.14159265 - - var x = Math.sin(phi) * Math.cos(theta) * r - var y = Math.cos(phi) * r - half_body - var z = Math.sin(phi) * Math.sin(theta) * r - - positions.push(x, y, z) - normals.push(x / r, y / r, z / r) - uvs.push(u, 1 - v) - } - } - - // Cylinder body (only if there's body height) - if (body_height > 0) { - vertex_offset += (segments / 2 + 1) * (segments + 1) - - // Top ring - for (var i = 0; i <= segments; i++) { - var u = i / segments - var angle = u * 2 * 3.14159265 - var x = Math.cos(angle) * r - var z = Math.sin(angle) * r - positions.push(x, half_body, z) - normals.push(x / r, 0, z / r) - uvs.push(u, 0.5) - } - - // Bottom ring - for (var i = 0; i <= segments; i++) { - var u = i / segments - var angle = u * 2 * 3.14159265 - var x = Math.cos(angle) * r - var z = Math.sin(angle) * r - positions.push(x, -half_body, z) - normals.push(x / r, 0, z / r) - uvs.push(u, 0) - } - } - - // Generate indices for top hemisphere - for (var lat = 0; lat < segments / 2; lat++) { - for (var lon = 0; lon < segments; lon++) { - var a = lat * (segments + 1) + lon - var b = a + segments + 1 - indices.push(a, b, a + 1) - indices.push(b, b + 1, a + 1) - } - } - - // Generate indices for bottom hemisphere - var bottom_offset = (segments / 2 + 1) * (segments + 1) - for (var lat = 0; lat < segments / 2; lat++) { - for (var lon = 0; lon < segments; lon++) { - var a = bottom_offset + lat * (segments + 1) + lon - var b = a + segments + 1 - indices.push(a, a + 1, b) - indices.push(b, a + 1, b + 1) - } - } - - // Generate indices for cylinder body - if (body_height > 0) { - var cylinder_offset = 2 * (segments / 2 + 1) * (segments + 1) - for (var i = 0; i < segments; i++) { - var top_idx = cylinder_offset + i - var bottom_idx = cylinder_offset + segments + 1 + i - indices.push(top_idx, bottom_idx, top_idx + 1) - indices.push(bottom_idx, bottom_idx + 1, top_idx + 1) - } - } - - _flip_tri_winding(indices) - 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) { +function load_texture(path, 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 - } + var tier = opts.type || "normal" + var style = _styles[_state.style] + return resources_mod.load_texture(path, style, tier) } -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) +// ============================================================================ +// Animation API +// ============================================================================ + +function anim_info(model) { + if (!model || !model._internal) return [] + var internal = model._internal + var result = [] + for (var i = 0; i < internal.animations.length; i++) { + var anim = internal.animations[i] + result.push({ + name: anim.name || ("clip_" + text(i)), + duration: anim.duration || 0, + index: i + }) } - child.parent = parent - if (parent) parent.children.push(child) - transform_mark_dirty(child) + return result } -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) +function sample_pose(model, name, time_val) { + if (!model || !model._internal) return null + var internal = model._internal + + // Find animation by name or index + var anim_idx = -1 + if (typeof name == "number") { + anim_idx = name } 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) -} - -function _v3_cross(ax, ay, az, bx, by, bz) { - return { - x: ay * bz - az * by, - y: az * bx - ax * bz, - z: ax * by - ay * bx - } -} - -function _v3_norm(x, y, z) { - var len = Math.sqrt(x * x + y * y + z * z) - if (len <= 0) return { x: 0, y: 0, z: 0 } - return { x: x / len, y: y / len, z: z / len } -} - -function screen_ray(screen_x, screen_y) { - var c = _state.camera - var w = _state.resolution_w * 2 - var h = _state.resolution_h * 2 - if (_state.window && _state.window.sizeInPixels) { - var s = _state.window.sizeInPixels - if (s && s.length >= 2) { - w = s[0] - h = s[1] - } - } - if (w <= 0 || h <= 0) return null - - var nx = (screen_x / w) * 2 - 1 - var ny = 1 - (screen_y / h) * 2 - - var fx = c.target_x - c.x - var fy = c.target_y - c.y - var fz = c.target_z - c.z - var f = _v3_norm(fx, fy, fz) - - var up = _v3_norm(c.up_x, c.up_y, c.up_z) - var r = _v3_cross(f.x, f.y, f.z, up.x, up.y, up.z) - r = _v3_norm(r.x, r.y, r.z) - var u = _v3_cross(r.x, r.y, r.z, f.x, f.y, f.z) - u = _v3_norm(u.x, u.y, u.z) - - var aspect = _state.resolution_w / _state.resolution_h - var tan_half = Math.tan((c.fov * 0.5) * 3.14159265 / 180) - - var dx = f.x + r.x * (nx * aspect * tan_half) + u.x * (ny * tan_half) - var dy = f.y + r.y * (nx * aspect * tan_half) + u.y * (ny * tan_half) - var dz = f.z + r.z * (nx * aspect * tan_half) + u.z * (ny * tan_half) - var d = _v3_norm(dx, dy, dz) - - return { - ox: c.x, - oy: c.y, - oz: c.z, - dx: d.x, - dy: d.y, - dz: d.z - } -} - -function screen_cast_ground(screen_x, screen_y, ground_y) { - ground_y = ground_y != null ? ground_y : 0 - var ray = screen_ray(screen_x, screen_y) - if (!ray) return null - if (ray.dy == 0) return null - var t = (ground_y - ray.oy) / ray.dy - if (t <= 0) return null - return { - x: ray.ox + ray.dx * t, - y: ray.oy + ray.dy * t, - z: ray.oz + ray.dz * t, - t: t - } -} - -// ============================================================================ -// 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 -// ============================================================================ - -// Uber material - base prototype for all materials with sane defaults -var _uber_material = { - kind: "lit", // "lit" or "unlit" - texture: null, - color: [1, 1, 1, 1], // base_color_factor (RGBA) - alpha_mode: "opaque", // "opaque", "mask", or "blend" - alpha_cutoff: 0.5, // for mask mode - double_sided: false, - unlit: false, - hero: false // if true, uses hero texture tier instead of normal -} - -function make_material(kind, opts) { - opts = opts || {} - var mat = Object.create(_uber_material) - mat.kind = kind || "lit" - mat.texture = opts.texture || null - mat.color = opts.color || [1, 1, 1, 1] - mat.alpha_mode = opts.alpha_mode || "opaque" - mat.alpha_cutoff = opts.alpha_cutoff != null ? opts.alpha_cutoff : 0.5 - mat.double_sided = opts.double_sided || false - mat.unlit = kind == "unlit" || opts.unlit || false - mat.hero = opts.hero || false - return mat -} - -// Recalculate all textures in a model for the current platform style -// Call this after switch_style() to update model textures -function recalc_model_textures(model, tier_override) { - if (!model || !model._original_images) return - - var tier = tier_override || model._tex_tier || "normal" - model._tex_tier = tier - - // Resize all textures from originals - for (var i = 0; i < model._original_images.length; i++) { - var orig = model._original_images[i] - if (!orig) continue - - var img = _resize_image_for_platform(orig, tier) - var new_tex = _create_texture(img.width, img.height, img.pixels) - new_tex._style_tag = _state.style - new_tex._tier = tier - new_tex[TEX_ORIGINAL] = orig - - // Update model's texture array - model.textures[i] = new_tex - } - - // Update material textures to point to new resized textures - if (model._gltf && model._gltf.materials) { - for (var mi = 0; mi < model.materials.length; mi++) { - var mat = model.materials[mi] - var gmat = model._gltf.materials[mi] - if (gmat && gmat.pbr && gmat.pbr.base_color_texture) { - var tex_info = gmat.pbr.base_color_texture - var tex_obj = model._gltf.textures[tex_info.texture] - if (tex_obj && tex_obj.image != null && model.textures[tex_obj.image]) { - mat.texture = model.textures[tex_obj.image] - } + for (var i = 0; i < internal.animations.length; i++) { + if (internal.animations[i].name == name) { + anim_idx = i + break } } } - // Update mesh textures - for (var mi = 0; mi < model.meshes.length; mi++) { - var mesh = model.meshes[mi] - if (mesh.material_index != null && model.materials[mesh.material_index]) { - mesh.texture = model.materials[mesh.material_index].texture - } + if (anim_idx < 0 || anim_idx >= internal.animations.length) { + return null } -} - -function set_material(material) { - _state.current_material = material -} - -function get_material() { - return _state.current_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 - - // Build joint palettes for all skins (if any) - var skin_palettes = [] - if (model.skins && model.skins.length > 0) { - for (var si = 0; si < model.skins.length; si++) { - var skin = model.skins[si] - // Collect world matrices for each joint + + var anim = internal.animations[anim_idx] + var duration = anim.duration || 0 + + // Clamp time + if (time_val < 0) time_val = 0 + if (time_val > duration) time_val = duration + + // Create a temporary animation instance and sample it + var instance = anim_mod.create_instance(internal) + anim_mod.play(instance, anim_idx, false) + anim_mod.set_time(instance, time_val) + anim_mod.apply(instance) + + // Build pose: array of node transforms + var pose = { + _internal: internal, + _anim_idx: anim_idx, + _time: time_val, + node_matrices: [] + } + + for (var ni = 0; ni < internal.nodes.length; ni++) { + pose.node_matrices.push(resources_mod.get_transform_world_matrix(internal.nodes[ni])) + } + + // Build skin palettes if model has skins + if (internal.skins && internal.skins.length > 0) { + pose.skin_palettes = [] + for (var si = 0; si < internal.skins.length; si++) { + var skin = internal.skins[si] var world_matrices = [] for (var j = 0; j < skin.joints.length; j++) { var node_idx = skin.joints[j] - var jnode = model.nodes[node_idx] + var jnode = internal.nodes[node_idx] if (jnode) { - var jworld = transform_get_world_matrix(jnode) - // Apply extra transform if provided + world_matrices.push(resources_mod.get_transform_world_matrix(jnode)) + } else { + world_matrices.push(model_c.mat4_identity()) + } + } + var palette = model_c.build_joint_palette(world_matrices, skin.inv_bind, skin.joint_count) + pose.skin_palettes.push(palette) + } + } + + return pose +} + +// ============================================================================ +// Drawing +// ============================================================================ + +function draw_model(model, transform, pose) { + if (!model || !model._internal) return + var internal = model._internal + + var view_matrix = camera_mod.get_view_matrix() + var proj_matrix = camera_mod.get_proj_matrix() + var extra_transform = transform || null + + // Get skin palettes from pose or build default + var skin_palettes = [] + if (pose && pose.skin_palettes) { + skin_palettes = pose.skin_palettes + } else if (internal.skins && internal.skins.length > 0) { + for (var si = 0; si < internal.skins.length; si++) { + var skin = internal.skins[si] + var world_matrices = [] + for (var j = 0; j < skin.joints.length; j++) { + var node_idx = skin.joints[j] + var jnode = internal.nodes[node_idx] + if (jnode) { + var jworld = resources_mod.get_transform_world_matrix(jnode) if (extra_transform) { jworld = model_c.mat4_mul(extra_transform, jworld) } @@ -1412,1119 +471,141 @@ function draw_model(model, transform, anim_instance) { world_matrices.push(model_c.mat4_identity()) } } - // Build palette using C function var palette = model_c.build_joint_palette(world_matrices, skin.inv_bind, skin.joint_count) skin_palettes.push(palette) } } - - // 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 - - // Determine which material to use: - // 1. If current_material is set, use it (user override) - // 2. Otherwise use the mesh's material from the model - // 3. Fall back to uber material defaults - var mat = _state.current_material - if (!mat && mesh.material_index != null && model.materials[mesh.material_index]) { - mat = model.materials[mesh.material_index] - } - if (!mat) { - mat = _uber_material - } - - // Get texture: prefer mesh texture, then material texture, then white - var tex = mesh.texture || mat.texture || _state.white_texture - - // Build uniforms with material properties - var uniforms = _build_uniforms(world_matrix, view_matrix, proj_matrix, mat) - - // Get palette for skinned mesh (use first skin for now) - var palette = null - if (mesh.skinned && skin_palettes.length > 0) { - palette = skin_palettes[0] - } - - _draw_mesh(mesh, uniforms, tex, mat, palette) - _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() - - for (var i = 0; i < model.meshes.length; i++) { - var mesh = model.meshes[i] - - // Determine material - var mat = _state.current_material - if (!mat && mesh.material_index != null && model.materials[mesh.material_index]) { - mat = model.materials[mesh.material_index] - } - if (!mat) { - mat = _uber_material - } - - var tex = mesh.texture || mat.texture || _state.white_texture - var uniforms = _build_uniforms(world_matrix, view_matrix, proj_matrix, mat) - - // Get palette for skinned mesh - var palette = null - if (mesh.skinned && skin_palettes.length > 0) { - palette = skin_palettes[0] - } - - _draw_mesh(mesh, uniforms, tex, mat, palette) - _state.draw_calls++ - _state.triangles += mesh.index_count / 3 - } - } -} - -// Build uniform buffer using C helper (blob API doesn't have write_f32) -// mat can be a material object or a tint array for backwards compatibility -function _build_uniforms(model_mat, view_mat, proj_mat, mat) { - // Handle backwards compatibility: if mat is an array, treat as tint - var tint = [1, 1, 1, 1] - var alpha_mode = 0 // 0=opaque, 1=mask, 2=blend - var alpha_cutoff = 0.5 - var unlit = 0 - - if (Array.isArray(mat)) { - tint = mat - } else if (mat) { - tint = mat.color || [1, 1, 1, 1] - if (mat.alpha_mode == "mask") alpha_mode = 1 - else if (mat.alpha_mode == "blend") alpha_mode = 2 - alpha_cutoff = mat.alpha_cutoff != null ? mat.alpha_cutoff : 0.5 - unlit = (mat.unlit || mat.kind == "unlit") ? 1 : 0 - } - - 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, - alpha_mode: alpha_mode, - alpha_cutoff: alpha_cutoff, - unlit: unlit - }) -} - -// 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 = [] -} - -// ============================================================================ -// 7b) Sprites & Billboards -// ============================================================================ - -// Supported image extensions for sprite loading -var _sprite_extensions = ["png", "jpg", "jpeg", "gif", "bmp", "tga"] - -// Scan a folder and build manifest of available sprites (base_path -> extension) -function _scan_sprite_folder(folder) { - if (_state.sprite_manifest) return // Already scanned - _state.sprite_manifest = {} - var list = io.readdir(folder) - if (!list) return - - for (var i = 0; i < list.length; i++) { - var item = list[i] - var dot_idx = item.lastIndexOf('.') - if (dot_idx < 0) continue + // Draw each mesh in the model array + for (var i = 0; i < model.length; i++) { + var entry = model[i] + var mesh = entry.mesh + var mat = entry.material + var node_idx = entry._node_index - var base = item.slice(0, dot_idx) - var ext = item.slice(dot_idx + 1).toLowerCase() + // Get node world matrix (from pose or computed) + var node_world + if (pose && pose.node_matrices && pose.node_matrices[node_idx]) { + node_world = pose.node_matrices[node_idx] + } else { + node_world = resources_mod.get_transform_world_matrix(internal.nodes[node_idx]) + } - // Check if it's a supported image extension - var supported = false - for (var j = 0; j < _sprite_extensions.length; j++) { - if (ext == _sprite_extensions[j]) { - supported = true - break - } - } - if (!supported) continue + // Apply extra transform + var world_matrix = extra_transform + ? model_c.mat4_mul(extra_transform, node_world) + : node_world - // Build full base path (folder/base) - var base_path = folder + "/" + base + var tex = mesh.texture || mat.color_map || backend.get_white_texture() + var uniforms = _build_uniforms(world_matrix, view_matrix, proj_matrix, mat) - // Only store first found extension (no overwriting) - if (!_state.sprite_manifest[base_path]) { - _state.sprite_manifest[base_path] = ext + var palette = null + if (mesh.skinned && skin_palettes.length > 0) { + palette = skin_palettes[0] } + + _queue_draw(mesh, uniforms, tex, mat, palette) } } -// Load a sprite by base path (e.g. "assets/goblin" checks for goblin.png, goblin.jpg, etc) -function load_sprite(path) { - // Check cache first - if (_state.sprite_cache[path]) { - return _state.sprite_cache[path] - } +function draw_mesh(mesh, transform, material) { + var view_matrix = camera_mod.get_view_matrix() + var proj_matrix = camera_mod.get_proj_matrix() + var world_matrix = transform || model_c.mat4_identity() + var mat = material || _default_material - // Extract folder and base name - var slash_idx = path.lastIndexOf('/') - var folder = slash_idx >= 0 ? path.slice(0, slash_idx) : "." + var tex = mat.color_map || backend.get_white_texture() + var uniforms = _build_uniforms(world_matrix, view_matrix, proj_matrix, mat) - // Scan folder if not already done - _scan_sprite_folder(folder) - - // Look up in manifest - var ext = _state.sprite_manifest[path] - if (!ext) { - log.console("retro3d: sprite not found: " + path) - return null - } - - // Load the image - var full_path = path + "." + ext - var data = io.slurp(full_path) - if (!data) { - log.console("retro3d: failed to load sprite file: " + full_path) - return null - } - - // Decode based on extension - var img = null - if (ext == "png") { - img = png.decode(data) - } else { - // For other formats, try png decoder (may need to add other decoders) - img = png.decode(data) - } - - if (!img) { - log.console("retro3d: failed to decode sprite: " + full_path) - return null - } - - // Create texture - var tex = _create_texture(img.width, img.height, img.pixels) - - // Create sprite object - var sprite = { - texture: tex, - width: img.width, - height: img.height, - path: path - } - - // Cache it - _state.sprite_cache[path] = sprite - - return sprite + _queue_draw(mesh, uniforms, tex, mat, null) } -// Create shared quad mesh for sprites/billboards (unit quad, bottom-left origin) -function _get_sprite_quad() { - if (_state.sprite_quad) return _state.sprite_quad +function draw_billboard(texture, x, y, z, size, mat) { + if (!texture) return + size = size || 1.0 + mat = mat || _default_material - // Unit quad with bottom-left at origin, extends to (1, 1) - // Positions: x, y, z (z=0 for 2D) - var positions = [ - 0, 0, 0, // bottom-left - 1, 0, 0, // bottom-right - 1, 1, 0, // top-right - 0, 1, 0 // top-left - ] + var cam_eye = camera_mod.get_eye() + var dx = cam_eye.x - x + var dz = cam_eye.z - z + var yaw = Math.atan2(dx, dz) + var q = math_mod.euler_to_quat(0, yaw, 0) - // Normals pointing towards camera (negative Z in view space, but we'll use +Z for billboard facing) - var normals = [ - 0, 0, 1, - 0, 0, 1, - 0, 0, 1, - 0, 0, 1 - ] + var transform = math_mod.trs_matrix(x, y, z, q.x, q.y, q.z, q.w, size, size, 1) - // UVs: standard texture coordinates - var uvs = [ - 0, 1, // bottom-left - 1, 1, // bottom-right - 1, 0, // top-right - 0, 0 // top-left - ] - - var indices = [0, 1, 2, 0, 2, 3] - - _state.sprite_quad = _make_model_from_arrays(positions, normals, uvs, indices) - return _state.sprite_quad -} - -// Draw a 2D sprite at screen position (x, y in 320x240 space) -// opts: { color: [r,g,b,a], mode: "cutout" | "blend" } -function draw_sprite(sprite, x, y, opts) { - if (!sprite || !sprite.texture) return - - opts = opts || {} - var color = opts.color || [1, 1, 1, 1] - var mode = opts.mode || "cutout" - - // Get sprite dimensions - var w = sprite.width - var h = sprite.height - - // Set up orthographic projection for 2D (320x240, origin bottom-left) - push_state() - - // Save current camera and set up 2D ortho - camera_ortho(0, 320, 0, 240, -1, 1) - camera_look_at(0, 0, 0, 0, 0, -1, 0, 1, 0) - - // Create material for sprite - var alpha_mode = mode == "blend" ? "blend" : "mask" - var mat = make_material("unlit", { - texture: sprite.texture, - color: color, - alpha_mode: alpha_mode, - alpha_cutoff: 0.5, - double_sided: true - }) - set_material(mat) - - // Create transform for sprite position and scale - var transform = make_transform() - transform.x = x - transform.y = y - transform.z = 0 - transform.sx = w - transform.sy = h - transform.sz = 1 - transform_mark_dirty(transform) - - // Draw the quad - var quad = _get_sprite_quad() - draw_model(quad, transform) - - pop_state() -} - -// Draw a billboard in 3D space at world position (x, y, z) -// opts: { color: [r,g,b,a], mode: "cutout" | "blend", face: "camera" | "y" } -function draw_billboard(sprite, x, y, z, opts) { - if (!sprite || !sprite.texture) return - - opts = opts || {} - var color = opts.color || [1, 1, 1, 1] - var mode = opts.mode || "cutout" - var face = opts.face || "camera" - - // Get sprite dimensions (scale to world units - assume 1 pixel = 1/100 world unit) - var scale = 1 / 100 - var w = sprite.width * scale - var h = sprite.height * scale - - // Create material for billboard (unlit by default) - var alpha_mode = mode == "blend" ? "blend" : "mask" - var mat = make_material("unlit", { - texture: sprite.texture, - color: color, - alpha_mode: alpha_mode, - alpha_cutoff: 0.5, - double_sided: true - }) - - push_state() - set_material(mat) - - // Calculate billboard rotation to face camera - var cam = _state.camera - var dx = cam.x - x - var dy = cam.y - y - var dz = cam.z - z - - var yaw = 0 - var pitch = 0 - - if (face == "camera") { - // Full camera-facing billboard - yaw = Math.atan2(dx, dz) - var dist_xz = Math.sqrt(dx * dx + dz * dz) - pitch = Math.atan2(dy, dist_xz) - } else { - // Y-axis aligned billboard (only rotates around Y) - yaw = Math.atan2(dx, dz) - pitch = 0 + var quad = make_plane(1, 1) + var billboard_mat = { + color_map: texture, + paint: mat.paint || [1, 1, 1, 1], + coverage: mat.coverage || "cutoff", + face: "double", + lamp: "unlit" } - // Build rotation quaternion from yaw and pitch - // First rotate around Y (yaw), then around X (pitch) - var cy = Math.cos(yaw / 2) - var sy = Math.sin(yaw / 2) - var cp = Math.cos(pitch / 2) - var sp = Math.sin(pitch / 2) - - // Combine: q_yaw * q_pitch - var qx = cy * sp - var qy = sy * cp - var qz = -sy * sp - var qw = cy * cp - - // Create transform - // Anchor is bottom-center, so offset X by -w/2 - var transform = make_transform() - transform.x = x - transform.y = y - transform.z = z - transform.qx = qx - transform.qy = qy - transform.qz = qz - transform.qw = qw - transform.sx = w - transform.sy = h - transform.sz = 1 - transform_mark_dirty(transform) - - // We need to offset the quad so it's centered horizontally - // Create a child transform for the offset - var offset_transform = make_transform() - offset_transform.x = -0.5 // Offset by half width (quad is 0-1, so -0.5 centers it) - offset_transform.y = 0 - offset_transform.z = 0 - transform_set_parent(offset_transform, transform) - - // Draw the quad - var quad = _get_sprite_quad() - draw_model(quad, offset_transform) - - pop_state() + draw_mesh(quad, transform, billboard_mat) +} + +function draw_sprite(texture, x, y, size, mat) { + // 2D sprite - uses orthographic projection + // TODO: implement 2D sprite rendering } // ============================================================================ -// 8) Animation +// Debug API // ============================================================================ -function anim_instance(model) { - return anim_mod.create_instance(model) +function debug_point(vertex, size) { + size = size || 1.0 + // TODO: implement point rendering } -function anim_clip_count(model_or_instance) { - if (model_or_instance.animations) { - // It's an instance - return anim_mod.clip_count(model_or_instance) - } - // It's a model - return model_or_instance.animation_count || 0 +function debug_line(vertex_a, vertex_b, width) { + width = width || 1.0 + // TODO: implement line rendering } -function anim_clip_duration(model_or_instance, clip_index) { - if (model_or_instance.animations && model_or_instance.model) { - // It's an instance - return anim_mod.clip_duration(model_or_instance, clip_index) - } - // It's a model - create temp instance - if (!model_or_instance.animations || clip_index >= model_or_instance.animations.length) return 0 - return model_or_instance.animations[clip_index].duration || 0 -} - -function anim_clip_name(instance, clip_index) { - return anim_mod.clip_name(instance, clip_index) -} - -function anim_find_clip(instance, name) { - return anim_mod.find_clip(instance, name) -} - -function anim_play(anim, clip_index, loop) { - anim_mod.play(anim, clip_index, loop) -} - -function anim_stop(anim) { - anim_mod.stop(anim) -} - -function anim_set_time(anim, seconds) { - anim_mod.set_time(anim, seconds) -} - -function anim_set_speed(anim, speed) { - anim_mod.set_speed(anim, speed) -} - -function anim_update(anim, dt_val) { - dt_val = dt_val != null ? dt_val : _state.dt - anim_mod.update(anim, dt_val) -} - -function anim_apply(anim) { - anim_mod.apply(anim) -} - -function anim_pop_events(anim) { - return [] +function debug_grid(size, step, norm, color) { + norm = norm || {x: 0, y: 1, z: 0} + color = color || [0.5, 0.5, 0.5, 1] + // TODO: implement grid rendering } // ============================================================================ -// 8b) Skinning +// Frame management // ============================================================================ -function skin_get(model, skin_index) { - if (!model.skins || skin_index >= model.skins.length) return null - return model.skins[skin_index] +function clear(r, g, b, a) { + if (a == null) a = 1.0 + _state._clear_color = [r, g, b, a] + _state._clear_depth = true } -function skin_build_palette(skin, model) { - return skin_mod.build_palette(skin, model, { transform_get_world_matrix: transform_get_world_matrix }) -} - -function skin_get_joint_world(skin, joint_index, model) { - return skin_mod.get_joint_world(skin, joint_index, model, { transform_get_world_matrix: transform_get_world_matrix }) -} - -function skin_find_joint(skin, name, model) { - return skin_mod.find_joint(skin, name, model) -} - -function skin_attachment_transform(skin, joint_index, model, offset_mat) { - return skin_mod.attachment_transform(skin, joint_index, model, { transform_get_world_matrix: transform_get_world_matrix }, offset_mat) -} - -// ============================================================================ -// 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() { - return Math.random() -} - -function irand(min_inclusive, max_inclusive) { - return Math.floor(rand() * (max_inclusive - min_inclusive + 1)) + min_inclusive -} - -// ============================================================================ -// Internal GPU functions -// ============================================================================ - -// Get or create a pipeline for the given configuration -// skinned: bool, alpha_mode: "opaque"|"mask"|"blend", cull: "back"|"front"|"none" -function _get_pipeline(skinned, alpha_mode, cull) { - var key = `${skinned}_${alpha_mode}_${cull}` - if (_state.pipelines[key]) return _state.pipelines[key] - - // Determine blend settings based on alpha mode - var blend_enabled = alpha_mode == "blend" - var depth_write = alpha_mode != "blend" // blend mode typically doesn't write depth - - var blend_config = { enabled: false } - if (blend_enabled) { - blend_config = { - enabled: true, - src_rgb: "src_alpha", - dst_rgb: "one_minus_src_alpha", - op_rgb: "add", - src_alpha: "one", - dst_alpha: "one_minus_src_alpha", - op_alpha: "add" - } - } - - // Determine cull mode - var cull_mode = cull == "none" ? "none" : (cull == "front" ? "front" : "back") - - var vert_shader = skinned ? _state.skinned_vert_shader : _state.vert_shader - if (!vert_shader) return null - - var pitch = skinned ? 80 : 48 - var vertex_attrs = [ - { 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 } - ] - if (skinned) { - vertex_attrs.push({ location: 4, buffer_slot: 0, format: "float4", offset: 48 }) - vertex_attrs.push({ location: 5, buffer_slot: 0, format: "float4", offset: 64 }) - } - - var pipeline = new gpu_mod.graphics_pipeline(_state.gpu, { - vertex: vert_shader, - fragment: _state.frag_shader, - primitive: "triangle", - cull: cull_mode, - face: "counter_clockwise", - fill: "fill", - vertex_buffer_descriptions: [{ - slot: 0, - pitch: pitch, - input_rate: "vertex" - }], - vertex_attributes: vertex_attrs, - target: { - color_targets: [{ format: _state.swapchain_format, blend: blend_config }], - depth: "d32 float s8" - }, - depth: { - test: true, - write: depth_write, - compare: "less" - } - }) - - _state.pipelines[key] = pipeline - return pipeline -} - -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 - } - - _state.vert_shader = new gpu_mod.shader(_state.gpu, { - code: vert_code, - stage: "vertex", - format: "msl", - entrypoint: "vertex_main", - num_uniform_buffers: 2 - }) - - _state.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 - }) - - // Store swapchain format for pipeline creation - _state.swapchain_format = _state.gpu.swapchain_format(_state.window) - - // Load skinned vertex shader - var skinned_vert_code = io.slurp("shaders/retro3d_skinned.vert.msl") - if (skinned_vert_code) { - _state.skinned_vert_shader = new gpu_mod.shader(_state.gpu, { - code: skinned_vert_code, - stage: "vertex", - format: "msl", - entrypoint: "vertex_main", - num_uniform_buffers: 3 - }) - } - - // Create default pipelines (opaque, back-face culling) - _get_pipeline(false, "opaque", "back") - _get_pipeline(true, "opaque", "back") - - // 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, mat, palette) { - // This will be called during render pass - _state._pending_draws = _state._pending_draws || [] - - // Extract material properties for pipeline selection - var alpha_mode = "opaque" - var double_sided = false - if (mat && typeof mat == "object" && !Array.isArray(mat)) { - alpha_mode = mat.alpha_mode || "opaque" - double_sided = mat.double_sided || false - } - - _state._pending_draws.push({ - mesh: mesh, - uniforms: uniforms, - texture: texture, - alpha_mode: alpha_mode, - double_sided: double_sided, - palette: palette - }) -} - -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 - var ms = mouse.get_state() - if (ms) { - _state.mouse_x = ms.x - _state.mouse_y = ms.y - } - _state.mouse_dx = 0 - _state.mouse_dy = 0 + + // Begin input frame + input_mod.begin_frame() } function _process_events() { - var ev - while ((ev = events.poll()) != null) { - if (ev.type == "quit" || ev.type == "window_close_requested") { - return false - } - if (ev.type == "mouse_motion") { - _state.mouse_x = ev.x - _state.mouse_y = ev.y - _state.mouse_dx += ev.xrel - _state.mouse_dy += ev.yrel - } - 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 + return input_mod.process_events() } 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 - // Sort by alpha mode: opaque first, then mask, then blend (for proper transparency) - var draws = _state._pending_draws || [] - draws.sort(function(a, b) { - var order = { opaque: 0, mask: 1, blend: 2 } - return (order[a.alpha_mode] || 0) - (order[b.alpha_mode] || 0) - }) - - for (var i = 0; i < draws.length; i++) { - var d = draws[i] - - // Select pipeline based on skinned, alpha_mode, and double_sided - var skinned = d.mesh.skinned && d.palette - var cull = d.double_sided ? "none" : "back" - var pipeline = _get_pipeline(skinned, d.alpha_mode, cull) - - if (!pipeline) continue - - swap_pass.bind_pipeline(pipeline) - 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) - - // Push uniforms - cmd.push_vertex_uniform_data(1, d.uniforms) - cmd.push_fragment_uniform_data(1, d.uniforms) - - // Push joint palette for skinned meshes - if (skinned && d.palette) { - cmd.push_vertex_uniform_data(2, d.palette) - } - - 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() + // Submit all pending draws to backend + var result = backend.submit_frame( + _state._pending_draws, + _state._clear_color, + _state._clear_depth, + _state.style_id + ) + _state.draw_calls = result.draw_calls + _state.triangles = result.triangles _state.frame_count++ // Check triangle budget and warn (once per minute max) @@ -2533,126 +614,169 @@ function _end_frame() { // Check if triangle count exceeds platform budget, warn once per minute function _check_tri_budget() { - def style = _styles[_state.style] + var style = _styles[_state.style] if (!style || !style.tri_budget) return if (_state.triangles > style.tri_budget) { - def now = time_mod.number() + var now = time_mod.number() // Only warn once per minute (60 seconds) if (now - _tri_warning_state.last_warn_time >= 60) { - log.console("[retro3d] WARNING: Triangle count " + text(_state.triangles) + + log.console("[lance3d] WARNING: Triangle count " + text(_state.triangles) + " exceeds " + _state.style + " budget of " + text(style.tri_budget)) _tri_warning_state.last_warn_time = now } } } -// Export the module +// ============================================================================ +// Internal helpers +// ============================================================================ + +function _queue_draw(mesh, uniforms, texture, mat, palette) { + _state._pending_draws.push({ + mesh: mesh, + uniforms: uniforms, + texture: texture, + coverage: mat.coverage || "opaque", + face: mat.face || "single", + palette: palette + }) +} + +function _build_uniforms(model_mat, view_mat, proj_mat, mat) { + var paint = mat.paint || [1, 1, 1, 1] + var alpha_mode = 0 + if (mat.coverage == "cutoff") alpha_mode = 1 + else if (mat.coverage == "blend") alpha_mode = 2 + var unlit = mat.lamp == "unlit" ? 1 : 0 + + return model_c.build_uniforms({ + model: model_mat, + view: view_mat, + projection: proj_mat, + ambient: _state.lighting.ambient, + light_dir: _state.lighting.sun_dir, + light_color: _state.lighting.sun_color, + light_intensity: 1.0, + fog_near: _state.fog.enabled ? _state.fog.near : 10000, + fog_far: _state.fog.enabled ? _state.fog.far : 10001, + fog_color: _state.fog.enabled ? _state.fog.color : null, + tint: paint, + style_id: _state.style_id, + resolution_w: _state.resolution_w, + resolution_h: _state.resolution_h, + alpha_mode: alpha_mode, + alpha_cutoff: 0.5, + unlit: unlit + }) +} + +function _make_mesh_from_arrays(positions, normals, uvs, indices) { + var vertex_count = positions.length / 3 + + 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.indices = model_c.u16_blob(indices) + mesh.index_type = "uint16" + + var packed = model_c.pack_vertices(mesh) + + return { + vertex_buffer: backend.create_vertex_buffer(packed.data), + index_buffer: backend.create_index_buffer(mesh.indices), + index_count: mesh.index_count, + index_type: "uint16", + vertex_count: vertex_count, + texture: null, + skinned: false + } +} + +// ============================================================================ +// Export +// ============================================================================ + return { + // System / Style set_style: set_style, get_style: get_style, + get_style_config: get_style_config, switch_style: switch_style, set_resolution: set_resolution, time: time, dt: dt, stat: stat, log: log_msg, - + set_lighting: set_lighting, + set_fog: set_fog, + + // Transform/Matrix + identity_matrix: math_mod.identity_matrix, + translation_matrix: math_mod.translation_matrix, + rotation_matrix: math_mod.rotation_matrix, + scale_matrix: math_mod.scale_matrix, + trs_matrix: math_mod.trs_matrix, + euler_to_quat: math_mod.euler_to_quat, + euler_matrix: math_mod.euler_matrix, + multiply_matrices: math_mod.multiply_matrices, + + // Draw API load_model: load_model, recalc_model_textures: recalc_model_textures, make_cube: make_cube, make_sphere: make_sphere, make_cylinder: make_cylinder, make_plane: make_plane, - make_cone: make_cone, - make_capsule: make_capsule, - make_model: make_model, load_texture: load_texture, - make_texture: make_texture, - load_sprite: load_sprite, - draw_sprite: draw_sprite, - draw_billboard: draw_billboard, - 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, - screen_ray: screen_ray, - screen_cast_ground: screen_cast_ground, - - push_state: push_state, - pop_state: pop_state, - - make_material: make_material, - set_material: set_material, - get_material: get_material, - set_ambient: set_ambient, - set_light_dir: set_light_dir, - set_fog: set_fog, - - clear: clear, - clear_depth: clear_depth, + anim_info: anim_info, + sample_pose: sample_pose, 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_clip_name: anim_clip_name, - anim_find_clip: anim_find_clip, - 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_apply: anim_apply, - anim_pop_events: anim_pop_events, + draw_mesh: draw_mesh, + draw_billboard: draw_billboard, + draw_sprite: draw_sprite, - skin_get: skin_get, - skin_build_palette: skin_build_palette, - skin_get_joint_world: skin_get_joint_world, - skin_find_joint: skin_find_joint, - skin_attachment_transform: skin_attachment_transform, - - 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 + // Camera + camera_look_at: camera_mod.look_at, + camera_perspective: camera_mod.perspective, + camera_ortho: camera_mod.ortho, + + // Collision + add_collider_sphere: collision_mod.add_collider_sphere, + add_collider_box: collision_mod.add_collider_box, + remove_collider: collision_mod.remove_collider, + overlaps: collision_mod.overlaps, + raycast: collision_mod.raycast, + + // Input + btn: input_mod.btn, + btnp: input_mod.btnp, + key: input_mod.key, + keyp: input_mod.keyp, + axis: input_mod.axis, + + // Math + seed: math_mod.seed, + rand: math_mod.rand, + irand: math_mod.irand, + + // Debug + point: debug_point, + line: debug_line, + grid: debug_grid, + + // Frame management + clear: clear, + + // Internal (for runner) _state: _state, _begin_frame: _begin_frame, _process_events: _process_events, _end_frame: _end_frame -} \ No newline at end of file +} diff --git a/examples/billboard_test.ce b/examples/billboard_test.ce deleted file mode 100644 index b6c152a..0000000 --- a/examples/billboard_test.ce +++ /dev/null @@ -1,156 +0,0 @@ -// Billboard Test for retro3d -// Usage: cell run examples/billboard_test.ce -// sprite_path should be without extension, e.g. "assets/goblin" -// Creates two rows of billboards going off into the distance like alongside a road -// Colors shift through hues as they get further away - -var time_mod = use('time') -var retro3d = use('core') - -var sprite_path = args[0] -if (!sprite_path) { - log.console("Usage: cell run examples/billboard_test.ce ") - log.console(" sprite_path: path without extension (e.g. 'assets/goblin')") - $_.stop() -} - -var sprite = null -var last_time = 0 -var cam_angle = 0 - -function hsv_to_rgb(h, s, v) { - var c = v * s - var x = c * (1 - Math.abs((h / 60) % 2 - 1)) - var m = v - c - - var r = 0, g = 0, b = 0 - if (h < 60) { r = c; g = x; b = 0 } - else if (h < 120) { r = x; g = c; b = 0 } - else if (h < 180) { r = 0; g = c; b = x } - else if (h < 240) { r = 0; g = x; b = c } - else if (h < 300) { r = x; g = 0; b = c } - else { r = c; g = 0; b = x } - - return [r + m, g + m, b + m, 1] -} - -function _init() { - log.console("retro3d Billboard Test") - log.console("Loading sprite: " + sprite_path) - - retro3d.set_style("ps1") - - sprite = retro3d.load_sprite(sprite_path) - if (!sprite) { - log.console("Error: Could not load sprite: " + sprite_path) - $_.stop() - return - } - - log.console("Sprite loaded: " + text(sprite.width) + "x" + text(sprite.height)) - log.console("Controls:") - log.console(" A/D - Rotate camera") - log.console(" ESC - Exit") - - retro3d.set_ambient(0.4, 0.4, 0.5) - retro3d.set_light_dir(0.5, 1.0, 0.3, 1.0, 0.95, 0.9, 1.0) - - last_time = time_mod.number() - frame() -} - -function _update(dt) { - if (retro3d._state.keys_held['escape']) { - $_.stop() - } - - if (retro3d._state.keys_held['a']) { - cam_angle = cam_angle - 1.5 * dt - } - if (retro3d._state.keys_held['d']) { - cam_angle = cam_angle + 1.5 * dt - } -} - -function _draw() { - retro3d.clear(0.2, 0.3, 0.4, 1.0) - - if (!sprite) return - - // Set up perspective camera - retro3d.camera_perspective(60, 0.1, 100) - - // Camera orbits around origin - var cam_dist = 8 - var cam_x = Math.sin(cam_angle) * cam_dist - var cam_z = Math.cos(cam_angle) * cam_dist - var cam_y = 2 - - retro3d.camera_look_at(cam_x, cam_y, cam_z, 0, 1, 0) - - // Draw ground plane - retro3d.push_state() - var ground_mat = retro3d.make_material("lit", { - color: [0.3, 0.4, 0.3, 1] - }) - retro3d.set_material(ground_mat) - var ground = retro3d.make_plane(20, 20) - var ground_transform = retro3d.make_transform() - retro3d.draw_model(ground, ground_transform) - retro3d.pop_state() - - // Draw two rows of billboards going into the distance - // Left row at x = -2, right row at x = 2 - var num_billboards = 10 - var spacing = 2.0 - var start_z = -2 - - for (var i = 0; i < num_billboards; i++) { - var z = start_z - i * spacing - var distance_norm = i / (num_billboards - 1) // 0 to 1 - - // Hue shifts from 0 (red) to 270 (purple) as distance increases - var hue = distance_norm * 270 - var color = hsv_to_rgb(hue, 0.9, 1.0) - - // Left row - retro3d.draw_billboard(sprite, -2, 0, z, { - color: color, - mode: "cutout", - face: "y" - }) - - // Right row (slightly different hue offset) - var hue_right = (hue + 30) % 360 - var color_right = hsv_to_rgb(hue_right, 0.9, 1.0) - - retro3d.draw_billboard(sprite, 2, 0, z, { - color: color_right, - mode: "cutout", - face: "y" - }) - } -} - -function frame() { - retro3d._begin_frame() - - if (!retro3d._process_events()) { - log.console("Exiting...") - $_.stop() - return - } - - var now = time_mod.number() - var dt = now - last_time - last_time = now - - _update(dt) - _draw() - - retro3d._end_frame() - - $_.delay(frame, 1/60) -} - -_init() diff --git a/examples/cube.ce b/examples/cube.ce index 8605ffa..704c5a1 100644 --- a/examples/cube.ce +++ b/examples/cube.ce @@ -1,13 +1,8 @@ -// Simple Cube Demo for retro3d -// Usage: cell run examples/cube.ce [style] -// style: ps1, n64, or saturn (default: ps1) +// Simple Cube Demo for lance3d +// Usage: cell run examples/cube.ce var time_mod = use('time') -var retro3d = use('core') - -// Parse command line arguments -var args = $_.args || [] -var style = args[0] || "ps1" +var lance3d = use('core') // Camera orbit state var cam_distance = 5 @@ -15,36 +10,47 @@ var cam_yaw = 0 var cam_pitch = 0.4 var auto_rotate = true -// Model and transform +// Models and materials var cube = null -var transform = null +var cube_mat = null +var ground = null +var ground_mat = null // Timing var last_time = 0 function _init() { - log.console("retro3d Cube Demo") - log.console("Style: " + style) + log.console("lance3d Cube Demo") - // Initialize retro3d with selected style - retro3d.set_style(style) + // Initialize lance3d with PS1 style + lance3d.set_style("ps1") - // Create a cube - cube = retro3d.make_cube(1, 1, 1) + // Create a cube mesh + cube = lance3d.make_cube(1, 1, 1) + cube_mat = { + color_map: null, + paint: [0.8, 0.3, 0.2, 1], + coverage: "opaque", + face: "single", + lamp: "lit" + } - // Create transform for the cube - transform = retro3d.make_transform() - transform.y = 0.5 + // Create ground plane + ground = lance3d.make_plane(10, 10) + ground_mat = { + color_map: null, + paint: [0.3, 0.5, 0.3, 1], + coverage: "opaque", + face: "single", + lamp: "lit" + } // Set up lighting - retro3d.set_ambient(0.3, 0.3, 0.35) - retro3d.set_light_dir(0.5, 1.0, 0.3, 1.0, 0.95, 0.9, 1.0) - - // Set up a default material - var mat = retro3d.make_material("lit", { - color: [0.8, 0.3, 0.2, 1] + lance3d.set_lighting({ + sun_dir: [0.5, 1.0, 0.3], + sun_color: [1.0, 0.95, 0.9], + ambient: [0.3, 0.3, 0.35] }) - retro3d.set_material(mat) last_time = time_mod.number() @@ -65,77 +71,62 @@ function _update(dt) { } // Handle input for camera orbit - if (retro3d._state.keys_held['a']) { + if (lance3d._state.keys_held['a']) { cam_yaw -= 2.0 * dt auto_rotate = false } - if (retro3d._state.keys_held['d']) { + if (lance3d._state.keys_held['d']) { cam_yaw += 2.0 * dt auto_rotate = false } - if (retro3d._state.keys_held['w']) { + if (lance3d._state.keys_held['w']) { cam_pitch += 2.0 * dt if (cam_pitch > 1.5) cam_pitch = 1.5 } - if (retro3d._state.keys_held['s']) { + if (lance3d._state.keys_held['s']) { cam_pitch -= 2.0 * dt if (cam_pitch < -1.5) cam_pitch = -1.5 } // Toggle auto-rotate - if (retro3d._state.keys_pressed[' ']) { + if (lance3d._state.keys_pressed['space']) { auto_rotate = !auto_rotate } // Exit on escape - if (retro3d._state.keys_held['escape']) { + if (lance3d._state.keys_held['escape']) { $_.stop() } } function _draw() { - // Clear with style-appropriate color - if (style == "ps1") { - retro3d.clear(0.1, 0.05, 0.15, 1.0) - } else if (style == "n64") { - retro3d.clear(0.0, 0.1, 0.2, 1.0) - } else { - retro3d.clear(0.05, 0.05, 0.1, 1.0) - } + // Clear + lance3d.clear(0.1, 0.05, 0.15, 1.0) // Set up camera - retro3d.camera_perspective(60, 0.1, 100) + lance3d.camera_perspective(60, 0.1, 100) // Calculate camera position from orbit var cam_x = Math.sin(cam_yaw) * Math.cos(cam_pitch) * cam_distance var cam_y = Math.sin(cam_pitch) * cam_distance + 0.5 var cam_z = Math.cos(cam_yaw) * Math.cos(cam_pitch) * cam_distance - retro3d.camera_look_at(cam_x, cam_y, cam_z, 0, 0.5, 0) + lance3d.camera_look_at(cam_x, cam_y, cam_z, 0, 0.5, 0) - // Draw the cube - retro3d.draw_model(cube, transform) + // Draw the cube with transform + var cube_transform = lance3d.translation_matrix(0, 0.5, 0) + lance3d.draw_mesh(cube, cube_transform, cube_mat) // Draw ground plane - retro3d.push_state() - var ground_mat = retro3d.make_material("lit", { - color: [0.3, 0.5, 0.3, 1] - }) - retro3d.set_material(ground_mat) - - var ground = retro3d.make_plane(10, 10) - var ground_transform = retro3d.make_transform() - retro3d.draw_model(ground, ground_transform) - - retro3d.pop_state() + lance3d.draw_mesh(ground, null, ground_mat) } function frame() { // Begin frame - retro3d._begin_frame() + lance3d._begin_frame() // Process events - if (!retro3d._process_events()) { + if (!lance3d._process_events()) { log.console("Exiting...") $_.stop() return @@ -152,8 +143,8 @@ function frame() { // Draw _draw() - // End frame (submit GPU commands) - retro3d._end_frame() + // End frame + lance3d._end_frame() // Schedule next frame $_.delay(frame, 1/60) diff --git a/examples/forest.ce b/examples/forest.ce index a656943..25fd457 100644 --- a/examples/forest.ce +++ b/examples/forest.ce @@ -1,117 +1,86 @@ -// Forest example (Diablo-style camera) for retro3d -// Fixed 3/4 camera, WASD movement, player faces the mouse using a ground screen-cast. +// Forest example (Diablo-style camera) for lance3d +// Fixed 3/4 camera, WASD movement // Press ESC to exit. var time_mod = use('time') -var retro3d = use('core') +var lance3d = use('core') -var ground_model = null -var ground_transform = null +// Meshes +var ground_mesh = null +var trunk_mesh = null +var canopy_mesh = null +var player_mesh = null + +// Materials var ground_mat = null - -var trunk_model = null var trunk_mat = null -var canopy_model = null var canopy_mat_a = null var canopy_mat_b = null - -var player_model = null var player_mat = null -var marker_model = null -var marker_transform = null -var marker_mat = null - +// Player state var player = { - transform: null, - speed: 6.0, - yaw: 0 + x: 0, + y: 0.5, + z: 0, + yaw: 0, + speed: 6.0 } +// Trees: array of {x, z, trunk_h, trunk_r, canopy_s, canopy_mat} var trees = [] var num_trees = 80 var cam_offset = { x: 10, y: 12, z: 10 } var last_time = 0 -var last_hit = null - -function _set_camera() { - var px = player.transform.x - var pz = player.transform.z - - retro3d.camera_perspective(55, 0.1, 300) - retro3d.camera_look_at( - px + cam_offset.x, - cam_offset.y, - pz + cam_offset.z, - px, 0, pz - ) -} - -function _set_yaw(t, yaw) { - player.yaw = yaw - var half = yaw / 2 - retro3d.transform_set_rotation_quat(t, 0, Math.sin(half), 0, Math.cos(half)) -} function _init() { - log.console("retro3d Forest (Diablo camera) Example") - log.console("WASD move") - log.console("Face mouse (ground cast)") - log.console("ESC exit") + log.console("lance3d Forest (Diablo camera) Example") + log.console("WASD move, ESC exit") - retro3d.set_style("ps1") - retro3d.seed(1337) + lance3d.set_style("ps1") - retro3d.set_ambient(0.35, 0.35, 0.40) - retro3d.set_light_dir(0.4, 1.0, 0.2, 1.0, 0.95, 0.9, 1.0) - retro3d.set_fog(40, 160, [0.55, 0.70, 0.90]) + lance3d.seed(1337) - ground_model = retro3d.make_plane(220, 220) - ground_transform = retro3d.make_transform() - retro3d.transform_set_position(ground_transform, 0, 0, 0) - ground_mat = retro3d.make_material("lit", { color: [0.12, 0.22, 0.10, 1.0] }) + lance3d.set_lighting({ + sun_dir: [0.4, 1.0, 0.2], + sun_color: [1.0, 0.95, 0.9], + ambient: [0.35, 0.35, 0.40] + }) - trunk_model = retro3d.make_cube(1, 1, 1) - canopy_model = retro3d.make_cube(1, 1, 1) - trunk_mat = retro3d.make_material("lit", { color: [0.35, 0.23, 0.14, 1.0] }) - canopy_mat_a = retro3d.make_material("lit", { color: [0.11, 0.40, 0.18, 1.0] }) - canopy_mat_b = retro3d.make_material("lit", { color: [0.09, 0.33, 0.14, 1.0] }) + lance3d.set_fog({ + enabled: true, + color: [0.55, 0.70, 0.90], + near: 40, + far: 160 + }) - player_model = retro3d.make_cube(1, 1, 1) - player_mat = retro3d.make_material("lit", { color: [0.85, 0.25, 0.20, 1.0] }) - player.transform = retro3d.make_transform() - retro3d.transform_set_position(player.transform, 0, 0.5, 0) - retro3d.transform_set_scale(player.transform, 0.7, 1.0, 0.7) - _set_yaw(player.transform, 0) + // Create meshes + ground_mesh = lance3d.make_plane(220, 220) + trunk_mesh = lance3d.make_cube(1, 1, 1) + canopy_mesh = lance3d.make_cube(1, 1, 1) + player_mesh = lance3d.make_cube(1, 1, 1) - marker_model = retro3d.make_cube(1, 1, 1) - marker_mat = retro3d.make_material("unlit", { color: [1.0, 0.95, 0.2, 1.0] }) - marker_transform = retro3d.make_transform() - retro3d.transform_set_scale(marker_transform, 0.15, 0.15, 0.15) + // Create materials + ground_mat = { paint: [0.12, 0.22, 0.10, 1.0], coverage: "opaque", face: "single", lamp: "lit" } + trunk_mat = { paint: [0.35, 0.23, 0.14, 1.0], coverage: "opaque", face: "single", lamp: "lit" } + canopy_mat_a = { paint: [0.11, 0.40, 0.18, 1.0], coverage: "opaque", face: "single", lamp: "lit" } + canopy_mat_b = { paint: [0.09, 0.33, 0.14, 1.0], coverage: "opaque", face: "single", lamp: "lit" } + player_mat = { paint: [0.85, 0.25, 0.20, 1.0], coverage: "opaque", face: "single", lamp: "lit" } + // Generate trees for (var i = 0; i < num_trees; i++) { - var x, z - x = (retro3d.rand() - 0.5) * 90 - z = (retro3d.rand() - 0.5) * 90 - - var trunk_h = retro3d.rand() * 3 + 2 - var trunk_r = retro3d.rand() * 0.25 + 0.25 - var canopy_s = retro3d.rand() * 1.0 + 1.5 - - var trunk = retro3d.make_transform() - retro3d.transform_set_position(trunk, x, trunk_h / 2, z) - retro3d.transform_set_scale(trunk, trunk_r, trunk_h, trunk_r) - - var canopy = retro3d.make_transform() - retro3d.transform_set_position(canopy, x, trunk_h + canopy_s / 2, z) - retro3d.transform_set_scale(canopy, canopy_s, canopy_s, canopy_s) + var x = (lance3d.rand() - 0.5) * 90 + var z = (lance3d.rand() - 0.5) * 90 trees.push({ - trunk: trunk, - canopy: canopy, - canopy_mat: retro3d.rand() < 0.5 ? canopy_mat_a : canopy_mat_b + x: x, + z: z, + trunk_h: lance3d.rand() * 3 + 2, + trunk_r: lance3d.rand() * 0.25 + 0.25, + canopy_s: lance3d.rand() * 1.0 + 1.5, + canopy_mat: lance3d.rand() < 0.5 ? canopy_mat_a : canopy_mat_b }) } @@ -120,123 +89,103 @@ function _init() { } function _update(dt) { - if (retro3d._state.keys_held['escape']) { + if (lance3d._state.keys_held['escape']) { $_.stop() return } - _set_camera() - - var mx = retro3d._state.mouse_x || 0 - var my = retro3d._state.mouse_y || 0 - var hit = retro3d.screen_cast_ground(mx, my, 0) - if (hit) { - last_hit = hit - retro3d.transform_set_position(marker_transform, hit.x, 0.05, hit.z) - - var px = player.transform.x - var pz = player.transform.z - var dx = hit.x - px - var dz = hit.z - pz - if (dx * dx + dz * dz > 0.00001) { - var yaw = Math.atan2(dx, dz) - _set_yaw(player.transform, yaw) - } - } - + // Movement input var forward = 0 var right = 0 - if (retro3d._state.keys_held['w']) forward += 1 - if (retro3d._state.keys_held['s']) forward -= 1 - if (retro3d._state.keys_held['d']) right -= 1 - if (retro3d._state.keys_held['a']) right += 1 + if (lance3d._state.keys_held['w']) forward += 1 + if (lance3d._state.keys_held['s']) forward -= 1 + if (lance3d._state.keys_held['d']) right -= 1 + if (lance3d._state.keys_held['a']) right += 1 if (forward != 0 || right != 0) { + // Update yaw based on movement direction + player.yaw = Math.atan2(forward, -right) + var fx = Math.sin(player.yaw) var fz = Math.cos(player.yaw) - var rx = Math.cos(player.yaw) - var rz = -Math.sin(player.yaw) - var vx = fx * forward + rx * right - var vz = fz * forward + rz * right - - var len = Math.sqrt(vx * vx + vz * vz) + var len = Math.sqrt(fx * fx + fz * fz) if (len > 0) { - vx /= len - vz /= len + fx /= len + fz /= len } - var px = player.transform.x + vx * player.speed * dt - var pz = player.transform.z + vz * player.speed * dt - retro3d.transform_set_position(player.transform, px, 0.5, pz) + player.x += fx * player.speed * dt + player.z += fz * player.speed * dt } } function _draw() { - retro3d.clear(0.55, 0.70, 0.90, 1.0) + lance3d.clear(0.55, 0.70, 0.90, 1.0) - _set_camera() + // Set up camera following player + lance3d.camera_perspective(55, 0.1, 300) + lance3d.camera_look_at( + player.x + cam_offset.x, + cam_offset.y, + player.z + cam_offset.z, + player.x, 0, player.z + ) - retro3d.push_state() - retro3d.set_material(ground_mat) - retro3d.draw_model(ground_model, ground_transform) - retro3d.pop_state() + // Draw ground + lance3d.draw_mesh(ground_mesh, null, ground_mat) - retro3d.push_state() - retro3d.set_material(trunk_mat) - for (let i = 0; i < trees.length; i++) { - retro3d.draw_model(trunk_model, trees[i].trunk) + // Draw tree trunks + for (var i = 0; i < trees.length; i++) { + var tree = trees[i] + var trunk_transform = lance3d.trs_matrix( + tree.x, tree.trunk_h / 2, tree.z, + 0, 0, 0, 1, + tree.trunk_r, tree.trunk_h, tree.trunk_r + ) + lance3d.draw_mesh(trunk_mesh, trunk_transform, trunk_mat) } - retro3d.pop_state() - retro3d.push_state() - for (let i = 0; i < trees.length; i++) { - retro3d.set_material(trees[i].canopy_mat) - retro3d.draw_model(canopy_model, trees[i].canopy) + // Draw tree canopies + for (var i = 0; i < trees.length; i++) { + var tree = trees[i] + var canopy_transform = lance3d.trs_matrix( + tree.x, tree.trunk_h + tree.canopy_s / 2, tree.z, + 0, 0, 0, 1, + tree.canopy_s, tree.canopy_s, tree.canopy_s + ) + lance3d.draw_mesh(canopy_mesh, canopy_transform, tree.canopy_mat) } - retro3d.pop_state() - retro3d.push_state() - retro3d.set_material(player_mat) - retro3d.draw_model(player_model, player.transform) - retro3d.pop_state() - - if (last_hit) { - retro3d.push_state() - retro3d.set_material(marker_mat) - retro3d.draw_model(marker_model, marker_transform) - retro3d.pop_state() - } + // Draw player + var q = lance3d.euler_to_quat(0, player.yaw, 0) + var player_transform = lance3d.trs_matrix( + player.x, player.y, player.z, + q.x, q.y, q.z, q.w, + 0.7, 1.0, 0.7 + ) + lance3d.draw_mesh(player_mesh, player_transform, player_mat) } function frame() { - // Begin frame - retro3d._begin_frame() + lance3d._begin_frame() - // Process events - if (!retro3d._process_events()) { + if (!lance3d._process_events()) { log.console("Exiting...") $_.stop() return } - // Calculate delta time var now = time_mod.number() var dt = now - last_time last_time = now - // Update _update(dt) - - // Draw _draw() - // End frame (submit GPU commands) - retro3d._end_frame() + lance3d._end_frame() - // Schedule next frame $_.delay(frame, 1/60) } -// Start _init() diff --git a/examples/modelview.ce b/examples/modelview.ce index 3439c30..87b36bd 100644 --- a/examples/modelview.ce +++ b/examples/modelview.ce @@ -1,20 +1,16 @@ -// Model Viewer for retro3d -// Usage: cell run examples/modelview.ce [style] -// style: ps1, n64, or saturn (default: ps1) -// Controls: F1=PS1, F2=N64, F3=Saturn +// Model Viewer for lance3d +// Usage: cell run examples/modelview.ce +// Controls: WASD orbit, Q/E zoom, R/F move target, SPACE toggle animation, 1-9 switch clip +// F1/F2/F3 - Switch style (PS1/N64/Saturn) var io = use('fd') var time_mod = use('time') -var retro3d = use('core') +var lance3d = use('core') + +log.console(lance3d.key) // Parse command line arguments var model_path = args[0] || "Duck.glb" -var style = args[1] || "ps1" - -// Available styles for cycling -var styles = ["ps1", "n64", "saturn"] -var current_style_idx = styles.indexOf(style) -if (current_style_idx < 0) current_style_idx = 0 // Camera orbit state var cam_distance = 5 @@ -24,53 +20,52 @@ var cam_target_y = 0 var orbit_speed = 2.0 var zoom_speed = 0.5 -// Model and transform +// Model var model = null -var transform = null // Animation state -var anim = null -var anim_playing = false +var animations = [] +var current_anim = 0 +var anim_time = 0 +var anim_playing = true +var anim_speed = 1.0 + +// Current style +var style = "ps1" // Timing var last_time = 0 function _init() { - log.console("retro3d Model Viewer") - log.console("Style: " + style) + log.console("lance3d Model Viewer") log.console("Loading: " + model_path) - // Initialize retro3d with selected style - retro3d.set_style(style) + // Initialize lance3d with PS1 style + lance3d.set_style(style) // Load the model - model = retro3d.load_model(model_path) + model = lance3d.load_model(model_path) if (!model) { log.console("Error: Could not load model: " + model_path) $_.stop() return } - log.console("Model loaded with " + text(model.meshes.length) + " mesh(es)") - log.console(" Nodes: " + text(model.nodes.length)) - log.console(" Textures: " + text(model.textures.length)) - log.console(" Animations: " + text(model.animation_count)) - log.console(" Skins: " + text(model.skins ? model.skins.length : 0)) + log.console("Model loaded with " + text(model.length) + " mesh(es)") - // Create transform for the model (this will be an extra parent transform) - transform = retro3d.make_transform() - - // Set up animation if model has animations - if (model.animation_count > 0) { - anim = retro3d.anim_instance(model) - retro3d.anim_play(anim, 0, true) - anim_playing = true - log.console(" Playing animation: " + (retro3d.anim_clip_name(anim, 0) || "clip 0")) + // Get animation info + animations = lance3d.anim_info(model) + log.console(" Animations: " + text(animations.length)) + for (var i = 0; i < animations.length; i++) { + log.console(" " + text(i) + ": " + animations[i].name + " (" + text(animations[i].duration) + "s)") } // 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) + lance3d.set_lighting({ + sun_dir: [0.5, 1.0, 0.3], + sun_color: [1.0, 0.95, 0.9], + ambient: [0.3, 0.3, 0.35] + }) last_time = time_mod.number() @@ -81,174 +76,145 @@ function _init() { log.console(" R/F - Move target up/down") log.console(" SPACE - Toggle animation") log.console(" 1-9 - Switch animation clip") - log.console(" F1 - PS1 style (128x128 tex, 2000 tris)") - log.console(" F2 - N64 style (32x32 tex, 3000 tris)") - log.console(" F3 - Saturn style (64x64 tex, 1500 tris)") log.console(" ESC - Exit") + log.console(" F1 - PS1 style") + log.console(" F2 - N64 style") + log.console(" F3 - Saturn style") // Start the main loop frame() } -function _switch_to_style(idx) { - if (idx == current_style_idx) return - if (idx < 0 || idx >= styles.length) return - - current_style_idx = idx - style = styles[idx] - - if (retro3d.switch_style(style)) { - // Recalculate model textures for new platform - if (model) { - retro3d.recalc_model_textures(model) - } - log.console("Switched to " + style.toUpperCase() + " style") - } -} - function _update(dt) { // Handle input for camera orbit - if (retro3d._state.keys_held['a']) { + if (lance3d.key('a')) { cam_yaw -= orbit_speed * dt } - if (retro3d._state.keys_held['d']) { + if (lance3d.key('d')) { cam_yaw += orbit_speed * dt } - if (retro3d._state.keys_held['w']) { + if (lance3d.key('w')) { cam_pitch += orbit_speed * dt if (cam_pitch > 1.5) cam_pitch = 1.5 } - if (retro3d._state.keys_held['s']) { + if (lance3d.key('s')) { cam_pitch -= orbit_speed * dt if (cam_pitch < -1.5) cam_pitch = -1.5 } // Zoom - if (retro3d._state.keys_held['q']) { + if (lance3d.key('q')) { cam_distance -= zoom_speed * dt * cam_distance if (cam_distance < 0.5) cam_distance = 0.5 } - if (retro3d._state.keys_held['e']) { + if (lance3d.key('e')) { cam_distance += zoom_speed * dt * cam_distance if (cam_distance > 100) cam_distance = 100 } // Move target up/down - if (retro3d._state.keys_held['r']) { + if (lance3d.key('r')) { cam_target_y += zoom_speed * dt } - if (retro3d._state.keys_held['f']) { + if (lance3d.key('f')) { cam_target_y -= zoom_speed * dt } // Exit on escape - if (retro3d._state.keys_held['escape']) { + if (lance3d.key('escape')) { $_.stop() } // Toggle animation with space - if (retro3d._state.keys_pressed['space'] && anim) { - if (anim_playing) { - retro3d.anim_stop(anim) - anim_playing = false - log.console("Animation paused") - } else { - anim.playing = true - anim_playing = true - log.console("Animation resumed") - } + if (lance3d.keyp('space') && animations.length > 0) { + anim_playing = !anim_playing + log.console(anim_playing ? "Animation resumed" : "Animation paused") } // Switch animation clips with number keys - if (anim && model.animation_count > 0) { + if (animations.length > 0) { for (var i = 1; i <= 9; i++) { - if (retro3d._state.keys_pressed[text(i)]) { + if (lance3d.keyp(text(i))) { var clip_idx = i - 1 - if (clip_idx < model.animation_count) { - retro3d.anim_play(anim, clip_idx, true) - anim_playing = true - log.console("Playing clip " + text(clip_idx) + ": " + (retro3d.anim_clip_name(anim, clip_idx) || "unnamed")) + if (clip_idx < animations.length) { + current_anim = clip_idx + anim_time = 0 + log.console("Playing clip " + text(clip_idx) + ": " + animations[clip_idx].name) } } } } // Switch platform style with F1-F3 - if (retro3d._state.keys_pressed['f1']) { - _switch_to_style(0) // PS1 + if (lance3d.keyp('f1')) { + _switch_to_style("ps1") } - if (retro3d._state.keys_pressed['f2']) { - _switch_to_style(1) // N64 + if (lance3d.keyp('f2')) { + _switch_to_style("n64") } - if (retro3d._state.keys_pressed['f3']) { - _switch_to_style(2) // Saturn + if (lance3d.keyp('f3')) { + _switch_to_style("saturn") } - // Update animation - if (anim && anim_playing) { - retro3d.anim_update(anim, dt) - retro3d.anim_apply(anim) + // Update animation time + if (anim_playing && animations.length > 0) { + anim_time += dt * anim_speed + var duration = animations[current_anim].duration + if (duration > 0) { + while (anim_time > duration) { + anim_time -= duration + } + } } } +function _switch_to_style(new_style) { + if (style == new_style) return + style = new_style + lance3d.switch_style(style) + // Recalculate model textures for new style + if (model) { + lance3d.recalc_model_textures(model) + } + log.console("Switched to " + style + " style") +} + function _draw() { - // Clear with a nice gradient-ish color based on style + // Clear with a nice color based on style if (style == "ps1") { - retro3d.clear(0.1, 0.05, 0.15, 1.0) + lance3d.clear(0.1, 0.05, 0.15, 1.0) } else if (style == "n64") { - retro3d.clear(0.0, 0.1, 0.2, 1.0) + lance3d.clear(0.0, 0.1, 0.2, 1.0) } else { - retro3d.clear(0.05, 0.05, 0.1, 1.0) + lance3d.clear(0.05, 0.05, 0.1, 1.0) } // Set up camera - retro3d.camera_perspective(60, 0.1, 100) + lance3d.camera_perspective(60, 0.1, 100) // Calculate camera position from orbit var cam_x = Math.sin(cam_yaw) * Math.cos(cam_pitch) * cam_distance var cam_y = Math.sin(cam_pitch) * cam_distance + cam_target_y var cam_z = Math.cos(cam_yaw) * Math.cos(cam_pitch) * cam_distance - retro3d.camera_look_at(cam_x, cam_y, cam_z, 0, cam_target_y, 0) + lance3d.camera_look_at(cam_x, cam_y, cam_z, 0, cam_target_y, 0) // Draw the model if (model) { - retro3d.draw_model(model, transform) + var pose = null + if (animations.length > 0) { + pose = lance3d.sample_pose(model, current_anim, anim_time) + } + lance3d.draw_model(model, null, pose) } - return - // Draw a ground grid using immediate mode - retro3d.push_state() - var grid_mat = retro3d.make_material("unlit", { - color: [0.3, 0.3, 0.3, 1] - }) - retro3d.set_material(grid_mat) - - retro3d.begin_lines() - retro3d.color(0.3, 1, 0.3, 1) - - var grid_size = 10 - var grid_step = 1 - for (var i = -grid_size; i <= grid_size; i += grid_step) { - // X lines - retro3d.vertex(i, 0, -grid_size) - retro3d.vertex(i, 0, grid_size) - // Z lines - retro3d.vertex(-grid_size, 0, i) - retro3d.vertex(grid_size, 0, i) - } - retro3d.end() - - retro3d.pop_state() - - } function frame() { // Begin frame - retro3d._begin_frame() + lance3d._begin_frame() // Process events - if (!retro3d._process_events()) { + if (!lance3d._process_events()) { log.console("Exiting...") $_.stop() return @@ -265,8 +231,8 @@ function frame() { // Draw _draw() - // End frame (submit GPU commands) - retro3d._end_frame() + // End frame + lance3d._end_frame() // Schedule next frame $_.delay(frame, 1/240) diff --git a/examples/sprite_test.ce b/examples/sprite_test.ce deleted file mode 100644 index c7759ce..0000000 --- a/examples/sprite_test.ce +++ /dev/null @@ -1,117 +0,0 @@ -// Sprite Test for retro3d -// Usage: cell run examples/sprite_test.ce -// sprite_path should be without extension, e.g. "assets/goblin" -// Fills screen with sprites, hue-shifted based on normalized screen position - -var time_mod = use('time') -var retro3d = use('core') - -var sprite_path = args[0] -if (!sprite_path) { - log.console("Usage: cell run examples/sprite_test.ce ") - log.console(" sprite_path: path without extension (e.g. 'assets/goblin')") - $_.stop() -} - -var sprite = null -var last_time = 0 - -function hsv_to_rgb(h, s, v) { - var c = v * s - var x = c * (1 - Math.abs((h / 60) % 2 - 1)) - var m = v - c - - var r = 0, g = 0, b = 0 - if (h < 60) { r = c; g = x; b = 0 } - else if (h < 120) { r = x; g = c; b = 0 } - else if (h < 180) { r = 0; g = c; b = x } - else if (h < 240) { r = 0; g = x; b = c } - else if (h < 300) { r = x; g = 0; b = c } - else { r = c; g = 0; b = x } - - return [r + m, g + m, b + m, 1] -} - -function _init() { - log.console("retro3d Sprite Test") - log.console("Loading sprite: " + sprite_path) - - retro3d.set_style("ps1") - - sprite = retro3d.load_sprite(sprite_path) - if (!sprite) { - log.console("Error: Could not load sprite: " + sprite_path) - $_.stop() - return - } - - log.console("Sprite loaded: " + text(sprite.width) + "x" + text(sprite.height)) - log.console("Press ESC to exit") - - last_time = time_mod.number() - frame() -} - -function _update(dt) { - if (retro3d._state.keys_held['escape']) { - $_.stop() - } -} - -function _draw() { - retro3d.clear(0.1, 0.1, 0.15, 1.0) - - if (!sprite) return - - var w = sprite.width - var h = sprite.height - - // Fill from bottom to top with sprites - // Hue shifts based on normalized screen position - var y = 0 - while (y < 240) { - var x = 0 - while (x < 320) { - // Normalized position (0-1) - var nx = x / 320 - var ny = y / 240 - - // Hue based on position (0-360 degrees) - var hue = (nx + ny) * 180 // Combined X and Y influence - hue = hue % 360 - - var color = hsv_to_rgb(hue, 0.8, 1.0) - - retro3d.draw_sprite(sprite, x, y, { - color: color, - mode: "cutout" - }) - - x = x + w - } - y = y + h - } -} - -function frame() { - retro3d._begin_frame() - - if (!retro3d._process_events()) { - log.console("Exiting...") - $_.stop() - return - } - - var now = time_mod.number() - var dt = now - last_time - last_time = now - - _update(dt) - _draw() - - retro3d._end_frame() - - $_.delay(frame, 1/60) -} - -_init() diff --git a/input.cm b/input.cm new file mode 100644 index 0000000..118141d --- /dev/null +++ b/input.cm @@ -0,0 +1,122 @@ +// Input module for lance3d +var events = use('sdl3/events') +var keyboard = use('sdl3/keyboard') +var mouse = use('sdl3/mouse') + +// Private input state +var _keys_held = {} +var _keys_pressed = {} +var _mouse_x = 0 +var _mouse_y = 0 +var _mouse_buttons = {} +var _mouse_buttons_pressed = {} +var _axes = [0, 0, 0, 0] + +// Key mapping for btn/btnp +var _key_map = ['z', 'x', 'c', 'v', 'a', 's', 'return', 'escape'] + +// Called at start of frame to reset pressed state +function begin_frame() { + _keys_pressed = {} + _mouse_buttons_pressed = {} + + var ms = mouse.get_state() + if (ms) { + _mouse_x = ms.x + _mouse_y = ms.y + } +} + +// Process SDL events, returns false if quit requested +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 && !_keys_held[key_name]) { + _keys_pressed[key_name] = true + } + _keys_held[key_name] = pressed + + // Map WASD to axes + if (key_name == "w") _axes[1] = pressed ? -1 : 0 + if (key_name == "s") _axes[1] = pressed ? 1 : 0 + if (key_name == "a") _axes[0] = pressed ? -1 : 0 + if (key_name == "d") _axes[0] = pressed ? 1 : 0 + } + if (ev.type == "mouse_button_down" || ev.type == "mouse_button_up") { + var btn_pressed = ev.type == "mouse_button_down" + if (btn_pressed && !_mouse_buttons[ev.button]) { + _mouse_buttons_pressed[ev.button] = true + } + _mouse_buttons[ev.button] = btn_pressed + } + } + return true +} + +// Check if a key is held (by name) +function key(name) { + return _keys_held[name.toLowerCase()] || false +} + +// Check if a key was just pressed this frame (by name) +function keyp(name) { + return _keys_pressed[name.toLowerCase()] || false +} + +// Check if a button is held (by index 0-7) +function btn(id, player) { + var key_name = _key_map[id] + return _keys_held[key_name] || false +} + +// Check if a button was just pressed (by index 0-7) +function btnp(id, player) { + var key_name = _key_map[id] + return _keys_pressed[key_name] || false +} + +// Get axis value (-1, 0, or 1) +function axis(id) { + return _axes[id] || 0 +} + +// Get mouse position +function mouse_pos() { + return { x: _mouse_x, y: _mouse_y } +} + +// Check if mouse button is held +function mouse_btn(button) { + return _mouse_buttons[button] || false +} + +// Check if mouse button was just pressed +function mouse_btnp(button) { + return _mouse_buttons_pressed[button] || false +} + +// Get raw access to keys_pressed for internal use +function get_keys_pressed() { + return _keys_pressed +} + +return { + begin_frame: begin_frame, + process_events: process_events, + key: key, + keyp: keyp, + btn: btn, + btnp: btnp, + axis: axis, + mouse_pos: mouse_pos, + mouse_btn: mouse_btn, + mouse_btnp: mouse_btnp, + get_keys_pressed: get_keys_pressed, +} diff --git a/lance3d.md b/lance3d.md index 1332bf3..eae94b2 100644 --- a/lance3d.md +++ b/lance3d.md @@ -1,4 +1,8 @@ ## Core API +Set style ps1 / n64 / saturn + +set_style("ps1" | "n64" | "saturn") + Get time since game boot, in seconds time() @@ -38,6 +42,9 @@ make_sphere(r, segments=12) -> mesh make_cylinder(r, h, segments=12) -> mesh make_plane(w, h) -> mesh +Gets information about the animations on a model +anim_info(model) + Generates a pose that can be applied to a model sample_pose(model, name, time) @@ -74,15 +81,12 @@ camera_perspective(fov_deg=60, near=0.1, far=1000) camera_ortho(left, right, bottom, top, near=-1, far=1) ## Collision API -retro3d.add_collider_sphere(transform, radius, opts={layer_mask:1, user:null}) -> collider -retro3d.add_collider_box(transform, sx, sy, sz, opts={layer_mask:1, user:null}) -> collider +add_collider_sphere(transform, radius, opts={layer_mask:1, user:null}) -> collider +add_collider_box(transform, sx, sy, sz, opts={layer_mask:1, user:null}) -> collider -retro3d.set_collider_transform(collider, transform) -retro3d.set_collider_layer(collider, layer_mask) +overlaps(layer_mask_a=null, layer_mask_b=null) -> array<{a: collider, b: collider}> -retro3d.overlaps(layer_mask_a=null, layer_mask_b=null) -> array<{a: collider, b: collider}> - -retro3d.raycast(ox,oy,oz, dx,dy,dz, opts={ +raycast(ox,oy,oz, dx,dy,dz, opts={ max_dist: Infinity, layer_mask: 0xFFFFFFFF }) -> null | {x,y,z, nx,ny,nz, distance, collider} @@ -102,9 +106,11 @@ irand(min_inclusive, max_inclusive) -> int vertex {x, y, z, u=0, v=0, color} point(vertex, size=1.0) line(vertex_a, vertex_b, width=1.0) +grid(size, step, norm = {x:0,y:1,z:0}, color) + triangle(vertex_a, vertex_b, vertex_c, mat=null) ray(ox,oy,oz, dx,dy,dz, len, opts) -grid(size, step, norm = {x:0,y:1,z:0}, color) + aabb(cx,cy,cz, ex,ey,ez, opts) // center+extents obb(transform, sx,sy,sz, opts) // oriented via transform frustum(camera_state_or_params, opts) diff --git a/math.cm b/math.cm new file mode 100644 index 0000000..8b4221d --- /dev/null +++ b/math.cm @@ -0,0 +1,133 @@ +// Math module for lance3d +var model_c = use('model') + +// Private RNG state +var _rng_seed = 12345 + +function seed(seed_int) { + _rng_seed = seed_int +} + +function rand() { + // Simple LCG for deterministic random + _rng_seed = (_rng_seed * 1103515245 + 12345) & 0x7fffffff + return _rng_seed / 0x7fffffff +} + +function irand(min_inclusive, max_inclusive) { + return Math.floor(rand() * (max_inclusive - min_inclusive + 1)) + min_inclusive +} + +// Matrix helpers +function identity_matrix() { + return model_c.mat4_identity() +} + +function translation_matrix(x, y, z) { + return model_c.mat4_from_trs(x, y, z, 0, 0, 0, 1, 1, 1, 1) +} + +function rotation_matrix(qx, qy, qz, qw) { + return model_c.mat4_from_trs(0, 0, 0, qx, qy, qz, qw, 1, 1, 1) +} + +function scale_matrix(sx, sy, sz) { + return model_c.mat4_from_trs(0, 0, 0, 0, 0, 0, 1, sx, sy, sz) +} + +function trs_matrix(x, y, z, qx, qy, qz, qw, sx, sy, sz) { + return model_c.mat4_from_trs(x, y, z, qx, qy, qz, qw, sx, sy, sz) +} + +function euler_to_quat(pitch, yaw, roll) { + var cy = Math.cos(yaw * 0.5) + var sy = Math.sin(yaw * 0.5) + var cp = Math.cos(pitch * 0.5) + var sp = Math.sin(pitch * 0.5) + var cr = Math.cos(roll * 0.5) + var sr = Math.sin(roll * 0.5) + + return { + x: sr * cp * cy - cr * sp * sy, + y: cr * sp * cy + sr * cp * sy, + z: cr * cp * sy - sr * sp * cy, + w: cr * cp * cy + sr * sp * sy + } +} + +function euler_matrix(pitch, yaw, roll) { + var q = euler_to_quat(pitch, yaw, roll) + return model_c.mat4_from_trs(0, 0, 0, q.x, q.y, q.z, q.w, 1, 1, 1) +} + +function multiply_matrices(a, b) { + return model_c.mat4_mul(a, b) +} + +// Vector helpers +function vec3_add(a, b) { + return {x: a.x + b.x, y: a.y + b.y, z: a.z + b.z} +} + +function vec3_sub(a, b) { + return {x: a.x - b.x, y: a.y - b.y, z: a.z - b.z} +} + +function vec3_scale(v, s) { + return {x: v.x * s, y: v.y * s, z: v.z * s} +} + +function vec3_dot(a, b) { + return a.x * b.x + a.y * b.y + a.z * b.z +} + +function vec3_cross(a, b) { + return { + x: a.y * b.z - a.z * b.y, + y: a.z * b.x - a.x * b.z, + z: a.x * b.y - a.y * b.x + } +} + +function vec3_length(v) { + return Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z) +} + +function vec3_normalize(v) { + var len = vec3_length(v) + if (len < 0.0001) return {x: 0, y: 0, z: 0} + return {x: v.x / len, y: v.y / len, z: v.z / len} +} + +function lerp(a, b, t) { + return a + (b - a) * t +} + +function clamp(v, min, max) { + if (v < min) return min + if (v > max) return max + return v +} + +return { + seed: seed, + rand: rand, + irand: irand, + identity_matrix: identity_matrix, + translation_matrix: translation_matrix, + rotation_matrix: rotation_matrix, + scale_matrix: scale_matrix, + trs_matrix: trs_matrix, + euler_to_quat: euler_to_quat, + euler_matrix: euler_matrix, + multiply_matrices: multiply_matrices, + vec3_add: vec3_add, + vec3_sub: vec3_sub, + vec3_scale: vec3_scale, + vec3_dot: vec3_dot, + vec3_cross: vec3_cross, + vec3_length: vec3_length, + vec3_normalize: vec3_normalize, + lerp: lerp, + clamp: clamp +} diff --git a/resources.cm b/resources.cm new file mode 100644 index 0000000..12a2824 --- /dev/null +++ b/resources.cm @@ -0,0 +1,552 @@ +// Resources module for lance3d - sprite and model loading +var io = use('fd') +var gltf = use('mload/gltf') +var model_c = use('model') +var png = use('cell-image/png') +var resize_mod = use('cell-image/resize') +var anim_mod = use('animation') +var skin_mod = use('skin') + +// Backend reference (set by core) +var _backend = null + +// Texture original data storage for re-resizing on style change +var TEX_ORIGINAL = Symbol("texture_original") + +// Default material prototype +var _default_material = { + color_map: null, + paint: [1, 1, 1, 1], + coverage: "opaque", + face: "single", + lamp: "lit" +} + +function set_backend(backend) { + _backend = backend +} + +// Get texture size for platform and tier +function get_tex_size(style, tier) { + if (!style) return 64 + var sizes = style.tex_sizes + if (tier == "hero" && sizes.hero) return sizes.hero + if (tier == "low" && sizes.low) return sizes.low + return sizes.normal || 64 +} + +// Resize an image to platform-appropriate size +function resize_image_for_platform(img, style, tier) { + var target_size = get_tex_size(style, tier) + var src_w = img.width + var src_h = img.height + + // If already at or below target size, return as-is + if (src_w <= target_size && src_h <= target_size) { + return img + } + + // Resize to fit within target_size x target_size (square) + var scale = target_size / Math.max(src_w, src_h) + var dst_w = Math.floor(src_w * scale) + var dst_h = Math.floor(src_h * scale) + if (dst_w < 1) dst_w = 1 + if (dst_h < 1) dst_h = 1 + + // Use nearest filter for retro look + return resize_mod.resize(img, dst_w, dst_h, { filter: "nearest" }) +} + +// Create a texture with platform-appropriate sizing, storing original for re-resize +function create_texture_for_platform(w, h, pixels, style, tier) { + var original = { width: w, height: h, pixels: pixels } + var img = resize_image_for_platform(original, style, tier) + var tex = _backend.create_texture(img.width, img.height, img.pixels) + + // Tag texture with current style and tier for cache invalidation + tex._style_name = style ? style.name : null + tex._tier = tier || "normal" + tex[TEX_ORIGINAL] = original + + return tex +} + +// Get or create resized texture for current platform +function get_platform_texture(tex, style, tier) { + if (!tex) return _backend.get_white_texture() + + // Check if texture needs re-resizing (style changed) + var style_name = style ? style.name : null + if (tex._style_name != style_name || tex._tier != tier) { + var original = tex[TEX_ORIGINAL] + if (original) { + var img = resize_image_for_platform(original, style, tier) + var new_tex = _backend.create_texture(img.width, img.height, img.pixels) + new_tex._style_name = style_name + new_tex._tier = tier + new_tex[TEX_ORIGINAL] = original + return new_tex + } + } + + return tex +} + +function load_texture(path, style, tier) { + var data = io.slurp(path) + if (!data) return null + + var img = png.decode(data) + if (!img) return null + + if (style) { + return create_texture_for_platform(img.width, img.height, img.pixels, style, tier) + } else { + return _backend.create_texture(img.width, img.height, img.pixels) + } +} + +function load_model(path, style, tier) { + var ext = path.slice(path.lastIndexOf('.') + 1).toLowerCase() + + if (ext != "gltf" && ext != "glb") { + log.console("resources: unsupported model format: " + ext) + return null + } + + var g = gltf.load(path, {pull_images: true, decode_images: true, mode: "used"}) + if (!g) return null + + var buffer_blob = g.buffers[0] ? g.buffers[0].blob : null + if (!buffer_blob) { + log.console("resources: gltf has no buffer data") + return null + } + + var result = [] + var textures = [] + var materials = [] + var original_images = [] + + // Load textures with platform-appropriate sizing + for (var ti = 0; ti < g.images.length; ti++) { + var img = g.images[ti] + var tex = null + if (img && img.pixels) { + original_images.push({ + width: img.pixels.width, + height: img.pixels.height, + pixels: img.pixels.pixels + }) + if (style) { + tex = create_texture_for_platform(img.pixels.width, img.pixels.height, img.pixels.pixels, style, tier) + } else { + tex = _backend.create_texture(img.pixels.width, img.pixels.height, img.pixels.pixels) + } + } else { + original_images.push(null) + } + textures.push(tex) + } + + // Create materials + var gltf_mats = g.materials || [] + for (var mi = 0; mi < gltf_mats.length; mi++) { + var gmat = gltf_mats[mi] + + var paint = [1, 1, 1, 1] + if (gmat.pbr && gmat.pbr.base_color_factor) { + paint = gmat.pbr.base_color_factor.slice() + } + + var color_map = null + if (gmat.pbr && gmat.pbr.base_color_texture) { + var tex_info = gmat.pbr.base_color_texture + var tex_obj = g.textures[tex_info.texture] + if (tex_obj && tex_obj.image != null && textures[tex_obj.image]) { + color_map = textures[tex_obj.image] + } + } + + var coverage = "opaque" + if (gmat.alpha_mode == "MASK") coverage = "cutoff" + else if (gmat.alpha_mode == "BLEND") coverage = "blend" + + materials.push({ + color_map: color_map, + paint: paint, + coverage: coverage, + face: gmat.double_sided ? "double" : "single", + lamp: gmat.unlit ? "unlit" : "lit" + }) + } + + // Build internal model structure for animations/skins + var internal_model = { + meshes: [], + nodes: [], + root_nodes: [], + textures: textures, + materials: materials, + animations: [], + skins: [], + _gltf: g, + _tex_tier: tier || "normal", + _original_images: original_images + } + + // Build node transforms + for (var ni = 0; ni < g.nodes.length; ni++) { + var node = g.nodes[ni] + var t = _make_internal_transform(node) + t.mesh_index = node.mesh + t.name = node.name + t.gltf_children = node.children || [] + internal_model.nodes.push(t) + } + + // Set up parent-child relationships + for (var ni = 0; ni < internal_model.nodes.length; ni++) { + var t = internal_model.nodes[ni] + for (var ci = 0; ci < t.gltf_children.length; ci++) { + var child_idx = t.gltf_children[ci] + if (child_idx < internal_model.nodes.length) { + _transform_set_parent(internal_model.nodes[child_idx], t) + } + } + } + + // Find root nodes + for (var ni = 0; ni < internal_model.nodes.length; ni++) { + if (!internal_model.nodes[ni].parent) { + internal_model.root_nodes.push(internal_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, textures) + if (gpu_mesh) { + gpu_mesh.name = mesh.name + gpu_mesh.mesh_index = mi + gpu_mesh.primitive_index = pi + internal_model.meshes.push(gpu_mesh) + } + } + } + + // Prepare animations and skins + internal_model.animations = anim_mod.prepare_animations(internal_model) + internal_model.skins = skin_mod.prepare_skins(internal_model) + + // Build result array: [{mesh, material, node_index}] + for (var ni = 0; ni < internal_model.nodes.length; ni++) { + var node = internal_model.nodes[ni] + if (node.mesh_index == null) continue + + for (var mi = 0; mi < internal_model.meshes.length; mi++) { + var mesh = internal_model.meshes[mi] + if (mesh.mesh_index != node.mesh_index) continue + + var mat_idx = mesh.material_index + var mat = (mat_idx != null && materials[mat_idx]) ? materials[mat_idx] : Object.create(_default_material) + + result.push({ + mesh: mesh, + material: mat, + _node_index: ni + }) + } + } + + // Attach internal model for animation support + result._internal = internal_model + + return result +} + +// Recalculate model textures for a new style +function recalc_model_textures(model, style, tier) { + if (!model || !model._internal) return + var internal = model._internal + + for (var ti = 0; ti < internal._original_images.length; ti++) { + var original = internal._original_images[ti] + if (!original) continue + + var new_tex = create_texture_for_platform(original.width, original.height, original.pixels, style, tier) + internal.textures[ti] = new_tex + } + + // Update material references + var g = internal._gltf + var gltf_mats = g.materials || [] + for (var mi = 0; mi < gltf_mats.length; mi++) { + var gmat = gltf_mats[mi] + if (gmat.pbr && gmat.pbr.base_color_texture) { + var tex_info = gmat.pbr.base_color_texture + var tex_obj = g.textures[tex_info.texture] + if (tex_obj && tex_obj.image != null && internal.textures[tex_obj.image]) { + internal.materials[mi].color_map = internal.textures[tex_obj.image] + } + } + } + + // Update mesh textures + for (var mi = 0; mi < internal.meshes.length; mi++) { + var mesh = internal.meshes[mi] + var mat_idx = mesh.material_index + if (mat_idx != null && internal.materials[mat_idx]) { + mesh.texture = internal.materials[mat_idx].color_map + } + } + + // Update result array materials + for (var i = 0; i < model.length; i++) { + var entry = model[i] + var mat_idx = entry.mesh.material_index + if (mat_idx != null && internal.materials[mat_idx]) { + entry.material = internal.materials[mat_idx] + } + } +} + +// Internal transform helpers +function _make_internal_transform(node) { + var t = { + parent: null, + children: [], + x: 0, y: 0, z: 0, + qx: 0, qy: 0, qz: 0, qw: 1, + sx: 1, sy: 1, sz: 1, + local_mat: null, + world_mat: null, + has_local_mat: false, + dirty_local: true, + dirty_world: true + } + + if (node.matrix) { + t.local_mat = model_c.mat4_from_array(node.matrix) + t.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.x = trans[0]; t.y = trans[1]; t.z = trans[2] + t.qx = rot[0]; t.qy = rot[1]; t.qz = rot[2]; t.qw = rot[3] + t.sx = scale[0]; t.sy = scale[1]; t.sz = scale[2] + } + + return t +} + +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) + child.dirty_world = true +} + +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 + } + + 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 _process_gltf_primitive(g, buffer_blob, prim, textures) { + var attrs = prim.attributes + if (attrs.POSITION == null) return null + + 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 color_acc = attrs.COLOR_0 != null ? g.accessors[attrs.COLOR_0] : null + var joints_acc = attrs.JOINTS_0 != null ? g.accessors[attrs.JOINTS_0] : null + var weights_acc = attrs.WEIGHTS_0 != null ? g.accessors[attrs.WEIGHTS_0] : null + var idx_acc = prim.indices != null ? g.accessors[prim.indices] : null + + var vertex_count = pos_acc.count + + 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 + ) + + 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 + ) + } + + 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 + ) + } + + var colors = null + if (color_acc) { + var color_view = g.views[color_acc.view] + colors = model_c.extract_accessor( + buffer_blob, + color_view.byte_offset || 0, + color_view.byte_stride || 0, + color_acc.byte_offset || 0, + color_acc.count, + color_acc.component_type, + color_acc.type + ) + } + + var joints = null + if (joints_acc) { + var joints_view = g.views[joints_acc.view] + joints = model_c.extract_accessor( + buffer_blob, + joints_view.byte_offset || 0, + joints_view.byte_stride || 0, + joints_acc.byte_offset || 0, + joints_acc.count, + joints_acc.component_type, + joints_acc.type + ) + } + + var weights = null + if (weights_acc) { + var weights_view = g.views[weights_acc.view] + weights = model_c.extract_accessor( + buffer_blob, + weights_view.byte_offset || 0, + weights_view.byte_stride || 0, + weights_acc.byte_offset || 0, + weights_acc.count, + weights_acc.component_type, + weights_acc.type + ) + } + + 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" + } + + var mesh_data = { + vertex_count: vertex_count, + positions: positions, + normals: normals, + uvs: uvs, + colors: colors, + joints: joints, + weights: weights + } + var packed = model_c.pack_vertices(mesh_data) + + var vertex_buffer = _backend.create_vertex_buffer(packed.data) + var index_buffer = indices ? _backend.create_index_buffer(indices) : null + + 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, + skinned: packed.skinned || false, + stride: packed.stride + } +} + +// Export transform helpers for use by other modules +function get_transform_world_matrix(t) { + return _transform_get_world_matrix(t) +} + +return { + set_backend: set_backend, + load_texture: load_texture, + load_model: load_model, + recalc_model_textures: recalc_model_textures, + create_texture_for_platform: create_texture_for_platform, + get_platform_texture: get_platform_texture, + get_transform_world_matrix: get_transform_world_matrix, + default_material: _default_material +} diff --git a/sdl.cm b/sdl.cm new file mode 100644 index 0000000..6dfb09e --- /dev/null +++ b/sdl.cm @@ -0,0 +1,376 @@ +// SDL3 GPU backend for lance3d +var video = use('sdl3/video') +var gpu_mod = use('sdl3/gpu') +var blob_mod = use('blob') +var io = use('fd') + +// Private state +var _gpu = null +var _window = null +var _swapchain_format = null +var _depth_texture = null +var _white_texture = null +var _vert_shader = null +var _frag_shader = null +var _skinned_vert_shader = null +var _sampler_nearest = null +var _sampler_linear = null +var _pipelines = {} +var _resolution_w = 640 +var _resolution_h = 480 + +// Initialize the GPU backend +function init(opts) { + opts = opts || {} + _resolution_w = opts.width || 640 + _resolution_h = opts.height || 480 + + _window = new video.window({ + title: opts.title || "lance3d", + width: _resolution_w, + height: _resolution_h + }) + + _gpu = new gpu_mod.gpu({ debug: true, shaders_msl: true }) + _gpu.claim_window(_window) + + 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("sdl backend: failed to load shaders") + return false + } + + _vert_shader = new gpu_mod.shader(_gpu, { + code: vert_code, + stage: "vertex", + format: "msl", + entrypoint: "vertex_main", + num_uniform_buffers: 2 + }) + + _frag_shader = new gpu_mod.shader(_gpu, { + code: frag_code, + stage: "fragment", + format: "msl", + entrypoint: "fragment_main", + num_uniform_buffers: 2, + num_samplers: 1 + }) + + _swapchain_format = _gpu.swapchain_format(_window) + + var skinned_vert_code = io.slurp("shaders/retro3d_skinned.vert.msl") + if (skinned_vert_code) { + _skinned_vert_shader = new gpu_mod.shader(_gpu, { + code: skinned_vert_code, + stage: "vertex", + format: "msl", + entrypoint: "vertex_main", + num_uniform_buffers: 3 + }) + } + + // Pre-create common pipelines + get_pipeline(false, "opaque", "back") + get_pipeline(true, "opaque", "back") + + _sampler_nearest = new gpu_mod.sampler(_gpu, { + min_filter: "nearest", + mag_filter: "nearest", + u: "repeat", + v: "repeat" + }) + + _sampler_linear = new gpu_mod.sampler(_gpu, { + min_filter: "linear", + mag_filter: "linear", + u: "repeat", + v: "repeat" + }) + + _depth_texture = new gpu_mod.texture(_gpu, { + width: _resolution_w, + height: _resolution_h, + format: "d32 float s8", + type: "2d", + layers: 1, + mip_levels: 1, + depth_target: true + }) + + var white_pixels = new blob_mod(32, true) + _white_texture = create_texture(1, 1, stone(white_pixels)) + + return true +} + +function get_window() { + return _window +} + +function get_gpu() { + return _gpu +} + +function get_resolution() { + return { width: _resolution_w, height: _resolution_h } +} + +function get_white_texture() { + return _white_texture +} + +function get_depth_texture() { + return _depth_texture +} + +// Get sampler based on style (0=ps1/saturn nearest, 1=n64 linear) +function get_sampler(style_id) { + return style_id == 1 ? _sampler_linear : _sampler_nearest +} + +function get_pipeline(skinned, alpha_mode, cull) { + var key = `${skinned}_${alpha_mode}_${cull}` + if (_pipelines[key]) return _pipelines[key] + + var blend_enabled = alpha_mode == "blend" + var depth_write = alpha_mode != "blend" + + var blend_config = { enabled: false } + if (blend_enabled) { + blend_config = { + enabled: true, + src_rgb: "src_alpha", + dst_rgb: "one_minus_src_alpha", + op_rgb: "add", + src_alpha: "one", + dst_alpha: "one_minus_src_alpha", + op_alpha: "add" + } + } + + var cull_mode = cull == "none" ? "none" : (cull == "front" ? "front" : "back") + + var vert_shader = skinned ? _skinned_vert_shader : _vert_shader + if (!vert_shader) return null + + var pitch = skinned ? 80 : 48 + var vertex_attrs = [ + { 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 } + ] + if (skinned) { + vertex_attrs.push({ location: 4, buffer_slot: 0, format: "float4", offset: 48 }) + vertex_attrs.push({ location: 5, buffer_slot: 0, format: "float4", offset: 64 }) + } + + var pipeline = new gpu_mod.graphics_pipeline(_gpu, { + vertex: vert_shader, + fragment: _frag_shader, + primitive: "triangle", + cull: cull_mode, + face: "counter_clockwise", + fill: "fill", + vertex_buffer_descriptions: [{ + slot: 0, + pitch: pitch, + input_rate: "vertex" + }], + vertex_attributes: vertex_attrs, + target: { + color_targets: [{ format: _swapchain_format, blend: blend_config }], + depth: "d32 float s8" + }, + depth: { + test: true, + write: depth_write, + compare: "less" + } + }) + + _pipelines[key] = pipeline + return pipeline +} + +function create_texture(w, h, pixels) { + var tex = new gpu_mod.texture(_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(_gpu, { + size: size, + usage: "upload" + }) + + transfer.copy_blob(_gpu, pixels) + + var cmd = _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 create_vertex_buffer(data) { + var size = data.length / 8 + var buffer = new gpu_mod.buffer(_gpu, { + size: size, + vertex: true + }) + + var transfer = new gpu_mod.transfer_buffer(_gpu, { + size: size, + usage: "upload" + }) + + transfer.copy_blob(_gpu, data) + + var cmd = _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(_gpu, { + size: size, + index: true + }) + + var transfer = new gpu_mod.transfer_buffer(_gpu, { + size: size, + usage: "upload" + }) + + transfer.copy_blob(_gpu, data) + + var cmd = _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 +} + +// Submit a frame of draws +// draws: array of { mesh, uniforms, texture, coverage, face, palette } +// clear_color: [r, g, b, a] +// clear_depth: boolean +// style_id: 0=ps1, 1=n64, 2=saturn (for sampler selection) +function submit_frame(draws, clear_color, clear_depth, style_id) { + if (!_gpu) return { draw_calls: 0, triangles: 0 } + + var cmd = _gpu.acquire_cmd_buffer() + + var pass_desc = { + color_targets: [{ + texture: null, + load: "clear", + store: "store", + clear_color: { + r: clear_color[0], + g: clear_color[1], + b: clear_color[2], + a: clear_color[3] + } + }] + } + + if (_depth_texture) { + pass_desc.depth_stencil = { + texture: _depth_texture, + load: 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(_window, pass_desc) + + // Sort draws: opaque first, then cutoff, then blend + draws.sort(function(a, b) { + var order = { opaque: 0, cutoff: 1, mask: 1, blend: 2 } + return (order[a.coverage] || 0) - (order[b.coverage] || 0) + }) + + var draw_calls = 0 + var triangles = 0 + var sampler = get_sampler(style_id) + + for (var i = 0; i < draws.length; i++) { + var d = draws[i] + + var skinned = d.mesh.skinned && d.palette + var cull = d.face == "double" ? "none" : "back" + var alpha_mode = d.coverage == "blend" ? "blend" : (d.coverage == "cutoff" || d.coverage == "mask" ? "mask" : "opaque") + var pipeline = get_pipeline(skinned, alpha_mode, cull) + + if (!pipeline) continue + + swap_pass.bind_pipeline(pipeline) + 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(1, d.uniforms) + cmd.push_fragment_uniform_data(1, d.uniforms) + + if (skinned && d.palette) { + cmd.push_vertex_uniform_data(2, d.palette) + } + + swap_pass.bind_fragment_samplers(0, [{ texture: d.texture, sampler: sampler }]) + + swap_pass.draw_indexed(d.mesh.index_count, 1, 0, 0, 0) + + draw_calls++ + triangles += d.mesh.index_count / 3 + } + + swap_pass.end() + cmd.submit() + + return { draw_calls: draw_calls, triangles: triangles } +} + +return { + init: init, + create_texture: create_texture, + create_vertex_buffer: create_vertex_buffer, + create_index_buffer: create_index_buffer, + submit_frame: submit_frame +}