Files
retro3d/core.cm
2025-12-13 00:46:00 -06:00

1606 lines
41 KiB
Plaintext

// retro3d fantasy game console
var io = use('fd')
var time_mod = use('time')
var blob_mod = use('blob')
var video = use('sdl3/video')
var gpu_mod = use('sdl3/gpu')
var events = use('sdl3/events')
var keyboard = use('sdl3/keyboard')
var gltf = use('mload/gltf')
var obj_loader = use('mload/obj')
var model_c = use('model')
var png = use('cell-image/png')
// Internal state
var _state = {
style: null,
style_id: 0, // 0=ps1, 1=n64, 2=saturn
resolution_w: 320,
resolution_h: 240,
boot_time: 0,
dt: 1/60,
frame_count: 0,
draw_calls: 0,
triangles: 0,
// GPU resources
window: null,
gpu: null,
pipeline_lit: null,
pipeline_unlit: null,
sampler_nearest: null,
sampler_linear: null,
depth_texture: null,
white_texture: null,
// Camera state
camera: {
x: 0, y: 0, z: 5,
target_x: 0, target_y: 0, target_z: 0,
up_x: 0, up_y: 1, up_z: 0,
projection: "perspective",
fov: 60, near: 0.1, far: 1000,
left: -1, right: 1, bottom: -1, top: 1
},
// Lighting state
ambient: [0.2, 0.2, 0.2],
light_dir: [0.5, 1.0, 0.3],
light_color: [1, 1, 1],
light_intensity: 1.0,
// Fog state
fog_near: 10,
fog_far: 100,
fog_color: null,
// Current material
current_material: null,
// State stack
state_stack: [],
// Immediate mode
im_mode: null,
im_vertices: [],
im_color: [1, 1, 1, 1],
// Input state
keys_held: {},
keys_pressed: {},
axes: [0, 0, 0, 0],
// RNG state
rng_seed: 12345
}
// Style configurations
var _styles = {
ps1: {
id: 0,
resolution: [320, 240],
vertex_snap: true,
affine_texturing: true,
filtering: "nearest",
color_depth: 15,
dither: false
},
n64: {
id: 1,
resolution: [320, 240],
vertex_snap: false,
affine_texturing: false,
filtering: "linear",
color_depth: 16,
dither: false
},
saturn: {
id: 2,
resolution: [320, 224],
vertex_snap: false,
affine_texturing: true,
filtering: "nearest",
color_depth: 15,
dither: true
}
}
// ============================================================================
// 1) System / Style / Time / Logging
// ============================================================================
function set_style(style_name) {
if (_state.style != null) return // Already set
def style = _styles[style_name]
if (!style) {
log.console("retro3d: unknown style: " + style_name)
return
}
_state.style = style_name
_state.style_id = style.id
_state.resolution_w = style.resolution[0]
_state.resolution_h = style.resolution[1]
_state.boot_time = time_mod.number()
_init_gpu()
}
function get_style() {
return _state.style
}
function set_resolution(w, h) {
_state.resolution_w = w
_state.resolution_h = h
}
function time() {
return time_mod.number() - _state.boot_time
}
function dt() {
return _state.dt
}
function stat(name) {
if (name == "draw_calls") return _state.draw_calls
if (name == "triangles") return _state.triangles
if (name == "fps") return 1 / _state.dt
if (name == "memory_bytes") return 0
return 0
}
function log_msg() {
var args = []
for (var i = 0; i < arguments.length; i++) {
args.push(text(arguments[i]))
}
log.console("[retro3d] " + args.join(" "))
}
// ============================================================================
// 2) Assets - Models, Textures, Audio
// ============================================================================
function load_model(path) {
var data = io.slurp(path)
if (!data) return null
var ext = path.slice(path.lastIndexOf('.') + 1).toLowerCase()
if (ext == "obj") {
var parsed = obj_loader.decode(data)
if (!parsed) return null
return _load_obj_model(parsed)
}
if (ext != "gltf" && ext != "glb") {
log.console("retro3d: unsupported model format: " + ext)
return null
}
// Parse gltf
var g = gltf.decode(data)
if (!g) return null
// Get the main buffer blob
var buffer_blob = g.buffers[0] ? g.buffers[0].blob : null
if (!buffer_blob) {
log.console("retro3d: gltf has no buffer data")
return null
}
// Build model structure
var model = {
meshes: [],
nodes: [],
root_nodes: [],
textures: [],
materials: g.materials || [],
animations: g.animations || [],
animation_count: g.animations ? g.animations.length : 0,
_gltf: g
}
// Load textures from embedded images
for (var ti = 0; ti < g.images.length; ti++) {
var img = g.images[ti]
var tex = null
if (img.kind == "buffer_view" && img.view != null) {
var view = g.views[img.view]
var img_data = _extract_buffer_view(buffer_blob, view)
if (img.mime == "image/png") {
var decoded = png.decode(img_data)
if (decoded) {
tex = _create_texture(decoded.width, decoded.height, decoded.pixels)
}
}
}
model.textures.push(tex)
}
// Build node transforms (preserving hierarchy)
for (var ni = 0; ni < g.nodes.length; ni++) {
var node = g.nodes[ni]
var t = null
if (node.matrix) {
t = make_transform({
local_mat: model_c.mat4_from_array(node.matrix),
has_local_mat: true
})
} else {
var trans = node.translation || [0, 0, 0]
var rot = node.rotation || [0, 0, 0, 1]
var scale = node.scale || [1, 1, 1]
t = make_transform({
x: trans[0], y: trans[1], z: trans[2],
qx: rot[0], qy: rot[1], qz: rot[2], qw: rot[3],
sx: scale[0], sy: scale[1], sz: scale[2]
})
}
t.mesh_index = node.mesh
t.name = node.name
t.gltf_children = node.children || []
model.nodes.push(t)
}
// Set up parent-child relationships
for (var ni = 0; ni < model.nodes.length; ni++) {
var t = model.nodes[ni]
for (var ci = 0; ci < t.gltf_children.length; ci++) {
var child_idx = t.gltf_children[ci]
if (child_idx < model.nodes.length) {
transform_set_parent(model.nodes[child_idx], t)
}
}
}
// Find root nodes (those without parents)
for (var ni = 0; ni < model.nodes.length; ni++) {
if (!model.nodes[ni].parent) {
model.root_nodes.push(model.nodes[ni])
}
}
// Process meshes
for (var mi = 0; mi < g.meshes.length; mi++) {
var mesh = g.meshes[mi]
for (var pi = 0; pi < mesh.primitives.length; pi++) {
var prim = mesh.primitives[pi]
var gpu_mesh = _process_gltf_primitive(g, buffer_blob, prim, model.textures)
if (gpu_mesh) {
gpu_mesh.name = mesh.name
gpu_mesh.mesh_index = mi
gpu_mesh.primitive_index = pi
model.meshes.push(gpu_mesh)
}
}
}
return model
}
function _extract_buffer_view(buffer_blob, view) {
// Extract a portion of the buffer as a new blob
var offset = view.byte_offset || 0
var length = view.byte_length
var newblob = buffer_blob.read_blob(offset*8, (offset + length)*8)
return stone(newblob)
}
function _process_gltf_primitive(g, buffer_blob, prim, textures) {
var attrs = prim.attributes
if (!attrs.POSITION) return null
// Get accessors
var pos_acc = g.accessors[attrs.POSITION]
var norm_acc = attrs.NORMAL != null ? g.accessors[attrs.NORMAL] : null
var uv_acc = attrs.TEXCOORD_0 != null ? g.accessors[attrs.TEXCOORD_0] : null
var idx_acc = prim.indices != null ? g.accessors[prim.indices] : null
var vertex_count = pos_acc.count
// Extract position data
var pos_view = g.views[pos_acc.view]
var positions = model_c.extract_accessor(
buffer_blob,
pos_view.byte_offset || 0,
pos_view.byte_stride || 0,
pos_acc.byte_offset || 0,
pos_acc.count,
pos_acc.component_type,
pos_acc.type
)
// Extract normals
var normals = null
if (norm_acc) {
var norm_view = g.views[norm_acc.view]
normals = model_c.extract_accessor(
buffer_blob,
norm_view.byte_offset || 0,
norm_view.byte_stride || 0,
norm_acc.byte_offset || 0,
norm_acc.count,
norm_acc.component_type,
norm_acc.type
)
}
// Extract UVs
var uvs = null
if (uv_acc) {
var uv_view = g.views[uv_acc.view]
uvs = model_c.extract_accessor(
buffer_blob,
uv_view.byte_offset || 0,
uv_view.byte_stride || 0,
uv_acc.byte_offset || 0,
uv_acc.count,
uv_acc.component_type,
uv_acc.type
)
}
// Extract indices
var indices = null
var index_count = 0
var index_type = "uint16"
if (idx_acc) {
var idx_view = g.views[idx_acc.view]
indices = model_c.extract_indices(
buffer_blob,
idx_view.byte_offset || 0,
idx_acc.byte_offset || 0,
idx_acc.count,
idx_acc.component_type
)
index_count = idx_acc.count
index_type = idx_acc.component_type == "u32" ? "uint32" : "uint16"
}
// Pack vertices
var mesh_data = {
vertex_count: vertex_count,
positions: positions,
normals: normals,
uvs: uvs,
colors: null
}
var packed = model_c.pack_vertices(mesh_data)
// Create GPU buffers
var vertex_buffer = _create_vertex_buffer(packed.data)
var index_buffer = indices ? _create_index_buffer(indices) : null
// Get material texture
var texture = null
if (prim.material != null && g.materials[prim.material]) {
var mat = g.materials[prim.material]
if (mat.pbr && mat.pbr.base_color_texture) {
var tex_info = mat.pbr.base_color_texture
var tex_obj = g.textures[tex_info.texture]
if (tex_obj && tex_obj.image != null && textures[tex_obj.image]) {
texture = textures[tex_obj.image]
}
}
}
return {
vertex_buffer: vertex_buffer,
index_buffer: index_buffer,
index_count: index_count,
index_type: index_type,
vertex_count: vertex_count,
material_index: prim.material,
texture: texture
}
}
function _load_obj_model(parsed) {
if (!parsed || !parsed.meshes || parsed.meshes.length == 0) return null
var model = {
meshes: [],
nodes: [],
root_nodes: [],
textures: [],
materials: [],
animations: [],
animation_count: 0
}
for (var i = 0; i < parsed.meshes.length; i++) {
var mesh = parsed.meshes[i]
var packed = model_c.pack_vertices(mesh)
var vertex_buffer = _create_vertex_buffer(packed.data)
var index_buffer = _create_index_buffer(mesh.indices)
model.meshes.push({
vertex_buffer: vertex_buffer,
index_buffer: index_buffer,
index_count: mesh.index_count,
index_type: mesh.index_type || "uint16",
vertex_count: mesh.vertex_count,
name: mesh.name,
texture: null
})
}
// Create a single root transform
var root = make_transform()
model.nodes.push(root)
model.root_nodes.push(root)
return model
}
function make_cube(w, h, d) {
var hw = w / 2, hh = h / 2, hd = d / 2
// 8 vertices, 36 indices (12 triangles)
var positions = [
// Front face
-hw, -hh, hd, hw, -hh, hd, hw, hh, hd, -hw, hh, hd,
// Back face
-hw, -hh, -hd, -hw, hh, -hd, hw, hh, -hd, hw, -hh, -hd,
// Top face
-hw, hh, -hd, -hw, hh, hd, hw, hh, hd, hw, hh, -hd,
// Bottom face
-hw, -hh, -hd, hw, -hh, -hd, hw, -hh, hd, -hw, -hh, hd,
// Right face
hw, -hh, -hd, hw, hh, -hd, hw, hh, hd, hw, -hh, hd,
// Left face
-hw, -hh, -hd, -hw, -hh, hd, -hw, hh, hd, -hw, hh, -hd
]
var normals = [
0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1,
0, 0,-1, 0, 0,-1, 0, 0,-1, 0, 0,-1,
0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0,
0,-1, 0, 0,-1, 0, 0,-1, 0, 0,-1, 0,
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0,
-1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0
]
var uvs = [
0,1, 1,1, 1,0, 0,0,
1,1, 1,0, 0,0, 0,1,
0,0, 0,1, 1,1, 1,0,
0,1, 1,1, 1,0, 0,0,
1,1, 1,0, 0,0, 0,1,
0,1, 1,1, 1,0, 0,0
]
var indices = [
0, 1, 2, 0, 2, 3,
4, 5, 6, 4, 6, 7,
8, 9,10, 8,10,11,
12,13,14, 12,14,15,
16,17,18, 16,18,19,
20,21,22, 20,22,23
]
return _make_model_from_arrays(positions, normals, uvs, indices)
}
function make_sphere(r, segments) {
if (!segments) segments = 12
var positions = []
var normals = []
var uvs = []
var indices = []
for (var y = 0; y <= segments; y++) {
var v = y / segments
var theta = v * 3.14159265
for (var x = 0; x <= segments; x++) {
var u = x / segments
var phi = u * 2 * 3.14159265
var nx = Math.sin(theta) * Math.cos(phi)
var ny = Math.cos(theta)
var nz = Math.sin(theta) * Math.sin(phi)
positions.push(nx * r, ny * r, nz * r)
normals.push(nx, ny, nz)
uvs.push(u, v)
}
}
for (var y = 0; y < segments; y++) {
for (var x = 0; x < segments; x++) {
var i = y * (segments + 1) + x
indices.push(i, i + 1, i + segments + 1)
indices.push(i + 1, i + segments + 2, i + segments + 1)
}
}
return _make_model_from_arrays(positions, normals, uvs, indices)
}
function make_cylinder(r, h, segments) {
if (!segments) segments = 12
var positions = []
var normals = []
var uvs = []
var indices = []
var hh = h / 2
// Side vertices
for (var i = 0; i <= segments; i++) {
var u = i / segments
var angle = u * 2 * 3.14159265
var nx = Math.cos(angle)
var nz = Math.sin(angle)
positions.push(nx * r, -hh, nz * r)
normals.push(nx, 0, nz)
uvs.push(u, 1)
positions.push(nx * r, hh, nz * r)
normals.push(nx, 0, nz)
uvs.push(u, 0)
}
// Side indices
for (var i = 0; i < segments; i++) {
var base = i * 2
indices.push(base, base + 1, base + 2)
indices.push(base + 1, base + 3, base + 2)
}
return _make_model_from_arrays(positions, normals, uvs, indices)
}
function make_plane(w, h) {
var hw = w / 2, hh = h / 2
var positions = [-hw, 0, -hh, hw, 0, -hh, hw, 0, hh, -hw, 0, hh]
var normals = [0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0]
var uvs = [0, 0, 1, 0, 1, 1, 0, 1]
var indices = [0, 1, 2, 0, 2, 3]
return _make_model_from_arrays(positions, normals, uvs, indices)
}
function make_model(vertices, indices, uvs_in, colors_in) {
var vertex_count = vertices.length / 3
var positions = vertices
var normals = []
var uvs = uvs_in || []
var colors = colors_in || []
// Generate flat normals
for (var i = 0; i < vertex_count; i++) {
normals.push(0, 1, 0)
}
return _make_model_from_arrays(positions, normals, uvs, indices, colors)
}
function load_texture(path) {
var png = use('cell-image/png')
var data = io.slurp(path)
if (!data) return null
var img = png.decode(data)
if (!img) return null
return _create_texture(img.width, img.height, img.pixels)
}
function make_texture(w, h, rgba_u8) {
return _create_texture(w, h, rgba_u8)
}
// Audio stubs (to be implemented with audio backend)
function load_sound(path) { return null }
function play_sound(sound, opts) { return null }
function stop_sound(voice_id) {}
function load_music(path) { return null }
function play_music(music, loop) {}
function stop_music() {}
function set_music_volume(v) {}
function set_sfx_volume(v) {}
// ============================================================================
// 3) Transforms - Hierarchical with dirty flags
// ============================================================================
function make_transform(opts) {
opts = opts || {}
return {
parent: opts.parent || null,
children: [],
// TRS (authoring) - translation
x: opts.x || 0,
y: opts.y || 0,
z: opts.z || 0,
// Quaternion rotation
qx: opts.qx || 0,
qy: opts.qy || 0,
qz: opts.qz || 0,
qw: opts.qw != null ? opts.qw : 1,
// Scale
sx: opts.sx != null ? opts.sx : 1,
sy: opts.sy != null ? opts.sy : 1,
sz: opts.sz != null ? opts.sz : 1,
// Matrices (opaque blobs)
local_mat: opts.local_mat || null,
world_mat: null,
// Authority: if true, local_mat is used directly, TRS ignored
has_local_mat: opts.has_local_mat || false,
// Cache invalidation
dirty_local: true,
dirty_world: true
}
}
function transform_set_parent(child, parent) {
if (child.parent) {
var idx = child.parent.children.indexOf(child)
if (idx >= 0) child.parent.children.splice(idx, 1)
}
child.parent = parent
if (parent) parent.children.push(child)
transform_mark_dirty(child)
}
function transform_mark_dirty(t) {
t.dirty_local = true
t.dirty_world = true
for (var i = 0; i < t.children.length; i++) {
transform_mark_world_dirty(t.children[i])
}
}
function transform_mark_world_dirty(t) {
t.dirty_world = true
for (var i = 0; i < t.children.length; i++) {
transform_mark_world_dirty(t.children[i])
}
}
function transform_get_local_matrix(t) {
if (!t.dirty_local && t.local_mat) return t.local_mat
if (t.has_local_mat && t.local_mat) {
t.dirty_local = false
return t.local_mat
}
// Build from TRS
t.local_mat = model_c.mat4_from_trs(
t.x, t.y, t.z,
t.qx, t.qy, t.qz, t.qw,
t.sx, t.sy, t.sz
)
t.dirty_local = false
return t.local_mat
}
function transform_get_world_matrix(t) {
if (!t.dirty_world && t.world_mat) return t.world_mat
var local = transform_get_local_matrix(t)
if (t.parent) {
var parent_world = transform_get_world_matrix(t.parent)
t.world_mat = model_c.mat4_mul(parent_world, local)
} else {
t.world_mat = local
}
t.dirty_world = false
return t.world_mat
}
function transform_set_position(t, x, y, z) {
t.x = x; t.y = y; t.z = z
t.has_local_mat = false
transform_mark_dirty(t)
}
function transform_set_rotation_quat(t, qx, qy, qz, qw) {
t.qx = qx; t.qy = qy; t.qz = qz; t.qw = qw
t.has_local_mat = false
transform_mark_dirty(t)
}
function transform_set_scale(t, sx, sy, sz) {
t.sx = sx; t.sy = sy; t.sz = sz
t.has_local_mat = false
transform_mark_dirty(t)
}
function transform_set_matrix(t, mat) {
t.local_mat = mat
t.has_local_mat = true
transform_mark_dirty(t)
}
// ============================================================================
// 4) Camera & Projection
// ============================================================================
function camera_look_at(ex, ey, ez, tx, ty, tz, upx, upy, upz) {
_state.camera.x = ex
_state.camera.y = ey
_state.camera.z = ez
_state.camera.target_x = tx
_state.camera.target_y = ty
_state.camera.target_z = tz
_state.camera.up_x = upx != null ? upx : 0
_state.camera.up_y = upy != null ? upy : 1
_state.camera.up_z = upz != null ? upz : 0
}
function camera_perspective(fov_deg, near, far) {
_state.camera.projection = "perspective"
_state.camera.fov = fov_deg || 60
_state.camera.near = near || 0.1
_state.camera.far = far || 1000
}
function camera_ortho(left, right, bottom, top, near, far) {
_state.camera.projection = "ortho"
_state.camera.left = left
_state.camera.right = right
_state.camera.bottom = bottom
_state.camera.top = top
_state.camera.near = near || -1
_state.camera.far = far || 1
}
function camera_get() {
return Object.assign({}, _state.camera)
}
// ============================================================================
// 5) Render State Stack
// ============================================================================
function push_state() {
_state.state_stack.push({
camera: Object.assign({}, _state.camera),
ambient: _state.ambient.slice(),
light_dir: _state.light_dir.slice(),
light_color: _state.light_color.slice(),
light_intensity: _state.light_intensity,
fog_near: _state.fog_near,
fog_far: _state.fog_far,
fog_color: _state.fog_color ? _state.fog_color.slice() : null,
current_material: _state.current_material,
im_color: _state.im_color.slice()
})
}
function pop_state() {
if (_state.state_stack.length == 0) return
var s = _state.state_stack.pop()
_state.camera = s.camera
_state.ambient = s.ambient
_state.light_dir = s.light_dir
_state.light_color = s.light_color
_state.light_intensity = s.light_intensity
_state.fog_near = s.fog_near
_state.fog_far = s.fog_far
_state.fog_color = s.fog_color
_state.current_material = s.current_material
_state.im_color = s.im_color
}
// ============================================================================
// 6) Materials, Lighting, Fog
// ============================================================================
function make_material(kind, opts) {
opts = opts || {}
return {
kind: kind,
texture: opts.texture || null,
color: opts.color || [1, 1, 1, 1]
}
}
function set_material(material) {
_state.current_material = material
}
function set_ambient(r, g, b) {
_state.ambient = [r, g, b]
}
function set_light_dir(dx, dy, dz, r, g, b, intensity) {
var len = Math.sqrt(dx*dx + dy*dy + dz*dz)
_state.light_dir = [dx/len, dy/len, dz/len]
_state.light_color = [r, g, b]
_state.light_intensity = intensity != null ? intensity : 1.0
}
function set_fog(near, far, color_or_null) {
_state.fog_near = near
_state.fog_far = far
_state.fog_color = color_or_null
}
// ============================================================================
// 7) Drawing - Models & Immediate Mode
// ============================================================================
function clear(r, g, b, a, clear_depth) {
if (a == null) a = 1.0
if (clear_depth == null) clear_depth = true
_state._clear_color = [r, g, b, a]
_state._clear_depth = clear_depth
}
function clear_depth() {
_state._clear_depth_only = true
}
function draw_model(model, transform, anim_instance) {
if (!model || !model.meshes) return
// Get view and projection matrices
var view_matrix = _compute_view_matrix()
var proj_matrix = _compute_projection_matrix()
// If transform is provided, use it as an additional parent for the model's root nodes
var extra_transform = transform ? transform_get_world_matrix(transform) : null
// Draw each node that has a mesh
for (var ni = 0; ni < model.nodes.length; ni++) {
var node = model.nodes[ni]
if (node.mesh_index == null) continue
// Find meshes for this node
for (var mi = 0; mi < model.meshes.length; mi++) {
var mesh = model.meshes[mi]
if (mesh.mesh_index != node.mesh_index) continue
// Get world matrix for this node
var node_world = transform_get_world_matrix(node)
// Apply extra transform if provided
var world_matrix = extra_transform
? model_c.mat4_mul(extra_transform, node_world)
: node_world
// Get material/texture
var mat = _state.current_material
var tint = mat ? mat.color : [1, 1, 1, 1]
var tex = mesh.texture || (mat && mat.texture ? mat.texture : _state.white_texture)
// Build uniforms in cell script
var uniforms = _build_uniforms(world_matrix, view_matrix, proj_matrix, tint)
_draw_mesh(mesh, uniforms, tex, mat ? mat.kind : "lit")
_state.draw_calls++
_state.triangles += mesh.index_count / 3
}
}
// If model has no nodes with meshes, draw meshes directly (fallback)
if (model.nodes.length == 0) {
var world_matrix = transform ? transform_get_world_matrix(transform) : model_c.mat4_identity()
var mat = _state.current_material
var tint = mat ? mat.color : [1, 1, 1, 1]
for (var i = 0; i < model.meshes.length; i++) {
var mesh = model.meshes[i]
var tex = mesh.texture || (mat && mat.texture ? mat.texture : _state.white_texture)
var uniforms = _build_uniforms(world_matrix, view_matrix, proj_matrix, tint)
_draw_mesh(mesh, uniforms, tex, mat ? mat.kind : "lit")
_state.draw_calls++
_state.triangles += mesh.index_count / 3
}
}
}
// Build uniform buffer using C helper (blob API doesn't have write_f32)
function _build_uniforms(model_mat, view_mat, proj_mat, tint) {
return model_c.build_uniforms({
model: model_mat,
view: view_mat,
projection: proj_mat,
ambient: _state.ambient,
light_dir: _state.light_dir,
light_color: _state.light_color,
light_intensity: _state.light_intensity,
fog_near: _state.fog_near,
fog_far: _state.fog_far,
fog_color: _state.fog_color,
tint: tint,
style_id: _state.style_id,
resolution_w: _state.resolution_w,
resolution_h: _state.resolution_h
})
}
// Immediate mode
function begin_triangles() { _state.im_mode = "triangles"; _state.im_vertices = [] }
function begin_lines() { _state.im_mode = "lines"; _state.im_vertices = [] }
function begin_points() { _state.im_mode = "points"; _state.im_vertices = [] }
function color(r, g, b, a) {
_state.im_color = [r, g, b, a != null ? a : 1.0]
}
function vertex(x, y, z, u, v) {
_state.im_vertices.push({
x: x, y: y, z: z,
u: u || 0, v: v || 0,
color: _state.im_color.slice()
})
}
function end() {
if (_state.im_vertices.length == 0) return
// Flush immediate mode geometry
_flush_immediate()
_state.im_mode = null
_state.im_vertices = []
}
// ============================================================================
// 8) Animation
// ============================================================================
function anim_instance(model) {
return {
model: model,
clip_index: 0,
time: 0,
speed: 1,
loop: true,
playing: false
}
}
function anim_clip_count(model) {
return model.animation_count || 0
}
function anim_clip_duration(model, clip_index) {
if (!model.animations || clip_index >= model.animations.length) return 0
return model.animations[clip_index].duration || 0
}
function anim_play(anim, clip_index, loop) {
anim.clip_index = clip_index
anim.loop = loop != null ? loop : true
anim.playing = true
anim.time = 0
}
function anim_stop(anim) {
anim.playing = false
}
function anim_set_time(anim, seconds) {
anim.time = seconds
}
function anim_set_speed(anim, speed) {
anim.speed = speed
}
function anim_update(anim, dt_val) {
if (!anim.playing) return
dt_val = dt_val != null ? dt_val : _state.dt
anim.time += dt_val * anim.speed
var duration = anim_clip_duration(anim.model, anim.clip_index)
if (duration > 0 && anim.time > duration) {
if (anim.loop) {
anim.time = anim.time % duration
} else {
anim.time = duration
anim.playing = false
}
}
}
function anim_pop_events(anim) {
return []
}
// ============================================================================
// 9) Collision (stubs for v1)
// ============================================================================
var _colliders = []
var _collider_id = 0
function add_collider_sphere(transform, radius, layer_mask, user) {
var c = {
id: _collider_id++,
type: "sphere",
transform: transform,
radius: radius,
layer_mask: layer_mask || 1,
user: user
}
_colliders.push(c)
return c
}
function add_collider_box(transform, sx, sy, sz, layer_mask, user) {
var c = {
id: _collider_id++,
type: "box",
transform: transform,
sx: sx, sy: sy, sz: sz,
layer_mask: layer_mask || 1,
user: user
}
_colliders.push(c)
return c
}
function remove_collider(collider) {
for (var i = 0; i < _colliders.length; i++) {
if (_colliders[i].id == collider.id) {
_colliders.splice(i, 1)
return
}
}
}
function overlaps(layer_mask_a, layer_mask_b) {
// Simple O(n^2) collision check
var results = []
for (var i = 0; i < _colliders.length; i++) {
for (var j = i + 1; j < _colliders.length; j++) {
var a = _colliders[i]
var b = _colliders[j]
if (layer_mask_a != null && !(a.layer_mask & layer_mask_a)) continue
if (layer_mask_b != null && !(b.layer_mask & layer_mask_b)) continue
if (_check_collision(a, b)) {
results.push({a: a, b: b})
}
}
}
return results
}
function raycast(ox, oy, oz, dx, dy, dz, max_dist, layer_mask) {
// Stub - returns null for now
return null
}
function _check_collision(a, b) {
// Simple sphere-sphere for now
var ax = a.transform.x, ay = a.transform.y, az = a.transform.z
var bx = b.transform.x, by = b.transform.y, bz = b.transform.z
var dx = bx - ax, dy = by - ay, dz = bz - az
var dist = Math.sqrt(dx*dx + dy*dy + dz*dz)
var ra = a.radius || Math.max(a.sx || 0, a.sy || 0, a.sz || 0)
var rb = b.radius || Math.max(b.sx || 0, b.sy || 0, b.sz || 0)
return dist < ra + rb
}
// ============================================================================
// 10) Input
// ============================================================================
function btn(id, player) {
// Map button IDs to keyboard keys for now
var key_map = ['z', 'x', 'c', 'v', 'a', 's', 'return', 'escape']
var key = key_map[id]
return _state.keys_held[key] || false
}
function btnp(id, player) {
var key_map = ['z', 'x', 'c', 'v', 'a', 's', 'return', 'escape']
var key = key_map[id]
return _state.keys_pressed[key] || false
}
function axis(id, player) {
return _state.axes[id] || 0
}
// ============================================================================
// 11) RNG & Math Helpers
// ============================================================================
function seed(seed_int) {
_state.rng_seed = seed_int
}
function rand() {
// Simple LCG
_state.rng_seed = (_state.rng_seed * 1103515245 + 12345) & 0x7fffffff
return _state.rng_seed / 0x7fffffff
}
function irand(min_inclusive, max_inclusive) {
return Math.floor(rand() * (max_inclusive - min_inclusive + 1)) + min_inclusive
}
// ============================================================================
// Internal GPU functions
// ============================================================================
function _init_gpu() {
// Create window
_state.window = new video.window({
title: "retro3d",
width: _state.resolution_w * 2,
height: _state.resolution_h * 2
})
// Create GPU device
_state.gpu = new gpu_mod.gpu({ debug: true, shaders_msl: true })
_state.gpu.claim_window(_state.window)
// Load shaders
var vert_code = io.slurp("shaders/retro3d.vert.msl")
var frag_code = io.slurp("shaders/retro3d.frag.msl")
if (!vert_code || !frag_code) {
log.console("retro3d: failed to load shaders")
return
}
var vert_shader = new gpu_mod.shader(_state.gpu, {
code: vert_code,
stage: "vertex",
format: "msl",
entrypoint: "vertex_main",
num_uniform_buffers: 2
})
var frag_shader = new gpu_mod.shader(_state.gpu, {
code: frag_code,
stage: "fragment",
format: "msl",
entrypoint: "fragment_main",
num_uniform_buffers: 2,
num_samplers: 1
})
// Create pipeline
var swapchain_format = _state.gpu.swapchain_format(_state.window)
_state.pipeline_lit = new gpu_mod.graphics_pipeline(_state.gpu, {
vertex: vert_shader,
fragment: frag_shader,
primitive: "triangle",
cull: "back",
face: "counter_clockwise",
fill: "fill",
vertex_buffer_descriptions: [{
slot: 0,
pitch: 48,
input_rate: "vertex"
}],
vertex_attributes: [
{ location: 0, buffer_slot: 0, format: "float3", offset: 0 },
{ location: 1, buffer_slot: 0, format: "float3", offset: 12 },
{ location: 2, buffer_slot: 0, format: "float2", offset: 24 },
{ location: 3, buffer_slot: 0, format: "float4", offset: 32 }
],
target: {
color_targets: [{ format: swapchain_format, blend: { enabled: false } }],
depth: "d32 float s8"
},
depth: {
test: true,
write: true,
compare: "less"
}
})
// Create samplers
var style = _styles[_state.style]
_state.sampler_nearest = new gpu_mod.sampler(_state.gpu, {
min_filter: "nearest",
mag_filter: "nearest",
u: "repeat",
v: "repeat"
})
_state.sampler_linear = new gpu_mod.sampler(_state.gpu, {
min_filter: "linear",
mag_filter: "linear",
u: "repeat",
v: "repeat"
})
// Create depth texture
_state.depth_texture = new gpu_mod.texture(_state.gpu, {
width: _state.resolution_w * 2,
height: _state.resolution_h * 2,
format: "d32 float s8",
type: "2d",
layers: 1,
mip_levels: 1,
depth_target: true
})
// Create white texture (1x1)
var white_pixels = new blob_mod(32, true)
_state.white_texture = _create_texture(1, 1, stone(white_pixels))
}
function _create_vertex_buffer(data) {
var size = data.length / 8
var buffer = new gpu_mod.buffer(_state.gpu, {
size: size,
vertex: true
})
var transfer = new gpu_mod.transfer_buffer(_state.gpu, {
size: size,
usage: "upload"
})
transfer.copy_blob(_state.gpu, data)
var cmd = _state.gpu.acquire_cmd_buffer()
var copy = cmd.copy_pass()
copy.upload_to_buffer(
{ transfer_buffer: transfer, offset: 0 },
{ buffer: buffer, offset: 0, size: size },
false
)
copy.end()
cmd.submit()
return buffer
}
function _create_index_buffer(data) {
var size = data.length / 8
var buffer = new gpu_mod.buffer(_state.gpu, {
size: size,
index: true
})
var transfer = new gpu_mod.transfer_buffer(_state.gpu, {
size: size,
usage: "upload"
})
transfer.copy_blob(_state.gpu, data)
var cmd = _state.gpu.acquire_cmd_buffer()
var copy = cmd.copy_pass()
copy.upload_to_buffer(
{ transfer_buffer: transfer, offset: 0 },
{ buffer: buffer, offset: 0, size: size },
false
)
copy.end()
cmd.submit()
return buffer
}
function _create_texture(w, h, pixels) {
var tex = new gpu_mod.texture(_state.gpu, {
width: w,
height: h,
format: "rgba8",
type: "2d",
layers: 1,
mip_levels: 1,
sampler: true
})
var size = w * h * 4
var transfer = new gpu_mod.transfer_buffer(_state.gpu, {
size: size,
usage: "upload"
})
transfer.copy_blob(_state.gpu, pixels)
var cmd = _state.gpu.acquire_cmd_buffer()
var copy = cmd.copy_pass()
copy.upload_to_texture(
{ transfer_buffer: transfer, offset: 0, pixels_per_row: w, rows_per_layer: h },
{ texture: tex, x: 0, y: 0, z: 0, w: w, h: h, d: 1 },
false
)
copy.end()
cmd.submit()
tex.width = w
tex.height = h
return tex
}
function _make_model_from_arrays(positions, normals, uvs, indices, colors) {
var vertex_count = positions.length / 3
// Build mesh object
var mesh = {
vertex_count: vertex_count,
index_count: indices.length
}
mesh.positions = model_c.f32_blob(positions)
mesh.normals = normals && normals.length > 0 ? model_c.f32_blob(normals) : null
mesh.uvs = uvs && uvs.length > 0 ? model_c.f32_blob(uvs) : null
mesh.colors = colors && colors.length > 0 ? model_c.f32_blob(colors) : null
mesh.indices = model_c.u16_blob(indices)
mesh.index_type = "uint16"
// Pack and create GPU buffers
var packed = model_c.pack_vertices(mesh)
return {
meshes: [{
vertex_buffer: _create_vertex_buffer(packed.data),
index_buffer: _create_index_buffer(mesh.indices),
index_count: mesh.index_count,
index_type: "uint16",
vertex_count: vertex_count,
texture: null,
mesh_index: 0
}],
nodes: [],
root_nodes: [],
textures: [],
materials: [],
animations: [],
animation_count: 0
}
}
function _compute_view_matrix() {
var c = _state.camera
return model_c.compute_view_matrix(
c.x, c.y, c.z,
c.target_x, c.target_y, c.target_z,
c.up_x, c.up_y, c.up_z
)
}
function _compute_projection_matrix() {
var c = _state.camera
var aspect = _state.resolution_w / _state.resolution_h
if (c.projection == "perspective") {
return model_c.compute_perspective(c.fov, aspect, c.near, c.far)
} else {
return model_c.compute_ortho(c.left, c.right, c.bottom, c.top, c.near, c.far)
}
}
function _draw_mesh(mesh, uniforms, texture, kind) {
// This will be called during render pass
_state._pending_draws = _state._pending_draws || []
_state._pending_draws.push({
mesh: mesh,
uniforms: uniforms,
texture: texture,
kind: kind
})
}
function _flush_immediate() {
// Convert immediate mode vertices to a temporary mesh and draw
if (_state.im_vertices.length == 0) return
var verts = _state.im_vertices
var positions = []
var normals = []
var uvs = []
var colors = []
var indices = []
for (var i = 0; i < verts.length; i++) {
var v = verts[i]
positions.push(v.x, v.y, v.z)
normals.push(0, 1, 0)
uvs.push(v.u, v.v)
colors.push(v.color[0], v.color[1], v.color[2], v.color[3])
indices.push(i)
}
var model = _make_model_from_arrays(positions, normals, uvs, indices, colors)
var transform = make_transform()
draw_model(model, transform)
}
// ============================================================================
// Frame rendering (called by cart runner)
// ============================================================================
function _begin_frame() {
_state.draw_calls = 0
_state.triangles = 0
_state.keys_pressed = {}
_state._pending_draws = []
_state._clear_color = [0, 0, 0, 1]
_state._clear_depth = true
}
function _process_events() {
var ev
while ((ev = events.poll()) != null) {
if (ev.type == "quit" || ev.type == "window_close_requested") {
return false
}
if (ev.type == "key_down" || ev.type == "key_up") {
var key_name = keyboard.get_key_name(ev.key).toLowerCase()
var pressed = ev.type == "key_down"
if (pressed && !_state.keys_held[key_name]) {
_state.keys_pressed[key_name] = true
}
_state.keys_held[key_name] = pressed
// Map WASD to axes
if (key_name == "w") _state.axes[1] = pressed ? -1 : 0
if (key_name == "s") _state.axes[1] = pressed ? 1 : 0
if (key_name == "a") _state.axes[0] = pressed ? -1 : 0
if (key_name == "d") _state.axes[0] = pressed ? 1 : 0
}
}
return true
}
function _end_frame() {
if (!_state.gpu) return
var cmd = _state.gpu.acquire_cmd_buffer()
// Get swapchain pass instead
var pass_desc = {
color_targets: [{
texture: null, // Will use swapchain
load: "clear",
store: "store",
clear_color: {
r: _state._clear_color[0],
g: _state._clear_color[1],
b: _state._clear_color[2],
a: _state._clear_color[3]
}
}]
}
if (_state.depth_texture) {
pass_desc.depth_stencil = {
texture: _state.depth_texture,
load: _state._clear_depth ? "clear" : "load",
store: "dont_care",
stencil_load: "clear",
stencil_store: "dont_care",
clear: 1.0,
clear_stencil: 0
}
}
var swap_pass = cmd.swapchain_pass(_state.window, pass_desc)
// Draw all pending meshes
var draws = _state._pending_draws || []
for (var i = 0; i < draws.length; i++) {
var d = draws[i]
swap_pass.bind_pipeline(_state.pipeline_lit)
swap_pass.bind_vertex_buffers(0, [{ buffer: d.mesh.vertex_buffer, offset: 0 }])
swap_pass.bind_index_buffer({ buffer: d.mesh.index_buffer, offset: 0 }, d.mesh.index_type == "uint32" ? 32 : 16)
// Shaders use [[buffer(1)]] for uniforms (buffer(0) is vertex data)
cmd.push_vertex_uniform_data(1, d.uniforms)
cmd.push_fragment_uniform_data(1, d.uniforms)
var sampler = _state.style_id == 1 ? _state.sampler_linear : _state.sampler_nearest
swap_pass.bind_fragment_samplers(0, [{ texture: d.texture, sampler: sampler }])
swap_pass.draw_indexed(d.mesh.index_count, 1, 0, 0, 0)
}
swap_pass.end()
cmd.submit()
_state.frame_count++
}
// Export the module
return {
set_style: set_style,
get_style: get_style,
set_resolution: set_resolution,
time: time,
dt: dt,
stat: stat,
log: log_msg,
load_model: load_model,
make_cube: make_cube,
make_sphere: make_sphere,
make_cylinder: make_cylinder,
make_plane: make_plane,
make_model: make_model,
load_texture: load_texture,
make_texture: make_texture,
load_sound: load_sound,
play_sound: play_sound,
stop_sound: stop_sound,
load_music: load_music,
play_music: play_music,
stop_music: stop_music,
set_music_volume: set_music_volume,
set_sfx_volume: set_sfx_volume,
make_transform: make_transform,
transform_set_parent: transform_set_parent,
transform_set_position: transform_set_position,
transform_set_rotation_quat: transform_set_rotation_quat,
transform_set_scale: transform_set_scale,
transform_set_matrix: transform_set_matrix,
transform_get_local_matrix: transform_get_local_matrix,
transform_get_world_matrix: transform_get_world_matrix,
camera_look_at: camera_look_at,
camera_perspective: camera_perspective,
camera_ortho: camera_ortho,
camera_get: camera_get,
push_state: push_state,
pop_state: pop_state,
make_material: make_material,
set_material: set_material,
set_ambient: set_ambient,
set_light_dir: set_light_dir,
set_fog: set_fog,
clear: clear,
clear_depth: clear_depth,
draw_model: draw_model,
begin_triangles: begin_triangles,
begin_lines: begin_lines,
begin_points: begin_points,
color: color,
vertex: vertex,
end: end,
anim_instance: anim_instance,
anim_clip_count: anim_clip_count,
anim_clip_duration: anim_clip_duration,
anim_play: anim_play,
anim_stop: anim_stop,
anim_set_time: anim_set_time,
anim_set_speed: anim_set_speed,
anim_update: anim_update,
anim_pop_events: anim_pop_events,
add_collider_sphere: add_collider_sphere,
add_collider_box: add_collider_box,
remove_collider: remove_collider,
overlaps: overlaps,
raycast: raycast,
btn: btn,
btnp: btnp,
axis: axis,
seed: seed,
rand: rand,
irand: irand,
// Internal functions for cart runner
_state: _state,
_begin_frame: _begin_frame,
_process_events: _process_events,
_end_frame: _end_frame
}