retro controls
This commit is contained in:
221
core.cm
221
core.cm
@@ -10,6 +10,7 @@ var gltf = use('mload/gltf')
|
||||
var obj_loader = use('mload/obj')
|
||||
var model_c = use('model')
|
||||
var png = use('cell-image/png')
|
||||
var resize_mod = use('cell-image/resize')
|
||||
var anim_mod = use('animation')
|
||||
var skin_mod = use('skin')
|
||||
|
||||
@@ -88,10 +89,12 @@ var _styles = {
|
||||
id: 0,
|
||||
resolution: [320, 240],
|
||||
vertex_snap: true,
|
||||
affine_texturing: true,
|
||||
affine_texturing: false,
|
||||
filtering: "nearest",
|
||||
color_depth: 15,
|
||||
dither: false
|
||||
dither: false,
|
||||
tex_sizes: { low: 64, normal: 128, hero: 256 },
|
||||
tri_budget: 2000
|
||||
},
|
||||
n64: {
|
||||
id: 1,
|
||||
@@ -100,7 +103,9 @@ var _styles = {
|
||||
affine_texturing: false,
|
||||
filtering: "linear",
|
||||
color_depth: 16,
|
||||
dither: false
|
||||
dither: false,
|
||||
tex_sizes: { normal: 32, hero: 64 },
|
||||
tri_budget: 3000
|
||||
},
|
||||
saturn: {
|
||||
id: 2,
|
||||
@@ -109,10 +114,23 @@ var _styles = {
|
||||
affine_texturing: true,
|
||||
filtering: "nearest",
|
||||
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
|
||||
// ============================================================================
|
||||
@@ -168,11 +186,108 @@ function log_msg() {
|
||||
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
|
||||
// ============================================================================
|
||||
|
||||
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()
|
||||
|
||||
if (ext == "obj") {
|
||||
@@ -180,7 +295,7 @@ function load_model(path) {
|
||||
if (!data) return null
|
||||
var parsed = obj_loader.decode(data)
|
||||
if (!parsed) return null
|
||||
return _load_obj_model(parsed)
|
||||
return _load_obj_model(parsed, tex_tier)
|
||||
}
|
||||
|
||||
if (ext != "gltf" && ext != "glb") {
|
||||
@@ -209,15 +324,25 @@ function load_model(path) {
|
||||
animations: [],
|
||||
animation_count: g.animations ? g.animations.length : 0,
|
||||
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++) {
|
||||
var img = g.images[ti]
|
||||
var tex = null
|
||||
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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
var model = {
|
||||
@@ -503,7 +628,9 @@ function _load_obj_model(parsed) {
|
||||
textures: [],
|
||||
materials: [],
|
||||
animations: [],
|
||||
animation_count: 0
|
||||
animation_count: 0,
|
||||
_tex_tier: tex_tier || "normal",
|
||||
_original_images: []
|
||||
}
|
||||
|
||||
for (var i = 0; i < parsed.meshes.length; i++) {
|
||||
@@ -900,7 +1027,8 @@ var _uber_material = {
|
||||
alpha_mode: "opaque", // "opaque", "mask", or "blend"
|
||||
alpha_cutoff: 0.5, // for mask mode
|
||||
double_sided: false,
|
||||
unlit: false
|
||||
unlit: false,
|
||||
hero: false // if true, uses hero texture tier instead of normal
|
||||
}
|
||||
|
||||
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.double_sided = opts.double_sided || false
|
||||
mat.unlit = kind == "unlit" || opts.unlit || false
|
||||
mat.hero = opts.hero || false
|
||||
return mat
|
||||
}
|
||||
|
||||
// Recalculate all textures in a model for the current platform style
|
||||
// Call this after switch_style() to update model textures
|
||||
function recalc_model_textures(model, tier_override) {
|
||||
if (!model || !model._original_images) return
|
||||
|
||||
var tier = tier_override || model._tex_tier || "normal"
|
||||
model._tex_tier = tier
|
||||
|
||||
// Resize all textures from originals
|
||||
for (var i = 0; i < model._original_images.length; i++) {
|
||||
var orig = model._original_images[i]
|
||||
if (!orig) continue
|
||||
|
||||
var img = _resize_image_for_platform(orig, tier)
|
||||
var new_tex = _create_texture(img.width, img.height, img.pixels)
|
||||
new_tex._style_tag = _state.style
|
||||
new_tex._tier = tier
|
||||
new_tex[TEX_ORIGINAL] = orig
|
||||
|
||||
// Update model's texture array
|
||||
model.textures[i] = new_tex
|
||||
}
|
||||
|
||||
// Update material textures to point to new resized textures
|
||||
if (model._gltf && model._gltf.materials) {
|
||||
for (var mi = 0; mi < model.materials.length; mi++) {
|
||||
var mat = model.materials[mi]
|
||||
var gmat = model._gltf.materials[mi]
|
||||
if (gmat && gmat.pbr && gmat.pbr.base_color_texture) {
|
||||
var tex_info = gmat.pbr.base_color_texture
|
||||
var tex_obj = model._gltf.textures[tex_info.texture]
|
||||
if (tex_obj && tex_obj.image != null && model.textures[tex_obj.image]) {
|
||||
mat.texture = model.textures[tex_obj.image]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
_state.current_material = material
|
||||
}
|
||||
@@ -1813,12 +1989,32 @@ function _end_frame() {
|
||||
cmd.submit()
|
||||
|
||||
_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
|
||||
return {
|
||||
set_style: set_style,
|
||||
get_style: get_style,
|
||||
switch_style: switch_style,
|
||||
set_resolution: set_resolution,
|
||||
time: time,
|
||||
dt: dt,
|
||||
@@ -1826,6 +2022,7 @@ return {
|
||||
log: log_msg,
|
||||
|
||||
load_model: load_model,
|
||||
recalc_model_textures: recalc_model_textures,
|
||||
make_cube: make_cube,
|
||||
make_sphere: make_sphere,
|
||||
make_cylinder: make_cylinder,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// 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
|
||||
|
||||
var io = use('fd')
|
||||
var time_mod = use('time')
|
||||
@@ -10,6 +11,11 @@ var retro3d = use('core')
|
||||
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
|
||||
var cam_yaw = 0
|
||||
@@ -75,12 +81,31 @@ 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")
|
||||
|
||||
// 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']) {
|
||||
@@ -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
|
||||
if (anim && anim_playing) {
|
||||
retro3d.anim_update(anim, dt)
|
||||
|
||||
2
model.c
2
model.c
@@ -721,7 +721,7 @@ JSValue js_model_build_uniforms(JSContext *js, JSValue this_val, int argc, JSVal
|
||||
JS_FreeValue(js, style_v);
|
||||
|
||||
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
|
||||
|
||||
// Resolution at offset 92-95 (w, h, unused, unused)
|
||||
|
||||
Reference in New Issue
Block a user