From 813d4e771cee8b7325ee1f01147e85af797bc111 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Mon, 29 Dec 2025 20:46:42 -0600 Subject: [PATCH] sprite vert --- clay2.cm | 8 +- compositor.cm | 652 ++++++++++++++++++-------------------------------- film2d.cm | 444 ++++++---------------------------- geometry.c | 86 +++++++ graphics.cm | 2 +- prosperon.cm | 3 +- sdl_gpu.cm | 81 ++----- sprite.cm | 101 +++++++- 8 files changed, 511 insertions(+), 866 deletions(-) diff --git a/clay2.cm b/clay2.cm index 0bdfc92b..97c72cd6 100644 --- a/clay2.cm +++ b/clay2.cm @@ -244,13 +244,7 @@ clay.layout = function(fn, size) { } function process_configs(configs) { - // Merge array of configs from right to left (right overrides left) - // And merge with base_config - var res = meme(base_config) - for (var c of configs) { - if (c) res = meme(res, c) - } - return res + return meme(base_config, ...configs) } function push_node(configs, contain_mode) { diff --git a/compositor.cm b/compositor.cm index 75b76d4b..42f27f48 100644 --- a/compositor.cm +++ b/compositor.cm @@ -1,120 +1,108 @@ -// compositor.cm - High-level compositing API +// compositor.cm - Unified Compositor (Rewritten) // -// The compositor orchestrates renderers (film2d, forward3d, imgui...) to produce a final image. -// It handles: -// - Renderer selection per layer (film2d, forward3d, imgui, retro3d...) -// - Camera + viewport binding per pass -// - Target ownership (composition root, fixed resolution, effect islands) -// - Presentation modes (stretch, letterbox, integer_scale, etc.) -// - Global post effects (CRT, etc.) -// - Layer stacking order +// The compositor is the SINGLE OWNER of all effects. +// It takes scene descriptions and produces abstract render plans. +// +// Architecture: +// Scene Tree (Retained) -> Compositor (Effect orchestration) -> Render Plan -> Backend // // The compositor does NOT know about sprites/tilemaps/text - that's renderer territory. -// It only knows about plates, targets, viewports, and post-processing. +// It only knows about plates, targets, viewports, effects, and post-processing. -// ======================================================================== -// PRESENTATION MODES (how child targets map into parent targets) -// ======================================================================== -// "disabled" : 1:1 mapping, no scaling -// "stretch" : stretch to fill parent rect -// "letterbox" : fit by largest dimension; bars in clear color -// "overscan" : fit by smallest dimension; overflow clipped -// "integer_scale" : scale by integer multiples; nearest sampling - -// ======================================================================== -// BLEND MODES (backend-agnostic) -// ======================================================================== -// "replace" : overwrite destination -// "over" : alpha over (default) -// "add" : additive - -// ======================================================================== -// RENDERER CAPABILITIES (queried during compilation) -// ======================================================================== -// Each renderer advertises what it can do inline vs requiring an island: -// - supports_mask_stencil: can do hard masks via stencil (no target needed) -// - supports_mask_alpha: can do soft masks (needs target) -// - supports_bloom: can do bloom effect -// - supports_blur: can do blur effect +var effects_mod = use('effects') var compositor = {} -// Default renderer capabilities (conservative - requires islands for everything) -var DEFAULT_CAPS = { - supports_mask_stencil: true, - supports_mask_alpha: true, - supports_bloom: true, - supports_blur: true +// Presentation modes +compositor.PRESENTATION = { + DISABLED: 'disabled', + STRETCH: 'stretch', + LETTERBOX: 'letterbox', + OVERSCAN: 'overscan', + INTEGER_SCALE: 'integer_scale' } -// ======================================================================== -// COMPILATION: Turn composition tree into render plan -// ======================================================================== +// Blend modes +compositor.BLEND = { + REPLACE: 'replace', + OVER: 'over', + ADD: 'add' +} -// Compile a composition into a render plan -// comp: composition description (the DSL) -// renderers: map of renderer_type -> renderer module -// backend: the render backend (sdl_gpu, etc.) +// Compile a composition tree into a render plan compositor.compile = function(comp, renderers, backend) { var ctx = { renderers: renderers, backend: backend, passes: [], targets: {}, + persistent_targets: {}, target_counter: 0, - imgui_node: null + screen_size: null, + target_size: null, + + // Target allocation + alloc_target: function(w, h, hint) { + var key = (hint || 'target') + '_' + text(this.target_counter++) + this.targets[key] = {w: w, h: h, key: key} + return {type: 'target', key: key, w: w, h: h} + }, + + // Persistent target (survives across frames) + get_persistent_target: function(key, w, h) { + if (!this.persistent_targets[key]) { + this.persistent_targets[key] = {w: w, h: h, key: key, persistent: true} + } + return {type: 'target', key: key, w: w, h: h, persistent: true} + } } - // Determine window/screen size from backend - ctx.screen_size = backend.get_window_size() || {width: 1280, height: 720} + // Get screen size from backend + ctx.screen_size = backend.get_window_size ? backend.get_window_size() : {width: 1280, height: 720} if (!ctx.screen_size.width) ctx.screen_size.width = 1280 if (!ctx.screen_size.height) ctx.screen_size.height = 720 - // Compile the composition tree - var result = compile_node(comp, ctx, null, ctx.screen_size) + // Normalize to w/h + ctx.screen_size.w = ctx.screen_size.width + ctx.screen_size.h = ctx.screen_size.height + + // Compile the tree + _compile_node(comp, ctx, null, ctx.screen_size) return { passes: ctx.passes, targets: ctx.targets, - final_output: result.output + persistent_targets: ctx.persistent_targets, + screen_size: ctx.screen_size } } -// Compile a single node in the composition tree -function compile_node(node, ctx, parent_target, parent_size) { +// Compile a single node +function _compile_node(node, ctx, parent_target, parent_size) { if (!node) return {output: null} var node_type = node.type - - // Determine if this node owns a target var owns_target = false var target = parent_target - var target_size = parent_size - - // Target ownership rules: - // 1. Composition root (target: "screen") - // 2. Node has explicit resolution - // 3. Node has effects that require isolation + var target_size = {w: parent_size.w || parent_size.width, h: parent_size.h || parent_size.height} + // Determine target ownership if (node.target == 'screen') { owns_target = true target = 'screen' - target_size = ctx.screen_size + target_size = {w: ctx.screen_size.w, h: ctx.screen_size.h} } else if (node.resolution) { owns_target = true - target_size = {width: node.resolution.w || node.resolution.width || ctx.screen_size.width, height: node.resolution.h || node.resolution.height || ctx.screen_size.height} - target = allocate_target(ctx, target_size, node.name || 'res_target') - } else if (node.effects && effects_require_island(node.effects, ctx)) { + target_size = {w: node.resolution.w || node.resolution.width, h: node.resolution.h || node.resolution.height} + target = ctx.alloc_target(target_size.w, target_size.h, node.name || 'res_target') + } else if (node.effects && _effects_require_target(node.effects, ctx)) { owns_target = true - target_size = parent_size || ctx.screen_size - target = allocate_target(ctx, target_size, node.name || 'effect_island') - } else if (node.pos) { - owns_target = true - target_size = parent_size || ctx.screen_size - target = allocate_target(ctx, target_size, node.name || 'translated_target') + target = ctx.alloc_target(target_size.w, target_size.h, node.name || 'effect_target') } - // Handle clear for target owners + ctx.target_size = target_size + + // Handle clear if (owns_target && node.clear) { ctx.passes.push({ type: 'clear', @@ -123,21 +111,14 @@ function compile_node(node, ctx, parent_target, parent_size) { }) } - // Process based on node type + // Process by type if (node_type == 'composition') { - // Root composition - process layers - return compile_composition(node, ctx, target, target_size) + return _compile_composition(node, ctx, target, target_size) } else if (node_type == 'group') { - // Group with potential effects - process layers - return compile_group(node, ctx, target, target_size, parent_target, parent_size) - } else if (is_renderer_type(node_type)) { - // Renderer layer (film2d, forward3d, imgui, etc.) - return compile_renderer_layer(node, ctx, target, target_size, parent_target, parent_size) + return _compile_group(node, ctx, target, target_size, parent_target, parent_size) + } else if (_is_renderer_type(node_type)) { + return _compile_renderer(node, ctx, target, target_size, parent_target, parent_size) } else if (node_type == 'imgui') { - if (ctx.imgui_node) { - throw new Error("Only one imgui node is allowed in a composition") - } - ctx.imgui_node = node ctx.passes.push({ type: 'imgui', target: target, @@ -151,57 +132,49 @@ function compile_node(node, ctx, parent_target, parent_size) { return {output: target} } -// Compile a composition (root or nested) -function compile_composition(node, ctx, target, target_size) { +// Compile composition root +function _compile_composition(node, ctx, target, target_size) { var layers = node.layers || [] for (var i = 0; i < layers.length; i++) { var layer = layers[i] - var is_first = (i == 0) - - // Set default blend based on position if (!layer.blend) { - layer.blend = is_first ? 'replace' : 'over' + layer.blend = (i == 0) ? 'replace' : 'over' } - - compile_node(layer, ctx, target, target_size) + _compile_node(layer, ctx, target, target_size) } return {output: target} } -// Compile a group (may have effects) -function compile_group(node, ctx, target, target_size, parent_target, parent_size) { +// Compile group with effects +function _compile_group(node, ctx, target, target_size, parent_target, parent_size) { var layers = node.layers || [] var original_target = target - // Process child layers into this target + // Process child layers for (var i = 0; i < layers.length; i++) { var layer = layers[i] - var is_first = (i == 0) - if (!layer.blend) { - layer.blend = is_first ? 'replace' : 'over' + layer.blend = (i == 0) ? 'replace' : 'over' } - - compile_node(layer, ctx, target, target_size) + _compile_node(layer, ctx, target, target_size) } - // Apply effects if any - if (node.effects) { - for (var effect of node.effects) { - effect._node_id = node.name || `node_${ctx.target_counter}` - target = compile_effect(effect, ctx, target, target_size) + // Apply effects - this is where the compositor owns all effect logic + if (node.effects && node.effects.length > 0) { + for (var j = 0; j < node.effects.length; j++) { + var effect = node.effects[j] + target = _compile_effect(effect, ctx, target, target_size, node.name || ('group_' + ctx.target_counter)) } } - // If we allocated our own target (or effects changed target), composite back to parent + // Composite back to parent if needed var needs_composite = (original_target != parent_target || target != original_target) && parent_target if (needs_composite) { var presentation = node.presentation || 'disabled' var blend = node.blend || 'over' - // If parent is screen, use blit_to_screen pass type if (parent_target == 'screen') { ctx.passes.push({ type: 'blit_to_screen', @@ -228,28 +201,28 @@ function compile_group(node, ctx, target, target_size, parent_target, parent_siz return {output: target} } -// Compile a renderer layer (film2d, forward3d, etc.) -function compile_renderer_layer(node, ctx, target, target_size, parent_target, parent_size) { +// Compile renderer layer (film2d, forward3d, etc.) +function _compile_renderer(node, ctx, target, target_size, parent_target, parent_size) { var renderer_type = node.type var renderer = ctx.renderers[renderer_type] if (!renderer) { - log.console(`compositor: Unknown renderer type: ${renderer_type}`) + log.console(`compositor: Unknown renderer: ${renderer_type}`) return {output: target} } - // Determine if this layer owns its own target var layer_target = target var layer_size = target_size var owns_target = false + // Check for resolution override if (node.resolution) { owns_target = true - layer_size = {width: node.resolution.w, height: node.resolution.h} - layer_target = allocate_target(ctx, layer_size, node.name || renderer_type + '_target') + layer_size = {w: node.resolution.w, h: node.resolution.h} + layer_target = ctx.alloc_target(layer_size.w, layer_size.h, node.name || renderer_type + '_target') } - // Handle clear for target owners + // Clear if we own target if (owns_target && node.clear) { ctx.passes.push({ type: 'clear', @@ -270,7 +243,7 @@ function compile_renderer_layer(node, ctx, target, target_size, parent_target, p clear: owns_target ? node.clear : null }) - // If we have our own target, composite back to parent + // Composite back to parent if (owns_target && parent_target) { var presentation = node.presentation || 'disabled' var blend = node.blend || 'over' @@ -290,290 +263,128 @@ function compile_renderer_layer(node, ctx, target, target_size, parent_target, p return {output: layer_target} } -// Compile an effect -function compile_effect(effect, ctx, input_target, target_size) { +// Compile an effect using the effect registry +function _compile_effect(effect, ctx, input_target, target_size, node_id) { var effect_type = effect.type + var effect_def = effects_mod.get(effect_type) - if (effect_type == 'crt') { - var output = allocate_target(ctx, target_size, 'crt_output') - ctx.passes.push({ - type: 'shader_pass', - shader: 'crt', - input: input_target, - output: output, - uniforms: { - curvature: effect.curvature || 0.1, - scanline_intensity: effect.scanline_intensity || 0.3, - vignette: effect.vignette || 0.2, - resolution: [target_size.width, target_size.height] - } - }) - return output + if (!effect_def) { + log.console(`compositor: Unknown effect: ${effect_type}`) + return input_target } - if (effect_type == 'bloom') { - return compile_bloom_effect(effect, ctx, input_target, target_size) - } - - if (effect_type == 'mask') { - return compile_mask_effect(effect, ctx, input_target, target_size) - } - - if (effect_type == 'blur') { - return compile_blur_effect(effect, ctx, input_target, target_size) - } - - if (effect_type == 'accumulator') { - return compile_accumulator_effect(effect, ctx, input_target, target_size) - } - - return input_target -} - -// Compile bloom effect (threshold -> blur passes -> composite) -function compile_bloom_effect(effect, ctx, input_target, target_size) { - var threshold = effect.threshold || 0.8 - var intensity = effect.intensity || 1.0 - var blur_passes = effect.blur_passes || 3 - - // Threshold pass - var threshold_output = allocate_target(ctx, target_size, 'bloom_threshold') - ctx.passes.push({ - type: 'shader_pass', - shader: 'threshold', - input: input_target, - output: threshold_output, - uniforms: {threshold: threshold, intensity: intensity} - }) - - // Blur passes - var blur_src = threshold_output - var texel_size = [1/target_size.width, 1/target_size.height] - - for (var i = 0; i < blur_passes; i++) { - // Horizontal blur - var blur_h = allocate_target(ctx, target_size, 'bloom_blur_h_' + i) - ctx.passes.push({ - type: 'shader_pass', - shader: 'blur', - input: blur_src, - output: blur_h, - uniforms: {direction: [2, 0], texel_size: texel_size} - }) - - // Vertical blur - var blur_v = allocate_target(ctx, target_size, 'bloom_blur_v_' + i) - ctx.passes.push({ - type: 'shader_pass', - shader: 'blur', - input: blur_h, - output: blur_v, - uniforms: {direction: [0, 2], texel_size: texel_size} - }) - - blur_src = blur_v - } - - // Composite bloom back onto input - var output = allocate_target(ctx, target_size, 'bloom_output') - ctx.passes.push({ - type: 'composite_textures', - base: input_target, - overlay: blur_src, - output: output, - mode: 'add' - }) - - return output -} - -// Compile mask effect -// Optimization: use stencil for hard masks (soft: false), texture for soft masks -function compile_mask_effect(effect, ctx, input_target, target_size) { - var soft = effect.soft || false - var source = effect.source - var mode = effect.mode || 'alpha' - var space = effect.space || 'local' - var invert = effect.invert || false - - // For hard masks, we could use stencil (no extra target needed) - // For now, always use texture mask approach for simplicity - // TODO: implement stencil path for hard masks - - if (!soft) { - // Hard mask - could use stencil, but for now use texture - // This is where stencil optimization would go - } - - // Texture mask approach (works for both soft and hard) - var mask_target = allocate_target(ctx, target_size, 'mask_source') - - // Render mask source to its own target - ctx.passes.push({ - type: 'render_mask_source', - source: source, - target: mask_target, - target_size: target_size, - space: space - }) - - // Apply mask - var output = allocate_target(ctx, target_size, 'masked_output') - ctx.passes.push({ - type: 'apply_mask', - content: input_target, - mask: mask_target, - output: output, - mode: mode, - invert: invert - }) - - return output -} - -// Compile blur effect -function compile_blur_effect(effect, ctx, input_target, target_size) { - var passes = effect.passes || 2 - var texel_size = [1/target_size.width, 1/target_size.height] - - var src = input_target - - for (var i = 0; i < passes; i++) { - var blur_h = allocate_target(ctx, target_size, 'blur_h_' + i) - ctx.passes.push({ - type: 'shader_pass', - shader: 'blur', - input: src, - output: blur_h, - uniforms: {direction: [2, 0], texel_size: texel_size} - }) - - var blur_v = allocate_target(ctx, target_size, 'blur_v_' + i) - ctx.passes.push({ - type: 'shader_pass', - shader: 'blur', - input: blur_h, - output: blur_v, - uniforms: {direction: [0, 2], texel_size: texel_size} - }) - - src = blur_v - } - - return src -} - -// Compile accumulator effect (motion blur) -function compile_accumulator_effect(effect, ctx, input_target, target_size) { - var decay = effect.decay != null ? effect.decay : 0.9 - var node_id = effect._node_id || ctx.target_counter++ - - // Create persistent targets for ping-ponging - // We use stable keys based on node_id to ensure they persist across frames in the backend - var accum_prev_key = `accum_prev_${node_id}` - var accum_curr_key = `accum_curr_${node_id}` - - var accum_prev = {type: 'target', key: accum_prev_key, width: target_size.width, height: target_size.height, persistent: true} - var accum_curr = {type: 'target', key: accum_curr_key, width: target_size.width, height: target_size.height, persistent: true} - - // Register them in the plan so the backend knows to create them - ctx.targets[accum_prev_key] = {width: target_size.width, height: target_size.height, name: accum_prev_key, persistent: true} - ctx.targets[accum_curr_key] = {width: target_size.width, height: target_size.height, name: accum_curr_key, persistent: true} - - // Accumulation pass: curr = max(input, prev * decay) - ctx.passes.push({ - type: 'shader_pass', - shader: 'accumulator', - input: input_target, - extra_inputs: [accum_prev], - output: accum_curr, - uniforms: {decay: decay} - }) - - // Feedback pass: copy curr back to prev for next frame - ctx.passes.push({ - type: 'composite', - source: accum_curr, - dest: accum_prev, - source_size: target_size, - dest_size: target_size, - presentation: 'disabled' - }) - - return accum_curr -} - -// ======================================================================== -// HELPERS -// ======================================================================== - -function allocate_target(ctx, size, name) { - name = name || 'target' - size = size || ctx.screen_size || {width: 1280, height: 720} - var w = size.width || (size.w) || 1280 - var h = size.height || (size.h) || 720 - - var key = name + '_' + text(ctx.target_counter++) - ctx.targets[key] = { - width: w, - height: h, - name: key - } - return {type: 'target', key: key, width: w, height: h} -} - -function is_renderer_type(type) { - return type == 'film2d' || type == 'forward3d' || type == 'imgui' || type == 'retro3d' || type == 'simple2d' -} - -function effects_require_island(effects, ctx) { - // Check if any effect requires an offscreen target - for (var effect of effects) { - var type = effect.type - // Most effects require islands - if (type == 'bloom' || type == 'blur' || type == 'crt') return true - // Mask requires island unless we can use stencil - if (type == 'mask') { - var soft = effect.soft || false - if (soft) return true - // Hard mask could use stencil - check renderer caps - // For now, always require island - return true + // Build effect context + var effect_ctx = { + backend: ctx.backend, + target_size: {w: target_size.w, h: target_size.h}, + alloc_target: function(w, h, hint) { + return ctx.alloc_target(w, h, hint) + }, + get_persistent_target: function(key, w, h) { + return ctx.get_persistent_target(node_id + '_' + key, w, h) } } + + // Allocate output target + var output = ctx.alloc_target(target_size.w, target_size.h, effect_type + '_out') + + // Build effect passes + var effect_passes = effect_def.build_passes(input_target, output, effect, effect_ctx) + + // Convert effect passes to compositor passes + for (var i = 0; i < effect_passes.length; i++) { + var ep = effect_passes[i] + ctx.passes.push(_convert_effect_pass(ep, ctx)) + } + + return output +} + +// Convert effect pass to compositor pass format +function _convert_effect_pass(ep, ctx) { + switch (ep.type) { + case 'shader': + if (!ep.input && !ep.inputs) { + ep.input = ep.inputs[0] + } + return { + type: 'shader_pass', + shader: ep.shader, + input: ep.input || ep.inputs[0], + extra_inputs: ep.inputs ? ep.inputs.slice(1) : [], + output: ep.output, + uniforms: ep.uniforms + } + case 'composite': + return { + type: 'composite_textures', + base: ep.base, + overlay: ep.overlay, + output: ep.output, + mode: ep.blend || 'over' + } + case 'blit': + return { + type: 'composite', + source: ep.source, + dest: ep.dest, + source_size: ep.source.w ? {w: ep.source.w, h: ep.source.h} : ctx.target_size, + dest_size: ep.dest.w ? {w: ep.dest.w, h: ep.dest.h} : ctx.target_size, + presentation: 'disabled' + } + case 'render_subtree': + return { + type: 'render_mask_source', + source: ep.root, + target: ep.output, + target_size: {w: ep.output.w, h: ep.output.h}, + space: ep.space || 'local' + } + default: + return ep + } +} + +// Check if effects require a target +function _effects_require_target(effects, ctx) { + for (var i = 0; i < effects.length; i++) { + var effect = effects[i] + if (effects_mod.requires_target(effect.type)) return true + } return false } -// ======================================================================== -// PRESENTATION HELPERS -// ======================================================================== +// Check if type is a renderer +function _is_renderer_type(type) { + return type == 'film2d' || type == 'forward3d' || type == 'imgui' || type == 'retro3d' || type == 'simple2d' +} -// Calculate destination rect for presenting source into dest +// Calculate presentation rect compositor.calculate_presentation_rect = function(source_size, dest_size, mode) { - var sw = source_size.width - var sh = source_size.height - var dw = dest_size.width - var dh = dest_size.height + var sw = source_size.w || source_size.width + var sh = source_size.h || source_size.height + var dw = dest_size.w || dest_size.width + var dh = dest_size.h || dest_size.height - if (mode == 'disabled') return {x: 0, y: 0, width: number.min(sw,dw), height: number.min(sh,dh)} - if (mode == 'stretch') return {x: 0, y: 0, width: dw, height: dh} + if (mode == 'disabled') { + return {x: 0, y: 0, width: number.min(sw, dw), height: number.min(sh, dh)} + } + if (mode == 'stretch') { + return {x: 0, y: 0, width: dw, height: dh} + } var src_aspect = sw / sh var dst_aspect = dw / dh if (mode == 'letterbox') { - var scale = src_aspect > dst_aspect - ? dw / sw - : dh / sh + var scale = src_aspect > dst_aspect ? dw / sw : dh / sh var w = sw * scale var h = sh * scale return {x: (dw - w) / 2, y: (dh - h) / 2, width: w, height: h} } if (mode == 'overscan') { - var scale = src_aspect > dst_aspect - ? dh / sh - : dw / sw + var scale = src_aspect > dst_aspect ? dh / sh : dw / sw var w = sw * scale var h = sh * scale return {x: (dw - w) / 2, y: (dh - h) / 2, width: w, height: h} @@ -588,22 +399,24 @@ compositor.calculate_presentation_rect = function(source_size, dest_size, mode) return {x: (dw - w) / 2, y: (dh - h) / 2, width: w, height: h} } - // Default: no scaling return {x: 0, y: 0, width: sw, height: sh} } -// ======================================================================== -// PLAN EXECUTION -// ======================================================================== - // Execute a compiled render plan compositor.execute = function(plan, renderers, backend) { var target_cache = {} - // Pre-allocate all targets + // Pre-allocate targets for (var key in plan.targets) { var spec = plan.targets[key] - target_cache[key] = backend.get_or_create_target(spec.width, spec.height, key) + target_cache[key] = backend.get_or_create_target(spec.w, spec.h, key) + } + + for (var key in plan.persistent_targets) { + var spec = plan.persistent_targets[key] + if (!target_cache[key]) { + target_cache[key] = backend.get_or_create_target(spec.w, spec.h, key) + } } // Resolve target references @@ -616,18 +429,21 @@ compositor.execute = function(plan, renderers, backend) { // Execute passes var commands = [] - for (var pass of plan.passes) { - var pass_commands = execute_pass(pass, renderers, backend, resolve_target) - for (var cmd of pass_commands) { - commands.push(cmd) + for (var i = 0; i < plan.passes.length; i++) { + var pass = plan.passes[i] + var pass_cmds = _execute_pass(pass, renderers, backend, resolve_target, compositor) + for (var j = 0; j < pass_cmds.length; j++) { + commands.push(pass_cmds[j]) } } return {commands: commands} } -function execute_pass(pass, renderers, backend, resolve_target) { +// Execute a single pass +function _execute_pass(pass, renderers, backend, resolve_target, comp) { var commands = [] + var source switch (pass.type) { case 'clear': @@ -640,7 +456,7 @@ function execute_pass(pass, renderers, backend, resolve_target) { var renderer = renderers[pass.renderer] if (renderer && renderer.render) { var target = resolve_target(pass.target) - var render_result = renderer.render({ + var result = renderer.render({ root: pass.root, camera: pass.camera, target: target, @@ -648,9 +464,9 @@ function execute_pass(pass, renderers, backend, resolve_target) { blend: pass.blend, clear: pass.clear }, backend) - if (render_result && render_result.commands) { - for (var cmd of render_result.commands) { - commands.push(cmd) + if (result && result.commands) { + for (var i = 0; i < result.commands.length; i++) { + commands.push(result.commands[i]) } } } @@ -659,10 +475,10 @@ function execute_pass(pass, renderers, backend, resolve_target) { case 'shader_pass': var input = resolve_target(pass.input) var output = resolve_target(pass.output) - var extra_inputs = [] + var extra = [] if (pass.extra_inputs) { - for (var t of pass.extra_inputs) { - extra_inputs.push(resolve_target(t)) + for (var i = 0; i < pass.extra_inputs.length; i++) { + extra.push(resolve_target(pass.extra_inputs[i])) } } commands.push({ @@ -670,15 +486,15 @@ function execute_pass(pass, renderers, backend, resolve_target) { shader: pass.shader, input: input, output: output, - extra_inputs: extra_inputs, + extra_inputs: extra, uniforms: pass.uniforms }) break - case 'composite': - var source = resolve_target(pass.source) + case 'composite': { + source = resolve_target(pass.source) var dest = resolve_target(pass.dest) - var rect = compositor.calculate_presentation_rect(pass.source_size, pass.dest_size, pass.presentation) + var rect = comp.calculate_presentation_rect(pass.source_size, pass.dest_size, pass.presentation) if (pass.pos) { rect.x += (pass.pos.x || 0) rect.y += (pass.pos.y || 0) @@ -691,11 +507,12 @@ function execute_pass(pass, renderers, backend, resolve_target) { dst_rect: rect, filter: filter }) + } break - case 'blit_to_screen': - var source = resolve_target(pass.source) - var rect = compositor.calculate_presentation_rect(pass.source_size, pass.dest_size, pass.presentation) + case 'blit_to_screen': { + source = resolve_target(pass.source) + var rect = comp.calculate_presentation_rect(pass.source_size, pass.dest_size, pass.presentation) if (pass.pos) { rect.x += (pass.pos.x || 0) rect.y += (pass.pos.y || 0) @@ -708,9 +525,10 @@ function execute_pass(pass, renderers, backend, resolve_target) { dst_rect: rect, filter: filter }) + } break - case 'composite_textures': + case 'composite_textures': { var base = resolve_target(pass.base) var overlay = resolve_target(pass.overlay) var output = resolve_target(pass.output) @@ -721,9 +539,10 @@ function execute_pass(pass, renderers, backend, resolve_target) { output: output, mode: pass.mode || 'over' }) + } break - case 'apply_mask': + case 'apply_mask': { var content = resolve_target(pass.content) var mask = resolve_target(pass.mask) var output = resolve_target(pass.output) @@ -735,37 +554,38 @@ function execute_pass(pass, renderers, backend, resolve_target) { mode: pass.mode, invert: pass.invert }) + } break - case 'imgui': + case 'imgui': { commands.push({ cmd: 'imgui', draw: pass.draw, rect: pass.rect, target: resolve_target(pass.target) }) + } break - - case 'render_mask_source': - // This would render the mask source node - // For now, delegate to film2d renderer + + case 'render_mask_source': { var target = resolve_target(pass.target) var renderer = renderers['film2d'] if (renderer && renderer.render) { - var render_result = renderer.render({ + var result = renderer.render({ root: pass.source, - camera: {pos: [0, 0], width: pass.target_size.width, height: pass.target_size.height, anchor: [0, 0], ortho: true}, + camera: {pos: [0, 0], width: pass.target_size.w, height: pass.target_size.h, anchor: [0, 0], ortho: true}, target: target, target_size: pass.target_size, blend: 'replace', clear: {r: 0, g: 0, b: 0, a: 0} }, backend) - if (render_result && render_result.commands) { - for (var cmd of render_result.commands) { - commands.push(cmd) + if (result && result.commands) { + for (var i = 0; i < result.commands.length; i++) { + commands.push(result.commands[i]) } } } + } break } diff --git a/film2d.cm b/film2d.cm index 3beab615..9c71db99 100644 --- a/film2d.cm +++ b/film2d.cm @@ -1,38 +1,25 @@ -// film2d.cm - 2D Scene Renderer +// film2d.cm - 2D Scene Renderer (Rewritten) // // Handles scene tree traversal, sorting, batching, and draw command emission. -// This is the "how to draw a plate" module - it knows about sprites, tilemaps, -// text, particles, etc. +// This is the "how to draw a 2D plate" module. // -// The compositor calls this renderer with (root, camera, target, viewport, defaults) +// The compositor calls this renderer with (root, camera, target, target_size) // and it produces draw commands. // -// Scene-level effects (bloom, mask, blur on groups) are handled here: -// - Effects that can be done inline (stencil mask) are done without extra targets -// - Effects that need isolation (bloom, soft mask) create "islands" - render subtree -// to intermediate target, apply effect, composite back -// -// This module does NOT know about: -// - Global post effects (CRT over entire composition - that's compositor territory) -// - Other renderers (forward3d, imgui, etc.) +// This module does NOT handle effects - that's compositor territory. +// It only knows about sprites, tilemaps, text, particles, and rects. var film2d = {} -// Renderer capabilities - what this renderer can do inline vs requiring islands +// Renderer capabilities film2d.capabilities = { - supports_mask_stencil: true, // Hard masks via stencil (no target needed) - supports_mask_alpha: true, // Soft masks (needs target) - supports_bloom: true, // Bloom effect (needs target) - supports_blur: true // Blur effect (needs target) + supports_mask_stencil: true, + supports_mask_alpha: true, + supports_bloom: true, + supports_blur: true } -// ======================================================================== -// MAIN RENDER FUNCTION -// ======================================================================== - -// Render a scene tree to a target -// params: {root, camera, target, target_size, blend, clear} -// backend: the render backend (sdl_gpu, etc.) +// Main render function film2d.render = function(params, backend) { var root = params.root var camera = params.camera @@ -42,36 +29,31 @@ film2d.render = function(params, backend) { if (!root) return {commands: []} - // Context for effect processing - var ctx = { - backend: backend, - camera: camera, - target_size: target_size, - commands: [], - target_counter: 0 - } - - // Process scene tree, handling effects on groups - var drawables = process_scene_tree(root, camera, ctx) + // Collect all drawables from scene tree + var drawables = _collect_drawables(root, camera, null, null, null, null) // Sort by layer, then by Y for depth sorting + log.console(drawables.length) drawables.sort(function(a, b) { - if (a.layer != b.layer) return a.layer - b.layer + var difflayer = a.layer - b.layer + if (difflayer != 0) return difflayer return b.world_y - a.world_y }) // Build render commands - var commands = ctx.commands + var commands = [] commands.push({cmd: 'begin_render', target: target, clear: clear_color}) commands.push({cmd: 'set_camera', camera: camera}) // Batch and emit draw commands - var batches = batch_drawables(drawables) + var batches = _batch_drawables(drawables) var current_scissor = null - for (var batch of batches) { - // Emit scissor command if changed - if (!rect_equal(current_scissor, batch.scissor)) { + for (var i = 0; i < batches.length; i++) { + var batch = batches[i] + + // Emit scissor if changed + if (!_rect_equal(current_scissor, batch.scissor)) { commands.push({cmd: 'scissor', rect: batch.scissor}) current_scissor = batch.scissor } @@ -102,15 +84,6 @@ film2d.render = function(params, backend) { texture: batch.texture, material: batch.material }) - } else if (batch.type == 'blit_target') { - // Effect island result - blit the target texture - commands.push({ - cmd: 'blit', - texture: batch.drawable.target, - target: target, - dst_rect: {x: 0, y: 0, width: batch.drawable.width, height: batch.drawable.height}, - filter: 'linear' - }) } } @@ -119,275 +92,20 @@ film2d.render = function(params, backend) { return {target: target, commands: commands} } -// Process scene tree, handling effects on groups -// Returns drawables for the current level, may emit commands for effect islands -function process_scene_tree(node, camera, ctx, parent_tint, parent_opacity, parent_scissor, parent_pos) { +// Collect drawables from scene tree +function _collect_drawables(node, camera, parent_tint, parent_opacity, parent_scissor, parent_pos) { if (!node) return [] - // Check if this node has effects that need special handling - if (node.effects && node.effects.length > 0) { - return process_effect_group(node, camera, ctx, parent_tint, parent_opacity, parent_scissor, parent_pos) - } - - // No effects - collect drawables normally - return collect_drawables(node, camera, parent_tint, parent_opacity, parent_scissor, parent_pos, ctx) -} - -// Process a group with effects - creates an "island" and applies effects sequentially -function process_effect_group(node, camera, ctx, parent_tint, parent_opacity, parent_scissor, parent_pos) { - var effects = node.effects - var backend = ctx.backend - var target_size = ctx.target_size - - // 1. Render all children to an initial content target - var current_target = backend.get_or_create_target(target_size.width, target_size.height, 'effect_start_' + ctx.target_counter++) - var child_drawables = collect_children_drawables(node, camera, parent_tint, parent_opacity, parent_scissor, parent_pos, ctx) - - render_drawables_to_target(child_drawables, current_target, camera, ctx) - - // 2. Apply effects sequentially, each one consuming the previous target and producing a new one - for (var effect of effects) { - var effect_type = effect.type - - if (effect_type == 'bloom') { - current_target = apply_bloom_effect(current_target, effect, ctx) - } else if (effect_type == 'mask') { - current_target = apply_mask_effect(current_target, effect, ctx) - } else if (effect_type == 'blur') { - current_target = apply_blur_effect(current_target, effect, ctx) - } - } - - // 3. Return a single drawable that blits the final result - return [{ - type: 'blit_target', - layer: node.layer || 0, - world_y: 0, - target: current_target, - width: target_size.width, - height: target_size.height - }] -} - -// Helper: Render a list of drawables to a specific target -function render_drawables_to_target(drawables, target, camera, ctx) { - ctx.commands.push({cmd: 'begin_render', target: target, clear: {r: 0, g: 0, b: 0, a: 0}}) - ctx.commands.push({cmd: 'set_camera', camera: camera}) - - var batches = batch_drawables(drawables) - for (var batch of batches) { - if (batch.type == 'sprite_batch') { - ctx.commands.push({ - cmd: 'draw_batch', - batch_type: 'sprites', - geometry: {sprites: batch.sprites}, - texture: batch.texture, - material: batch.material - }) - } else if (batch.type == 'text') { - ctx.commands.push({cmd: 'draw_text', drawable: batch.drawable}) - } else if (batch.type == 'particles') { - ctx.commands.push({ - cmd: 'draw_batch', - batch_type: 'particles', - geometry: {sprites: batch.sprites}, - texture: batch.texture, - material: batch.material - }) - } else if (batch.type == 'blit_target') { - ctx.commands.push({ - cmd: 'blit', - texture: batch.drawable.target, - target: target, - dst_rect: {x: 0, y: 0, width: batch.drawable.width, height: batch.drawable.height}, - filter: 'linear' - }) - } - } - ctx.commands.push({cmd: 'end_render'}) -} - -// Apply bloom effect to a source target, returning a new target -function apply_bloom_effect(src_target, effect, ctx) { - var backend = ctx.backend - var target_size = ctx.target_size - - var threshold_target = backend.get_or_create_target(target_size.width, target_size.height, 'bloom_threshold_' + ctx.target_counter++) - var blur_target_a = backend.get_or_create_target(target_size.width, target_size.height, 'bloom_blur_a_' + ctx.target_counter++) - var blur_target_b = backend.get_or_create_target(target_size.width, target_size.height, 'bloom_blur_b_' + ctx.target_counter++) - var output_target = backend.get_or_create_target(target_size.width, target_size.height, 'bloom_output_' + ctx.target_counter++) - - // Threshold pass - ctx.commands.push({ - cmd: 'shader_pass', - shader: 'threshold', - input: src_target, - output: threshold_target, - uniforms: {threshold: effect.threshold || 0.8, intensity: effect.intensity || 1.0} - }) - - // Blur passes - var blur_passes = effect.blur_passes || 3 - var texel_size = [1/target_size.width, 1/target_size.height] - var blur_src = threshold_target - - for (var i = 0; i < blur_passes; i++) { - ctx.commands.push({ - cmd: 'shader_pass', - shader: 'blur', - input: blur_src, - output: blur_target_a, - uniforms: {direction: [2, 0], texel_size: texel_size} - }) - ctx.commands.push({ - cmd: 'shader_pass', - shader: 'blur', - input: blur_target_a, - output: blur_target_b, - uniforms: {direction: [0, 2], texel_size: texel_size} - }) - blur_src = blur_target_b - } - - // Composite bloom back onto source - ctx.commands.push({ - cmd: 'composite_textures', - base: src_target, - overlay: blur_src, - output: output_target, - mode: 'add' - }) - - return output_target -} - -// Apply mask effect to a source target, returning a new target -function apply_mask_effect(src_target, effect, ctx) { - var backend = ctx.backend - var target_size = ctx.target_size - var camera = ctx.camera - - var mask_target = backend.get_or_create_target(target_size.width, target_size.height, 'mask_source_' + ctx.target_counter++) - var output_target = backend.get_or_create_target(target_size.width, target_size.height, 'mask_output_' + ctx.target_counter++) - - // Render mask source - var mask_source = effect.source - if (mask_source) { - var mask_drawables = collect_drawables(mask_source, camera, null, null, null, null, ctx) - render_drawables_to_target(mask_drawables, mask_target, camera, ctx) - } - - // Apply mask - ctx.commands.push({ - cmd: 'apply_mask', - content_texture: src_target, - mask_texture: mask_target, - output: output_target, - mode: effect.mode || 'alpha', - invert: effect.invert || false - }) - - return output_target -} - -// Apply blur effect to a source target, returning a new target -function apply_blur_effect(src_target, effect, ctx) { - var backend = ctx.backend - var target_size = ctx.target_size - - var blur_target_a = backend.get_or_create_target(target_size.width, target_size.height, 'blur_a_' + ctx.target_counter++) - var blur_target_b = backend.get_or_create_target(target_size.width, target_size.height, 'blur_b_' + ctx.target_counter++) - - // Blur passes - var blur_passes = effect.passes || 2 - var texel_size = [1/target_size.width, 1/target_size.height] - var blur_src = src_target - var blur_dst = blur_target_a - - for (var i = 0; i < blur_passes; i++) { - // Horizontal blur - ctx.commands.push({ - cmd: 'shader_pass', - shader: 'blur', - input: blur_src, - output: blur_dst, - uniforms: {direction: [2, 0], texel_size: texel_size} - }) - - // Swap targets - var tmp = blur_src - blur_src = blur_dst - blur_dst = (blur_dst == blur_target_a) ? blur_target_b : blur_target_a - - // Vertical blur - ctx.commands.push({ - cmd: 'shader_pass', - shader: 'blur', - input: blur_src, - output: blur_dst, - uniforms: {direction: [0, 2], texel_size: texel_size} - }) - - tmp = blur_src - blur_src = blur_dst - blur_dst = tmp - } - - return blur_src -} - -// Collect drawables from children only (not the node itself) -function collect_children_drawables(node, camera, parent_tint, parent_opacity, parent_scissor, parent_pos, ctx) { - var drawables = [] - parent_tint = parent_tint || [1, 1, 1, 1] parent_opacity = parent_opacity != null ? parent_opacity : 1 parent_pos = parent_pos || {x: 0, y: 0} - // Compute node position - var node_pos = node.pos || {x: 0, y: 0} - var abs_x = parent_pos.x + (node_pos.x != null ? node_pos.x : (node_pos[0] || 0)) - var abs_y = parent_pos.y + (node_pos.y != null ? node_pos.y : (node_pos[1] || 0)) - var current_pos = {x: abs_x, y: abs_y} - - // Compute inherited tint/opacity - var node_tint = node.tint || node.color - var world_tint = [ - parent_tint[0] * (node_tint ? (node_tint.r != null ? node_tint.r : node_tint[0] || 1) : 1), - parent_tint[1] * (node_tint ? (node_tint.g != null ? node_tint.g : node_tint[1] || 1) : 1), - parent_tint[2] * (node_tint ? (node_tint.b != null ? node_tint.b : node_tint[2] || 1) : 1), - parent_tint[3] * (node_tint ? (node_tint.a != null ? node_tint.a : node_tint[3] || 1) : 1) - ] - var world_opacity = parent_opacity * (node.opacity != null ? node.opacity : 1) - - // Recurse children - if (node.children) { - for (var child of node.children) { - var child_drawables = process_scene_tree(child, camera, ctx, world_tint, world_opacity, parent_scissor, current_pos) - for (var d of child_drawables) drawables.push(d) - } - } - - return drawables -} - -// ======================================================================== -// SCENE TREE TRAVERSAL -// ======================================================================== - -function collect_drawables(node, camera, parent_tint, parent_opacity, parent_scissor, parent_pos, ctx) { - if (!node) return [] - - parent_tint = parent_tint || [1, 1, 1, 1] - parent_opacity = parent_opacity != null ? parent_opacity : 1 - var drawables = [] // Compute absolute position - parent_pos = parent_pos || {x: 0, y: 0} var node_pos = node.pos || {x: 0, y: 0} - var abs_x = parent_pos.x + (node_pos.x != null ? node_pos.x : (node_pos[0] || 0)) - var abs_y = parent_pos.y + (node_pos.y != null ? node_pos.y : (node_pos[1] || 0)) + var abs_x = parent_pos.x + (node_pos.x != null ? node_pos.x : node_pos[0] || 0) + var abs_y = parent_pos.y + (node_pos.y != null ? node_pos.y : node_pos[1] || 0) var current_pos = {x: abs_x, y: abs_y} // Compute inherited tint/opacity @@ -404,7 +122,6 @@ function collect_drawables(node, camera, parent_tint, parent_opacity, parent_sci var current_scissor = parent_scissor if (node.scissor) { if (parent_scissor) { - // Intersect parent and node scissor var x1 = Math.max(parent_scissor.x, node.scissor.x) var y1 = Math.max(parent_scissor.y, node.scissor.y) var x2 = Math.min(parent_scissor.x + parent_scissor.width, node.scissor.x + node.scissor.width) @@ -417,8 +134,10 @@ function collect_drawables(node, camera, parent_tint, parent_opacity, parent_sci // Handle different node types if (node.type == 'sprite' || (node.image && !node.type)) { - var sprite_drawables = collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_scissor) - for (var d of sprite_drawables) drawables.push(d) + var sprite_drawables = _collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_scissor) + for (var i = 0; i < sprite_drawables.length; i++) { + drawables.push(sprite_drawables[i]) + } } if (node.type == 'text') { @@ -436,7 +155,7 @@ function collect_drawables(node, camera, parent_tint, parent_opacity, parent_sci outline_color: node.outline_color, anchor_x: node.anchor_x, anchor_y: node.anchor_y, - color: tint_to_color(world_tint, world_opacity), + color: _tint_to_color(world_tint, world_opacity), scissor: current_scissor }) } @@ -449,14 +168,15 @@ function collect_drawables(node, camera, parent_tint, parent_opacity, parent_sci pos: {x: abs_x, y: abs_y}, width: node.width || 1, height: node.height || 1, - color: tint_to_color(world_tint, world_opacity), + color: _tint_to_color(world_tint, world_opacity), scissor: current_scissor }) } if (node.type == 'particles' || node.particles) { var particles = node.particles || [] - for (var p of particles) { + for (var i = 0; i < particles.length; i++) { + var p = particles[i] var px = p.pos ? p.pos.x : 0 var py = p.pos ? p.pos.y : 0 @@ -471,7 +191,7 @@ function collect_drawables(node, camera, parent_tint, parent_opacity, parent_sci height: (node.height || 1) * (p.scale || 1), anchor_x: 0.5, anchor_y: 0.5, - color: p.color || tint_to_color(world_tint, world_opacity), + color: p.color || _tint_to_color(world_tint, world_opacity), material: node.material, scissor: current_scissor }) @@ -479,20 +199,19 @@ function collect_drawables(node, camera, parent_tint, parent_opacity, parent_sci } if (node.type == 'tilemap' || node.tiles) { - var tile_drawables = collect_tilemap(node, abs_x, abs_y, world_tint, world_opacity, current_scissor) - for (var d of tile_drawables) drawables.push(d) + var tile_drawables = _collect_tilemap(node, abs_x, abs_y, world_tint, world_opacity, current_scissor) + for (var i = 0; i < tile_drawables.length; i++) { + drawables.push(tile_drawables[i]) + } } - // Recurse children - use process_scene_tree if ctx available (handles effects) + // Recurse children if (node.children) { - for (var child of node.children) { - var child_drawables - if (ctx) { - child_drawables = process_scene_tree(child, camera, ctx, world_tint, world_opacity, current_scissor, current_pos) - } else { - child_drawables = collect_drawables(child, camera, world_tint, world_opacity, current_scissor, current_pos, null) + for (var i = 0; i < node.children.length; i++) { + var child_drawables = _collect_drawables(node.children[i], camera, world_tint, world_opacity, current_scissor, current_pos) + for (var j = 0; j < child_drawables.length; j++) { + drawables.push(child_drawables[j]) } - for (var d of child_drawables) drawables.push(d) } } @@ -500,7 +219,7 @@ function collect_drawables(node, camera, parent_tint, parent_opacity, parent_sci } // Collect sprite drawables (handles slice and tile modes) -function collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_scissor) { +function _collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_scissor) { var drawables = [] if (node.slice && node.tile) { @@ -511,9 +230,8 @@ function collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_s var h = node.height || 1 var ax = node.anchor_x || 0 var ay = node.anchor_y || 0 - var tint = tint_to_color(world_tint, world_opacity) + var tint = _tint_to_color(world_tint, world_opacity) - // Helper to add a sprite drawable function add_sprite(rect, uv) { drawables.push({ type: 'sprite', @@ -533,7 +251,6 @@ function collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_s }) } - // Helper to emit tiled area function emit_tiled(rect, uv, tile_size) { var tx = tile_size ? (tile_size.x || tile_size) : rect.width var ty = tile_size ? (tile_size.y || tile_size) : rect.height @@ -558,12 +275,11 @@ function collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_s } } - // Top-left of whole sprite var x0 = abs_x - w * ax var y0 = abs_y - h * ay if (node.slice) { - // 9-Slice logic + // 9-slice var s = node.slice var L = s.left != null ? s.left : (typeof s == 'number' ? s : 0) var R = s.right != null ? s.right : (typeof s == 'number' ? s : 0) @@ -571,51 +287,36 @@ function collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_s var B = s.bottom != null ? s.bottom : (typeof s == 'number' ? s : 0) var stretch = s.stretch != null ? s.stretch : node.stretch - var Sx = stretch != null ? (stretch.x || stretch) : w var Sy = stretch != null ? (stretch.y || stretch) : h - // World sizes of borders var WL = L * Sx var WR = R * Sx var HT = T * Sy var HB = B * Sy - - // Middle areas var WM = w - WL - WR var HM = h - HT - HB - - // UV mid dimensions var UM = 1 - L - R var VM = 1 - T - B - - // Natural tile sizes for middle parts var TW = stretch != null ? UM * Sx : WM var TH = stretch != null ? VM * Sy : HM - // TL + // TL, TM, TR add_sprite({x: x0, y: y0, width: WL, height: HT}, {x:0, y:0, width: L, height: T}) - // TM emit_tiled({x: x0 + WL, y: y0, width: WM, height: HT}, {x:L, y:0, width: UM, height: T}, {x: TW, y: HT}) - // TR add_sprite({x: x0 + WL + WM, y: y0, width: WR, height: HT}, {x: 1-R, y:0, width: R, height: T}) - // ML + // ML, MM, MR emit_tiled({x: x0, y: y0 + HT, width: WL, height: HM}, {x:0, y:T, width: L, height: VM}, {x: WL, y: TH}) - // MM emit_tiled({x: x0 + WL, y: y0 + HT, width: WM, height: HM}, {x:L, y:T, width: UM, height: VM}, {x: TW, y: TH}) - // MR emit_tiled({x: x0 + WL + WM, y: y0 + HT, width: WR, height: HM}, {x: 1-R, y:T, width: R, height: VM}, {x: WR, y: TH}) - // BL + // BL, BM, BR add_sprite({x: x0, y: y0 + HT + HM, width: WL, height: HB}, {x:0, y: 1-B, width: L, height: B}) - // BM emit_tiled({x: x0 + WL, y: y0 + HT + HM, width: WM, height: HB}, {x:L, y: 1-B, width: UM, height: B}, {x: TW, y: HB}) - // BR add_sprite({x: x0 + WL + WM, y: y0 + HT + HM, width: WR, height: HB}, {x: 1-R, y: 1-B, width: R, height: B}) } else if (node.tile) { - // Full sprite tiling emit_tiled({x: x0, y: y0, width: w, height: h}, {x:0, y:0, width: 1, height: 1}, node.tile) } else { // Normal sprite @@ -640,14 +341,14 @@ function collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_s } // Collect tilemap drawables -function collect_tilemap(node, abs_x, abs_y, world_tint, world_opacity, current_scissor) { +function _collect_tilemap(node, abs_x, abs_y, world_tint, world_opacity, current_scissor) { var drawables = [] var tiles = node.tiles || [] var offset_x = node.offset_x || 0 var offset_y = node.offset_y || 0 var scale_x = node.scale_x || 1 var scale_y = node.scale_y || 1 - var tint = tint_to_color(world_tint, world_opacity) + var tint = _tint_to_color(world_tint, world_opacity) for (var x = 0; x < tiles.length; x++) { if (!tiles[x]) continue @@ -679,7 +380,7 @@ function collect_tilemap(node, abs_x, abs_y, world_tint, world_opacity, current_ return drawables } -function tint_to_color(tint, opacity) { +function _tint_to_color(tint, opacity) { return { r: tint[0], g: tint[1], @@ -688,37 +389,36 @@ function tint_to_color(tint, opacity) { } } -// ======================================================================== -// BATCHING -// ======================================================================== - -function batch_drawables(drawables) { +// Batch drawables for efficient rendering +function _batch_drawables(drawables) { var batches = [] var current_batch = null + + var mat = {blend: 'alpha', sampler: 'nearest'} - for (var drawable of drawables) { + array.for(drawables, drawable => { if (drawable.type == 'sprite') { var texture = drawable.texture || drawable.image - var material = drawable.material || {blend: 'alpha', sampler: 'nearest'} + var material = drawable.material || mat var scissor = drawable.scissor - // Start new batch if texture/material/scissor changed - if (!current_batch || - current_batch.type != 'sprite_batch' || - current_batch.texture != texture || - !rect_equal(current_batch.scissor, scissor) || - !materials_equal(current_batch.material, material)) { + // Check if can merge with current batch + if (current_batch && + current_batch.type == 'sprite_batch' && + current_batch.texture == texture && + _rect_equal(current_batch.scissor, scissor) && + _materials_equal(current_batch.material, material)) { + current_batch.sprites.push(drawable) + } else { if (current_batch) batches.push(current_batch) current_batch = { type: 'sprite_batch', texture: texture, material: material, scissor: scissor, - sprites: [] + sprites: [drawable] } } - - current_batch.sprites.push(drawable) } else { // Non-sprite: flush batch, add individually if (current_batch) { @@ -727,20 +427,20 @@ function batch_drawables(drawables) { } batches.push({type: drawable.type, drawable: drawable, scissor: drawable.scissor}) } - } + }) if (current_batch) batches.push(current_batch) return batches } -function rect_equal(a, b) { +function _rect_equal(a, b) { if (!a && !b) return true if (!a || !b) return false return a.x == b.x && a.y == b.y && a.width == b.width && a.height == b.height } -function materials_equal(a, b) { +function _materials_equal(a, b) { if (!a || !b) return a == b return a.blend == b.blend && a.sampler == b.sampler && a.shader == b.shader } diff --git a/geometry.c b/geometry.c index 4227bfde..09450195 100644 --- a/geometry.c +++ b/geometry.c @@ -1200,6 +1200,91 @@ JSC_CCALL(geometry_weave, return result; ) +JSC_CCALL(geometry_sprite_vertices, + JSValue sprite = argv[0]; + + // Get sprite properties + double x, y, w, h, u0, v0, u1, v1; + JSValue x_val = JS_GetPropertyStr(js, sprite, "x"); + JSValue y_val = JS_GetPropertyStr(js, sprite, "y"); + JSValue w_val = JS_GetPropertyStr(js, sprite, "w"); + JSValue h_val = JS_GetPropertyStr(js, sprite, "h"); + JSValue u0_val = JS_GetPropertyStr(js, sprite, "u0"); + JSValue v0_val = JS_GetPropertyStr(js, sprite, "v0"); + JSValue u1_val = JS_GetPropertyStr(js, sprite, "u1"); + JSValue v1_val = JS_GetPropertyStr(js, sprite, "v1"); + JSValue c_val = JS_GetPropertyStr(js, sprite, "c"); + + JS_ToFloat64(js, &x, x_val); + JS_ToFloat64(js, &y, y_val); + JS_ToFloat64(js, &w, w_val); + JS_ToFloat64(js, &h, h_val); + JS_ToFloat64(js, &u0, u0_val); + JS_ToFloat64(js, &v0, v0_val); + JS_ToFloat64(js, &u1, u1_val); + JS_ToFloat64(js, &v1, v1_val); + + HMM_Vec4 c = {1.0f, 1.0f, 1.0f, 1.0f}; + if (!JS_IsNull(c_val)) { + c = js2color(js, c_val); + } + + JS_FreeValue(js, x_val); + JS_FreeValue(js, y_val); + JS_FreeValue(js, w_val); + JS_FreeValue(js, h_val); + JS_FreeValue(js, u0_val); + JS_FreeValue(js, v0_val); + JS_FreeValue(js, u1_val); + JS_FreeValue(js, v1_val); + JS_FreeValue(js, c_val); + + // 4 vertices * 8 floats per vertex (x, y, u, v, r, g, b, a) + float vertex_data[4 * 8]; + + // v0: bottom-left + vertex_data[0] = x; + vertex_data[1] = y; + vertex_data[2] = u0; + vertex_data[3] = v1; // Flip V + vertex_data[4] = c.r; + vertex_data[5] = c.g; + vertex_data[6] = c.b; + vertex_data[7] = c.a; + + // v1: bottom-right + vertex_data[8] = x + w; + vertex_data[9] = y; + vertex_data[10] = u1; + vertex_data[11] = v1; // Flip V + vertex_data[12] = c.r; + vertex_data[13] = c.g; + vertex_data[14] = c.b; + vertex_data[15] = c.a; + + // v2: top-right + vertex_data[16] = x + w; + vertex_data[17] = y + h; + vertex_data[18] = u1; + vertex_data[19] = v0; // Flip V + vertex_data[20] = c.r; + vertex_data[21] = c.g; + vertex_data[22] = c.b; + vertex_data[23] = c.a; + + // v3: top-left + vertex_data[24] = x; + vertex_data[25] = y + h; + vertex_data[26] = u0; + vertex_data[27] = v0; // Flip V + vertex_data[28] = c.r; + vertex_data[29] = c.g; + vertex_data[30] = c.b; + vertex_data[31] = c.a; + + return js_new_blob_stoned_copy(js, vertex_data, sizeof(vertex_data)); +) + static const JSCFunctionListEntry js_geometry_funcs[] = { MIST_FUNC_DEF(geometry, rect_intersection, 2), MIST_FUNC_DEF(geometry, rect_intersects, 2), @@ -1213,6 +1298,7 @@ static const JSCFunctionListEntry js_geometry_funcs[] = { MIST_FUNC_DEF(geometry, rect_transform, 2), MIST_FUNC_DEF(geometry, tilemap_to_data, 1), MIST_FUNC_DEF(geometry, sprites_to_data, 1), + MIST_FUNC_DEF(geometry, sprite_vertices, 1), MIST_FUNC_DEF(geometry, transform_xy_blob, 2), MIST_FUNC_DEF(gpu, tile, 4), MIST_FUNC_DEF(gpu, slice9, 4), diff --git a/graphics.cm b/graphics.cm index 1114a394..079bb2e9 100644 --- a/graphics.cm +++ b/graphics.cm @@ -158,7 +158,7 @@ function create_image(path){ def bytes = io.slurp(path); var ext = path.split('.').pop() - let raw = decode_image(bytes, ext); + var raw = decode_image(bytes, ext); /* ── Case A: single surface (from make_texture) ────────────── */ if(raw && raw.width && raw.pixels && !isa(raw, array)) { diff --git a/prosperon.cm b/prosperon.cm index 57deb02c..fb24d97f 100644 --- a/prosperon.cm +++ b/prosperon.cm @@ -9,7 +9,6 @@ var io = use('cellfs') var geometry = use('geometry') var blob = use('blob') var imgui = use('imgui') -var utf8 = use('utf8') var json = use('json') var os = use('os') @@ -223,7 +222,7 @@ function make_shader(sh_file) { var file = `shaders/${shader_type}/${sh_file}.${shader_type}` if (shader_cache[file]) return shader_cache[file] - var refl = json.decode(utf8.decode(io.slurp(`shaders/reflection/${sh_file}.json`))) + var refl = json.decode(text(io.slurp(`shaders/reflection/${sh_file}.json`))) var shader = { code: io.slurp(file), diff --git a/sdl_gpu.cm b/sdl_gpu.cm index fcd99103..211955d9 100644 --- a/sdl_gpu.cm +++ b/sdl_gpu.cm @@ -647,6 +647,7 @@ function _create_gpu_texture(w, h, pixels) { function _load_image_file(path) { var bytes = io.slurp(path) + var decoded if (!bytes) return null var ext = path.split('.').pop().toLowerCase() @@ -663,14 +664,14 @@ function _load_image_file(path) { surface = qoi.decode(bytes) break case 'gif': - var decoded = gif.decode(bytes) + decoded = gif.decode(bytes) if (decoded && decoded.frames && decoded.frames.length > 0) { surface = decoded.frames[0] } break case 'ase': case 'aseprite': - var decoded = aseprite.decode(bytes) + decoded = aseprite.decode(bytes) if (decoded && decoded.frames && decoded.frames.length > 0) { surface = decoded.frames[0] } @@ -786,19 +787,21 @@ function _build_sprite_vertices(sprites, camera) { var vertices_per_sprite = 4 var indices_per_sprite = 6 - var vertex_data = new blob_mod(sprites.length * vertices_per_sprite * floats_per_vertex * 4) - var index_data = new blob_mod(sprites.length * indices_per_sprite * 2) + var vertex_data = new blob_mod(sprites.length * vertices_per_sprite * floats_per_vertex * 32) + var index_data = geometry.make_quad_indices(sprites.length) var vertex_count = 0 + + var white = {r: 1, g: 1, b: 1, a: 1} - for (var s of sprites) { - var px = s.pos.x != null ? s.pos.x : (s.pos[0] || 0) - var py = s.pos.y != null ? s.pos.y : (s.pos[1] || 0) + array.for(sprites, s => { + var px = s.pos.x + var py = s.pos.y var w = s.width || 1 var h = s.height || 1 var ax = s.anchor_x || 0 var ay = s.anchor_y || 0 - var c = s.color || {r: 1, g: 1, b: 1, a: 1} + var c = s.color || white // Apply anchor var x = px - w * ax @@ -809,58 +812,11 @@ function _build_sprite_vertices(sprites, camera) { var v0 = s.uv_rect ? s.uv_rect.y : 0 var u1 = s.uv_rect ? (s.uv_rect.x + s.uv_rect.width) : 1 var v1 = s.uv_rect ? (s.uv_rect.y + s.uv_rect.height) : 1 - - // Quad vertices (bottom-left, bottom-right, top-right, top-left) - // v0: bottom-left - vertex_data.wf(x) - vertex_data.wf(y) - vertex_data.wf(u0) - vertex_data.wf(v1) // Flip V - vertex_data.wf(c.r) - vertex_data.wf(c.g) - vertex_data.wf(c.b) - vertex_data.wf(c.a) - - // v1: bottom-right - vertex_data.wf(x + w) - vertex_data.wf(y) - vertex_data.wf(u1) - vertex_data.wf(v1) // Flip V - vertex_data.wf(c.r) - vertex_data.wf(c.g) - vertex_data.wf(c.b) - vertex_data.wf(c.a) - - // v2: top-right - vertex_data.wf(x + w) - vertex_data.wf(y + h) - vertex_data.wf(u1) - vertex_data.wf(v0) // Flip V - vertex_data.wf(c.r) - vertex_data.wf(c.g) - vertex_data.wf(c.b) - vertex_data.wf(c.a) - - // v3: top-left - vertex_data.wf(x) - vertex_data.wf(y + h) - vertex_data.wf(u0) - vertex_data.wf(v0) // Flip V - vertex_data.wf(c.r) - vertex_data.wf(c.g) - vertex_data.wf(c.b) - vertex_data.wf(c.a) - - // Indices (two triangles) - index_data.w16(vertex_count + 0) - index_data.w16(vertex_count + 1) - index_data.w16(vertex_count + 2) - index_data.w16(vertex_count + 0) - index_data.w16(vertex_count + 2) - index_data.w16(vertex_count + 3) - - vertex_count += 4 - } + + // call to implement + var verts = geometry.sprite_vertices({x,y,w,h,u0,v0,u1,v1,c}) + vertex_data.write_blob(verts) + }) return { vertices: stone(vertex_data), @@ -1042,6 +998,7 @@ function _execute_commands(commands, window_size) { var current_target = null var current_camera = null var pending_draws = [] + var target // Cache swapchain texture for the duration of this command buffer var _swapchain_tex = null @@ -1067,7 +1024,7 @@ function _execute_commands(commands, window_size) { } // Start new pass - var target = cmd.target + target = cmd.target var clear = cmd.clear if (target == 'screen') { @@ -1209,7 +1166,7 @@ function _execute_commands(commands, window_size) { imgui_mod.prepare(cmd_buffer) // Restart pass to the same target for rendering - var target = cmd.target + target = cmd.target var swap_tex = null if (target == 'screen') { swap_tex = get_swapchain_tex() diff --git a/sprite.cm b/sprite.cm index 49139876..56ad7bc0 100644 --- a/sprite.cm +++ b/sprite.cm @@ -1,6 +1,10 @@ -// sprite +// sprite.cm - Sprite node factory +// +// Returns a function that creates sprite instances via meme() + var sprite = { - pos: {x:0, y:0}, + type: 'sprite', + pos: null, layer: 0, image: null, width: 1, @@ -9,10 +13,95 @@ var sprite = { anchor_y: 0, scale_x: 1, scale_y: 1, - color: {r:1, g:1, b:1, a:1}, - animation: null, // 'walk', 'attack', etc + color: null, + uv_rect: null, + slice: null, + tile: null, + material: null, + animation: null, frame: 0, - opacity: 1 + opacity: 1, + + // Dirty tracking + dirty: 7, // DIRTY.ALL + + // Cached geometry (for retained mode) + geom_cache: null, + + // Setters that mark dirty + set_pos: function(x, y) { + if (!this.pos) this.pos = {x: 0, y: 0} + if (this.pos.x == x && this.pos.y == y) return this + this.pos.x = x + this.pos.y = y + this.dirty |= 1 // TRANSFORM + return this + }, + + set_image: function(img) { + if (this.image == img) return this + this.image = img + this.dirty |= 2 // CONTENT + return this + }, + + set_size: function(w, h) { + if (this.width == w && this.height == h) return this + this.width = w + this.height = h + this.dirty |= 2 // CONTENT + return this + }, + + set_anchor: function(x, y) { + if (this.anchor_x == x && this.anchor_y == y) return this + this.anchor_x = x + this.anchor_y = y + this.dirty |= 1 // TRANSFORM + return this + }, + + set_color: function(r, g, b, a) { + if (!this.color) this.color = {r: 1, g: 1, b: 1, a: 1} + if (this.color.r == r && this.color.g == g && this.color.b == b && this.color.a == a) return this + this.color.r = r + this.color.g = g + this.color.b = b + this.color.a = a + this.dirty |= 2 // CONTENT + return this + }, + + set_opacity: function(o) { + if (this.opacity == o) return this + this.opacity = o + this.dirty |= 2 // CONTENT + return this + } } -return sprite \ No newline at end of file +// Factory function +return function(props) { + var s = meme(sprite) + s.pos = {x: 0, y: 0} + s.color = {r: 1, g: 1, b: 1, a: 1} + s.dirty = 7 + + if (props) { + for (var k in props) { + if (k == 'pos' && props.pos) { + s.pos.x = props.pos.x || 0 + s.pos.y = props.pos.y || 0 + } else if (k == 'color' && props.color) { + s.color.r = props.color.r != null ? props.color.r : 1 + s.color.g = props.color.g != null ? props.color.g : 1 + s.color.b = props.color.b != null ? props.color.b : 1 + s.color.a = props.color.a != null ? props.color.a : 1 + } else { + s[k] = props[k] + } + } + } + + return s +} \ No newline at end of file