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 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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
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);
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user