split apart modules
This commit is contained in:
90
camera.cm
Normal file
90
camera.cm
Normal 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
235
collision.cm
Normal 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
|
||||
}
|
||||
@@ -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()
|
||||
109
examples/cube.ce
109
examples/cube.ce
@@ -1,13 +1,8 @@
|
||||
// Simple Cube Demo for retro3d
|
||||
// Usage: cell run examples/cube.ce [style]
|
||||
// style: ps1, n64, or saturn (default: ps1)
|
||||
// Simple Cube Demo for lance3d
|
||||
// Usage: cell run examples/cube.ce
|
||||
|
||||
var time_mod = use('time')
|
||||
var retro3d = use('core')
|
||||
|
||||
// Parse command line arguments
|
||||
var args = $_.args || []
|
||||
var style = args[0] || "ps1"
|
||||
var lance3d = use('core')
|
||||
|
||||
// Camera orbit state
|
||||
var cam_distance = 5
|
||||
@@ -15,36 +10,47 @@ var cam_yaw = 0
|
||||
var cam_pitch = 0.4
|
||||
var auto_rotate = true
|
||||
|
||||
// Model and transform
|
||||
// Models and materials
|
||||
var cube = null
|
||||
var transform = null
|
||||
var cube_mat = null
|
||||
var ground = null
|
||||
var ground_mat = null
|
||||
|
||||
// Timing
|
||||
var last_time = 0
|
||||
|
||||
function _init() {
|
||||
log.console("retro3d Cube Demo")
|
||||
log.console("Style: " + style)
|
||||
log.console("lance3d Cube Demo")
|
||||
|
||||
// Initialize retro3d with selected style
|
||||
retro3d.set_style(style)
|
||||
// Initialize lance3d with PS1 style
|
||||
lance3d.set_style("ps1")
|
||||
|
||||
// Create a cube
|
||||
cube = retro3d.make_cube(1, 1, 1)
|
||||
// Create a cube mesh
|
||||
cube = lance3d.make_cube(1, 1, 1)
|
||||
cube_mat = {
|
||||
color_map: null,
|
||||
paint: [0.8, 0.3, 0.2, 1],
|
||||
coverage: "opaque",
|
||||
face: "single",
|
||||
lamp: "lit"
|
||||
}
|
||||
|
||||
// Create transform for the cube
|
||||
transform = retro3d.make_transform()
|
||||
transform.y = 0.5
|
||||
// Create ground plane
|
||||
ground = lance3d.make_plane(10, 10)
|
||||
ground_mat = {
|
||||
color_map: null,
|
||||
paint: [0.3, 0.5, 0.3, 1],
|
||||
coverage: "opaque",
|
||||
face: "single",
|
||||
lamp: "lit"
|
||||
}
|
||||
|
||||
// Set up lighting
|
||||
retro3d.set_ambient(0.3, 0.3, 0.35)
|
||||
retro3d.set_light_dir(0.5, 1.0, 0.3, 1.0, 0.95, 0.9, 1.0)
|
||||
|
||||
// Set up a default material
|
||||
var mat = retro3d.make_material("lit", {
|
||||
color: [0.8, 0.3, 0.2, 1]
|
||||
lance3d.set_lighting({
|
||||
sun_dir: [0.5, 1.0, 0.3],
|
||||
sun_color: [1.0, 0.95, 0.9],
|
||||
ambient: [0.3, 0.3, 0.35]
|
||||
})
|
||||
retro3d.set_material(mat)
|
||||
|
||||
last_time = time_mod.number()
|
||||
|
||||
@@ -65,77 +71,62 @@ function _update(dt) {
|
||||
}
|
||||
|
||||
// Handle input for camera orbit
|
||||
if (retro3d._state.keys_held['a']) {
|
||||
if (lance3d._state.keys_held['a']) {
|
||||
cam_yaw -= 2.0 * dt
|
||||
auto_rotate = false
|
||||
}
|
||||
if (retro3d._state.keys_held['d']) {
|
||||
if (lance3d._state.keys_held['d']) {
|
||||
cam_yaw += 2.0 * dt
|
||||
auto_rotate = false
|
||||
}
|
||||
if (retro3d._state.keys_held['w']) {
|
||||
if (lance3d._state.keys_held['w']) {
|
||||
cam_pitch += 2.0 * dt
|
||||
if (cam_pitch > 1.5) cam_pitch = 1.5
|
||||
}
|
||||
if (retro3d._state.keys_held['s']) {
|
||||
if (lance3d._state.keys_held['s']) {
|
||||
cam_pitch -= 2.0 * dt
|
||||
if (cam_pitch < -1.5) cam_pitch = -1.5
|
||||
}
|
||||
|
||||
// Toggle auto-rotate
|
||||
if (retro3d._state.keys_pressed[' ']) {
|
||||
if (lance3d._state.keys_pressed['space']) {
|
||||
auto_rotate = !auto_rotate
|
||||
}
|
||||
|
||||
// Exit on escape
|
||||
if (retro3d._state.keys_held['escape']) {
|
||||
if (lance3d._state.keys_held['escape']) {
|
||||
$_.stop()
|
||||
}
|
||||
}
|
||||
|
||||
function _draw() {
|
||||
// Clear with style-appropriate color
|
||||
if (style == "ps1") {
|
||||
retro3d.clear(0.1, 0.05, 0.15, 1.0)
|
||||
} else if (style == "n64") {
|
||||
retro3d.clear(0.0, 0.1, 0.2, 1.0)
|
||||
} else {
|
||||
retro3d.clear(0.05, 0.05, 0.1, 1.0)
|
||||
}
|
||||
// Clear
|
||||
lance3d.clear(0.1, 0.05, 0.15, 1.0)
|
||||
|
||||
// Set up camera
|
||||
retro3d.camera_perspective(60, 0.1, 100)
|
||||
lance3d.camera_perspective(60, 0.1, 100)
|
||||
|
||||
// Calculate camera position from orbit
|
||||
var cam_x = Math.sin(cam_yaw) * Math.cos(cam_pitch) * cam_distance
|
||||
var cam_y = Math.sin(cam_pitch) * cam_distance + 0.5
|
||||
var cam_z = Math.cos(cam_yaw) * Math.cos(cam_pitch) * cam_distance
|
||||
|
||||
retro3d.camera_look_at(cam_x, cam_y, cam_z, 0, 0.5, 0)
|
||||
lance3d.camera_look_at(cam_x, cam_y, cam_z, 0, 0.5, 0)
|
||||
|
||||
// Draw the cube
|
||||
retro3d.draw_model(cube, transform)
|
||||
// Draw the cube with transform
|
||||
var cube_transform = lance3d.translation_matrix(0, 0.5, 0)
|
||||
lance3d.draw_mesh(cube, cube_transform, cube_mat)
|
||||
|
||||
// Draw ground plane
|
||||
retro3d.push_state()
|
||||
var ground_mat = retro3d.make_material("lit", {
|
||||
color: [0.3, 0.5, 0.3, 1]
|
||||
})
|
||||
retro3d.set_material(ground_mat)
|
||||
|
||||
var ground = retro3d.make_plane(10, 10)
|
||||
var ground_transform = retro3d.make_transform()
|
||||
retro3d.draw_model(ground, ground_transform)
|
||||
|
||||
retro3d.pop_state()
|
||||
lance3d.draw_mesh(ground, null, ground_mat)
|
||||
}
|
||||
|
||||
function frame() {
|
||||
// Begin frame
|
||||
retro3d._begin_frame()
|
||||
lance3d._begin_frame()
|
||||
|
||||
// Process events
|
||||
if (!retro3d._process_events()) {
|
||||
if (!lance3d._process_events()) {
|
||||
log.console("Exiting...")
|
||||
$_.stop()
|
||||
return
|
||||
@@ -152,8 +143,8 @@ function frame() {
|
||||
// Draw
|
||||
_draw()
|
||||
|
||||
// End frame (submit GPU commands)
|
||||
retro3d._end_frame()
|
||||
// End frame
|
||||
lance3d._end_frame()
|
||||
|
||||
// Schedule next frame
|
||||
$_.delay(frame, 1/60)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
122
input.cm
Normal 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,
|
||||
}
|
||||
22
lance3d.md
22
lance3d.md
@@ -1,4 +1,8 @@
|
||||
## Core API
|
||||
Set style ps1 / n64 / saturn
|
||||
|
||||
set_style("ps1" | "n64" | "saturn")
|
||||
|
||||
Get time since game boot, in seconds
|
||||
time()
|
||||
|
||||
@@ -38,6 +42,9 @@ make_sphere(r, segments=12) -> mesh
|
||||
make_cylinder(r, h, segments=12) -> mesh
|
||||
make_plane(w, h) -> mesh
|
||||
|
||||
Gets information about the animations on a model
|
||||
anim_info(model)
|
||||
|
||||
Generates a pose that can be applied to a model
|
||||
sample_pose(model, name, time)
|
||||
|
||||
@@ -74,15 +81,12 @@ camera_perspective(fov_deg=60, near=0.1, far=1000)
|
||||
camera_ortho(left, right, bottom, top, near=-1, far=1)
|
||||
|
||||
## Collision API
|
||||
retro3d.add_collider_sphere(transform, radius, opts={layer_mask:1, user:null}) -> collider
|
||||
retro3d.add_collider_box(transform, sx, sy, sz, opts={layer_mask:1, user:null}) -> collider
|
||||
add_collider_sphere(transform, radius, opts={layer_mask:1, user:null}) -> collider
|
||||
add_collider_box(transform, sx, sy, sz, opts={layer_mask:1, user:null}) -> collider
|
||||
|
||||
retro3d.set_collider_transform(collider, transform)
|
||||
retro3d.set_collider_layer(collider, layer_mask)
|
||||
overlaps(layer_mask_a=null, layer_mask_b=null) -> array<{a: collider, b: collider}>
|
||||
|
||||
retro3d.overlaps(layer_mask_a=null, layer_mask_b=null) -> array<{a: collider, b: collider}>
|
||||
|
||||
retro3d.raycast(ox,oy,oz, dx,dy,dz, opts={
|
||||
raycast(ox,oy,oz, dx,dy,dz, opts={
|
||||
max_dist: Infinity,
|
||||
layer_mask: 0xFFFFFFFF
|
||||
}) -> null | {x,y,z, nx,ny,nz, distance, collider}
|
||||
@@ -102,9 +106,11 @@ irand(min_inclusive, max_inclusive) -> int
|
||||
vertex {x, y, z, u=0, v=0, color}
|
||||
point(vertex, size=1.0)
|
||||
line(vertex_a, vertex_b, width=1.0)
|
||||
grid(size, step, norm = {x:0,y:1,z:0}, color)
|
||||
|
||||
triangle(vertex_a, vertex_b, vertex_c, mat=null)
|
||||
ray(ox,oy,oz, dx,dy,dz, len, opts)
|
||||
grid(size, step, norm = {x:0,y:1,z:0}, color)
|
||||
|
||||
aabb(cx,cy,cz, ex,ey,ez, opts) // center+extents
|
||||
obb(transform, sx,sy,sz, opts) // oriented via transform
|
||||
frustum(camera_state_or_params, opts)
|
||||
|
||||
133
math.cm
Normal file
133
math.cm
Normal 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
552
resources.cm
Normal 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
376
sdl.cm
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user