split apart modules

This commit is contained in:
2025-12-14 01:36:35 -06:00
parent a223d3d2b3
commit 828db06c74
13 changed files with 2212 additions and 2941 deletions

90
camera.cm Normal file
View File

@@ -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
}

235
collision.cm Normal file
View File

@@ -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
}

2758
core.cm

File diff suppressed because it is too large Load Diff

View File

@@ -1,156 +0,0 @@
// Billboard Test for retro3d
// Usage: cell run examples/billboard_test.ce <sprite_path>
// 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 <sprite_path>")
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()

View File

@@ -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)

View File

@@ -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()

View File

@@ -1,20 +1,16 @@
// Model Viewer for retro3d
// Usage: cell run examples/modelview.ce <model_path> [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 <model_path>
// 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)

View File

@@ -1,117 +0,0 @@
// Sprite Test for retro3d
// Usage: cell run examples/sprite_test.ce <sprite_path>
// 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 <sprite_path>")
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()

122
input.cm Normal file
View File

@@ -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,
}

View File

@@ -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)

133
math.cm Normal file
View File

@@ -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
}

552
resources.cm Normal file
View File

@@ -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
}

376
sdl.cm Normal file
View File

@@ -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
}