retro controls

This commit is contained in:
2025-12-13 19:37:30 -06:00
parent d4eee53926
commit d6c4e35201
3 changed files with 246 additions and 13 deletions

221
core.cm
View File

@@ -10,6 +10,7 @@ var gltf = use('mload/gltf')
var obj_loader = use('mload/obj') var obj_loader = use('mload/obj')
var model_c = use('model') var model_c = use('model')
var png = use('cell-image/png') var png = use('cell-image/png')
var resize_mod = use('cell-image/resize')
var anim_mod = use('animation') var anim_mod = use('animation')
var skin_mod = use('skin') var skin_mod = use('skin')
@@ -88,10 +89,12 @@ var _styles = {
id: 0, id: 0,
resolution: [320, 240], resolution: [320, 240],
vertex_snap: true, vertex_snap: true,
affine_texturing: true, affine_texturing: false,
filtering: "nearest", filtering: "nearest",
color_depth: 15, color_depth: 15,
dither: false dither: false,
tex_sizes: { low: 64, normal: 128, hero: 256 },
tri_budget: 2000
}, },
n64: { n64: {
id: 1, id: 1,
@@ -100,7 +103,9 @@ var _styles = {
affine_texturing: false, affine_texturing: false,
filtering: "linear", filtering: "linear",
color_depth: 16, color_depth: 16,
dither: false dither: false,
tex_sizes: { normal: 32, hero: 64 },
tri_budget: 3000
}, },
saturn: { saturn: {
id: 2, id: 2,
@@ -109,10 +114,23 @@ var _styles = {
affine_texturing: true, affine_texturing: true,
filtering: "nearest", filtering: "nearest",
color_depth: 15, color_depth: 15,
dither: true dither: true,
tex_sizes: { low: 32, normal: 64, hero: 128 },
tri_budget: 1500
} }
} }
// Track original image data for textures (needed for re-resizing on style change)
// Key: texture object, Value: { width, height, pixels }
// Using symbol property since WeakMap not available
var TEX_ORIGINAL = Symbol("texture_original")
// Triangle budget warning state
var _tri_warning_state = {
last_warn_time: 0,
warned_this_cycle: false
}
// ============================================================================ // ============================================================================
// 1) System / Style / Time / Logging // 1) System / Style / Time / Logging
// ============================================================================ // ============================================================================
@@ -168,11 +186,108 @@ function log_msg() {
log.console("[retro3d] " + args.join(" ")) log.console("[retro3d] " + args.join(" "))
} }
// Switch platform style at runtime (re-resizes all cached textures)
function switch_style(style_name) {
def style = _styles[style_name]
if (!style) {
log.console("retro3d: unknown style: " + style_name)
return false
}
_state.style = style_name
_state.style_id = style.id
_state.resolution_w = style.resolution[0]
_state.resolution_h = style.resolution[1]
// Invalidate texture cache - textures will be re-resized on next use
// WeakMap doesn't have clear(), so we just create a new one
// The old textures will be garbage collected
log.console("retro3d: switched to " + style_name + " style")
return true
}
// Get texture size for current platform and tier
function _get_tex_size(tier) {
def style = _styles[_state.style]
if (!style) return 64
def sizes = style.tex_sizes
if (tier == "hero" && sizes.hero) return sizes.hero
if (tier == "low" && sizes.low) return sizes.low
return sizes.normal || 64
}
// Resize an image to platform-appropriate size
function _resize_image_for_platform(img, tier) {
def target_size = _get_tex_size(tier)
def src_w = img.width
def src_h = img.height
// If already at or below target size, return as-is
if (src_w <= target_size && src_h <= target_size) {
return img
}
// Resize to fit within target_size x target_size (square)
def scale = target_size / Math.max(src_w, src_h)
def dst_w = Math.floor(src_w * scale)
def dst_h = Math.floor(src_h * scale)
if (dst_w < 1) dst_w = 1
if (dst_h < 1) dst_h = 1
// Use nearest filter for retro look
return resize_mod.resize(img, dst_w, dst_h, { filter: "nearest" })
}
// Create a texture with platform-appropriate sizing, storing original for re-resize
function _create_texture_for_platform(w, h, pixels, tier) {
def original = { width: w, height: h, pixels: pixels }
def img = _resize_image_for_platform(original, tier)
def tex = _create_texture(img.width, img.height, img.pixels)
// Tag texture with current style and tier for cache invalidation
tex._style_tag = _state.style
tex._tier = tier || "normal"
// Store original for re-resizing on style switch
tex[TEX_ORIGINAL] = original
// _texture_originals.set(tex, original) - not using WeakMap anymore
return tex
}
// Get or create resized texture for current platform
function _get_platform_texture(tex, tier) {
if (!tex) return _state.white_texture
// Check if texture needs re-resizing (style changed)
if (tex._style_tag != _state.style || tex._tier != tier) {
def original = tex[TEX_ORIGINAL]
if (original) {
def img = _resize_image_for_platform(original, tier)
// Create new GPU texture with resized data
def new_tex = _create_texture(img.width, img.height, img.pixels)
new_tex._style_tag = _state.style
new_tex._tier = tier
new_tex[TEX_ORIGINAL] = original
return new_tex
}
}
return tex
}
// ============================================================================ // ============================================================================
// 2) Assets - Models, Textures, Audio // 2) Assets - Models, Textures, Audio
// ============================================================================ // ============================================================================
function load_model(path) { // load_model(path, [opts])
// opts.type: "normal" (default) or "hero" - applies texture tier to all materials
function load_model(path, opts) {
opts = opts || {}
var tex_tier = opts.type || "normal"
var ext = path.slice(path.lastIndexOf('.') + 1).toLowerCase() var ext = path.slice(path.lastIndexOf('.') + 1).toLowerCase()
if (ext == "obj") { if (ext == "obj") {
@@ -180,7 +295,7 @@ function load_model(path) {
if (!data) return null if (!data) return null
var parsed = obj_loader.decode(data) var parsed = obj_loader.decode(data)
if (!parsed) return null if (!parsed) return null
return _load_obj_model(parsed) return _load_obj_model(parsed, tex_tier)
} }
if (ext != "gltf" && ext != "glb") { if (ext != "gltf" && ext != "glb") {
@@ -209,15 +324,25 @@ function load_model(path) {
animations: [], animations: [],
animation_count: g.animations ? g.animations.length : 0, animation_count: g.animations ? g.animations.length : 0,
skins: [], skins: [],
_gltf: g _gltf: g,
_tex_tier: tex_tier,
_original_images: []
} }
// Load textures from decoded gltf images // Load textures from decoded gltf images with platform-appropriate sizing
for (var ti = 0; ti < g.images.length; ti++) { for (var ti = 0; ti < g.images.length; ti++) {
var img = g.images[ti] var img = g.images[ti]
var tex = null var tex = null
if (img && img.pixels) { if (img && img.pixels) {
tex = _create_texture(img.pixels.width, img.pixels.height, img.pixels.pixels) // Store original image data for re-resizing on style switch
model._original_images.push({
width: img.pixels.width,
height: img.pixels.height,
pixels: img.pixels.pixels
})
tex = _create_texture_for_platform(img.pixels.width, img.pixels.height, img.pixels.pixels, tex_tier)
} else {
model._original_images.push(null)
} }
model.textures.push(tex) model.textures.push(tex)
} }
@@ -493,7 +618,7 @@ function _process_gltf_primitive(g, buffer_blob, prim, textures) {
} }
} }
function _load_obj_model(parsed) { function _load_obj_model(parsed, tex_tier) {
if (!parsed || !parsed.meshes || parsed.meshes.length == 0) return null if (!parsed || !parsed.meshes || parsed.meshes.length == 0) return null
var model = { var model = {
@@ -503,7 +628,9 @@ function _load_obj_model(parsed) {
textures: [], textures: [],
materials: [], materials: [],
animations: [], animations: [],
animation_count: 0 animation_count: 0,
_tex_tier: tex_tier || "normal",
_original_images: []
} }
for (var i = 0; i < parsed.meshes.length; i++) { for (var i = 0; i < parsed.meshes.length; i++) {
@@ -900,7 +1027,8 @@ var _uber_material = {
alpha_mode: "opaque", // "opaque", "mask", or "blend" alpha_mode: "opaque", // "opaque", "mask", or "blend"
alpha_cutoff: 0.5, // for mask mode alpha_cutoff: 0.5, // for mask mode
double_sided: false, double_sided: false,
unlit: false unlit: false,
hero: false // if true, uses hero texture tier instead of normal
} }
function make_material(kind, opts) { function make_material(kind, opts) {
@@ -913,9 +1041,57 @@ function make_material(kind, opts) {
mat.alpha_cutoff = opts.alpha_cutoff != null ? opts.alpha_cutoff : 0.5 mat.alpha_cutoff = opts.alpha_cutoff != null ? opts.alpha_cutoff : 0.5
mat.double_sided = opts.double_sided || false mat.double_sided = opts.double_sided || false
mat.unlit = kind == "unlit" || opts.unlit || false mat.unlit = kind == "unlit" || opts.unlit || false
mat.hero = opts.hero || false
return mat return mat
} }
// Recalculate all textures in a model for the current platform style
// Call this after switch_style() to update model textures
function recalc_model_textures(model, tier_override) {
if (!model || !model._original_images) return
var tier = tier_override || model._tex_tier || "normal"
model._tex_tier = tier
// Resize all textures from originals
for (var i = 0; i < model._original_images.length; i++) {
var orig = model._original_images[i]
if (!orig) continue
var img = _resize_image_for_platform(orig, tier)
var new_tex = _create_texture(img.width, img.height, img.pixels)
new_tex._style_tag = _state.style
new_tex._tier = tier
new_tex[TEX_ORIGINAL] = orig
// Update model's texture array
model.textures[i] = new_tex
}
// Update material textures to point to new resized textures
if (model._gltf && model._gltf.materials) {
for (var mi = 0; mi < model.materials.length; mi++) {
var mat = model.materials[mi]
var gmat = model._gltf.materials[mi]
if (gmat && gmat.pbr && gmat.pbr.base_color_texture) {
var tex_info = gmat.pbr.base_color_texture
var tex_obj = model._gltf.textures[tex_info.texture]
if (tex_obj && tex_obj.image != null && model.textures[tex_obj.image]) {
mat.texture = model.textures[tex_obj.image]
}
}
}
}
// Update mesh textures
for (var mi = 0; mi < model.meshes.length; mi++) {
var mesh = model.meshes[mi]
if (mesh.material_index != null && model.materials[mesh.material_index]) {
mesh.texture = model.materials[mesh.material_index].texture
}
}
}
function set_material(material) { function set_material(material) {
_state.current_material = material _state.current_material = material
} }
@@ -1813,12 +1989,32 @@ function _end_frame() {
cmd.submit() cmd.submit()
_state.frame_count++ _state.frame_count++
// Check triangle budget and warn (once per minute max)
_check_tri_budget()
}
// Check if triangle count exceeds platform budget, warn once per minute
function _check_tri_budget() {
def style = _styles[_state.style]
if (!style || !style.tri_budget) return
if (_state.triangles > style.tri_budget) {
def now = time_mod.number()
// Only warn once per minute (60 seconds)
if (now - _tri_warning_state.last_warn_time >= 60) {
log.console("[retro3d] WARNING: Triangle count " + text(_state.triangles) +
" exceeds " + _state.style + " budget of " + text(style.tri_budget))
_tri_warning_state.last_warn_time = now
}
}
} }
// Export the module // Export the module
return { return {
set_style: set_style, set_style: set_style,
get_style: get_style, get_style: get_style,
switch_style: switch_style,
set_resolution: set_resolution, set_resolution: set_resolution,
time: time, time: time,
dt: dt, dt: dt,
@@ -1826,6 +2022,7 @@ return {
log: log_msg, log: log_msg,
load_model: load_model, load_model: load_model,
recalc_model_textures: recalc_model_textures,
make_cube: make_cube, make_cube: make_cube,
make_sphere: make_sphere, make_sphere: make_sphere,
make_cylinder: make_cylinder, make_cylinder: make_cylinder,

View File

@@ -1,6 +1,7 @@
// Model Viewer for retro3d // Model Viewer for retro3d
// Usage: cell run examples/modelview.ce <model_path> [style] // Usage: cell run examples/modelview.ce <model_path> [style]
// style: ps1, n64, or saturn (default: ps1) // style: ps1, n64, or saturn (default: ps1)
// Controls: F1=PS1, F2=N64, F3=Saturn
var io = use('fd') var io = use('fd')
var time_mod = use('time') var time_mod = use('time')
@@ -10,6 +11,11 @@ var retro3d = use('core')
var model_path = args[0] || "Duck.glb" var model_path = args[0] || "Duck.glb"
var style = args[1] || "ps1" 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 // Camera orbit state
var cam_distance = 5 var cam_distance = 5
var cam_yaw = 0 var cam_yaw = 0
@@ -75,12 +81,31 @@ function _init() {
log.console(" R/F - Move target up/down") log.console(" R/F - Move target up/down")
log.console(" SPACE - Toggle animation") log.console(" SPACE - Toggle animation")
log.console(" 1-9 - Switch animation clip") 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(" ESC - Exit")
// Start the main loop // Start the main loop
frame() 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) { function _update(dt) {
// Handle input for camera orbit // Handle input for camera orbit
if (retro3d._state.keys_held['a']) { if (retro3d._state.keys_held['a']) {
@@ -148,6 +173,17 @@ function _update(dt) {
} }
} }
// Switch platform style with F1-F3
if (retro3d._state.keys_pressed['f1']) {
_switch_to_style(0) // PS1
}
if (retro3d._state.keys_pressed['f2']) {
_switch_to_style(1) // N64
}
if (retro3d._state.keys_pressed['f3']) {
_switch_to_style(2) // Saturn
}
// Update animation // Update animation
if (anim && anim_playing) { if (anim && anim_playing) {
retro3d.anim_update(anim, dt) retro3d.anim_update(anim, dt)

View File

@@ -721,7 +721,7 @@ JSValue js_model_build_uniforms(JSContext *js, JSValue this_val, int argc, JSVal
JS_FreeValue(js, style_v); JS_FreeValue(js, style_v);
uniforms[89] = (style_id == 0) ? 1.0f : 0.0f; // vertex_snap for PS1 uniforms[89] = (style_id == 0) ? 1.0f : 0.0f; // vertex_snap for PS1
uniforms[90] = (style_id == 0) ? 1.0f : 0.0f; // affine texturing for PS1 uniforms[90] = (style_id == -1) ? 1.0f : 0.0f; // affine texturing for PS1
uniforms[91] = (style_id == 2) ? 1.0f : 0.0f; // dither for Saturn uniforms[91] = (style_id == 2) ? 1.0f : 0.0f; // dither for Saturn
// Resolution at offset 92-95 (w, h, unused, unused) // Resolution at offset 92-95 (w, h, unused, unused)