sprite vert

This commit is contained in:
2025-12-29 20:46:42 -06:00
parent 837659fcdc
commit 813d4e771c
8 changed files with 511 additions and 866 deletions

View File

@@ -244,13 +244,7 @@ clay.layout = function(fn, size) {
} }
function process_configs(configs) { function process_configs(configs) {
// Merge array of configs from right to left (right overrides left) return meme(base_config, ...configs)
// And merge with base_config
var res = meme(base_config)
for (var c of configs) {
if (c) res = meme(res, c)
}
return res
} }
function push_node(configs, contain_mode) { function push_node(configs, contain_mode) {

View File

@@ -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. // The compositor is the SINGLE OWNER of all effects.
// It handles: // It takes scene descriptions and produces abstract render plans.
// - Renderer selection per layer (film2d, forward3d, imgui, retro3d...) //
// - Camera + viewport binding per pass // Architecture:
// - Target ownership (composition root, fixed resolution, effect islands) // Scene Tree (Retained) -> Compositor (Effect orchestration) -> Render Plan -> Backend
// - Presentation modes (stretch, letterbox, integer_scale, etc.)
// - Global post effects (CRT, etc.)
// - Layer stacking order
// //
// The compositor does NOT know about sprites/tilemaps/text - that's renderer territory. // 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.
// ======================================================================== var effects_mod = use('effects')
// 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 compositor = {} var compositor = {}
// Default renderer capabilities (conservative - requires islands for everything) // Presentation modes
var DEFAULT_CAPS = { compositor.PRESENTATION = {
supports_mask_stencil: true, DISABLED: 'disabled',
supports_mask_alpha: true, STRETCH: 'stretch',
supports_bloom: true, LETTERBOX: 'letterbox',
supports_blur: true OVERSCAN: 'overscan',
INTEGER_SCALE: 'integer_scale'
} }
// ======================================================================== // Blend modes
// COMPILATION: Turn composition tree into render plan compositor.BLEND = {
// ======================================================================== REPLACE: 'replace',
OVER: 'over',
ADD: 'add'
}
// Compile a composition into a render plan // Compile a composition tree into a render plan
// comp: composition description (the DSL)
// renderers: map of renderer_type -> renderer module
// backend: the render backend (sdl_gpu, etc.)
compositor.compile = function(comp, renderers, backend) { compositor.compile = function(comp, renderers, backend) {
var ctx = { var ctx = {
renderers: renderers, renderers: renderers,
backend: backend, backend: backend,
passes: [], passes: [],
targets: {}, targets: {},
persistent_targets: {},
target_counter: 0, 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 // Get screen size from backend
ctx.screen_size = backend.get_window_size() || {width: 1280, height: 720} 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.width) ctx.screen_size.width = 1280
if (!ctx.screen_size.height) ctx.screen_size.height = 720 if (!ctx.screen_size.height) ctx.screen_size.height = 720
// Compile the composition tree // Normalize to w/h
var result = compile_node(comp, ctx, null, ctx.screen_size) 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 { return {
passes: ctx.passes, passes: ctx.passes,
targets: ctx.targets, 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 // Compile a single node
function compile_node(node, ctx, parent_target, parent_size) { function _compile_node(node, ctx, parent_target, parent_size) {
if (!node) return {output: null} if (!node) return {output: null}
var node_type = node.type var node_type = node.type
// Determine if this node owns a target
var owns_target = false var owns_target = false
var target = parent_target var target = parent_target
var target_size = parent_size var target_size = {w: parent_size.w || parent_size.width, h: parent_size.h || parent_size.height}
// Target ownership rules:
// 1. Composition root (target: "screen")
// 2. Node has explicit resolution
// 3. Node has effects that require isolation
// Determine target ownership
if (node.target == 'screen') { if (node.target == 'screen') {
owns_target = true owns_target = true
target = 'screen' target = 'screen'
target_size = ctx.screen_size target_size = {w: ctx.screen_size.w, h: ctx.screen_size.h}
} else if (node.resolution) { } else if (node.resolution) {
owns_target = true 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_size = {w: node.resolution.w || node.resolution.width, h: node.resolution.h || node.resolution.height}
target = allocate_target(ctx, target_size, node.name || 'res_target') target = ctx.alloc_target(target_size.w, target_size.h, node.name || 'res_target')
} else if (node.effects && effects_require_island(node.effects, ctx)) { } else if (node.effects && _effects_require_target(node.effects, ctx)) {
owns_target = true owns_target = true
target_size = parent_size || ctx.screen_size target = ctx.alloc_target(target_size.w, target_size.h, node.name || 'effect_target')
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')
} }
// Handle clear for target owners ctx.target_size = target_size
// Handle clear
if (owns_target && node.clear) { if (owns_target && node.clear) {
ctx.passes.push({ ctx.passes.push({
type: 'clear', 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') { 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') { } else if (node_type == 'group') {
// Group with potential effects - process layers return _compile_group(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)) {
} else if (is_renderer_type(node_type)) { return _compile_renderer(node, ctx, target, target_size, parent_target, parent_size)
// Renderer layer (film2d, forward3d, imgui, etc.)
return compile_renderer_layer(node, ctx, target, target_size, parent_target, parent_size)
} else if (node_type == 'imgui') { } 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({ ctx.passes.push({
type: 'imgui', type: 'imgui',
target: target, target: target,
@@ -151,57 +132,49 @@ function compile_node(node, ctx, parent_target, parent_size) {
return {output: target} return {output: target}
} }
// Compile a composition (root or nested) // Compile composition root
function compile_composition(node, ctx, target, target_size) { function _compile_composition(node, ctx, target, target_size) {
var layers = node.layers || [] var layers = node.layers || []
for (var i = 0; i < layers.length; i++) { for (var i = 0; i < layers.length; i++) {
var layer = layers[i] var layer = layers[i]
var is_first = (i == 0)
// Set default blend based on position
if (!layer.blend) { 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} return {output: target}
} }
// Compile a group (may have effects) // Compile group with effects
function compile_group(node, ctx, target, target_size, parent_target, parent_size) { function _compile_group(node, ctx, target, target_size, parent_target, parent_size) {
var layers = node.layers || [] var layers = node.layers || []
var original_target = target var original_target = target
// Process child layers into this target // Process child layers
for (var i = 0; i < layers.length; i++) { for (var i = 0; i < layers.length; i++) {
var layer = layers[i] var layer = layers[i]
var is_first = (i == 0)
if (!layer.blend) { 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 // Apply effects - this is where the compositor owns all effect logic
if (node.effects) { if (node.effects && node.effects.length > 0) {
for (var effect of node.effects) { for (var j = 0; j < node.effects.length; j++) {
effect._node_id = node.name || `node_${ctx.target_counter}` var effect = node.effects[j]
target = compile_effect(effect, ctx, target, target_size) 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 var needs_composite = (original_target != parent_target || target != original_target) && parent_target
if (needs_composite) { if (needs_composite) {
var presentation = node.presentation || 'disabled' var presentation = node.presentation || 'disabled'
var blend = node.blend || 'over' var blend = node.blend || 'over'
// If parent is screen, use blit_to_screen pass type
if (parent_target == 'screen') { if (parent_target == 'screen') {
ctx.passes.push({ ctx.passes.push({
type: 'blit_to_screen', type: 'blit_to_screen',
@@ -228,28 +201,28 @@ function compile_group(node, ctx, target, target_size, parent_target, parent_siz
return {output: target} return {output: target}
} }
// Compile a renderer layer (film2d, forward3d, etc.) // Compile renderer layer (film2d, forward3d, etc.)
function compile_renderer_layer(node, ctx, target, target_size, parent_target, parent_size) { function _compile_renderer(node, ctx, target, target_size, parent_target, parent_size) {
var renderer_type = node.type var renderer_type = node.type
var renderer = ctx.renderers[renderer_type] var renderer = ctx.renderers[renderer_type]
if (!renderer) { if (!renderer) {
log.console(`compositor: Unknown renderer type: ${renderer_type}`) log.console(`compositor: Unknown renderer: ${renderer_type}`)
return {output: target} return {output: target}
} }
// Determine if this layer owns its own target
var layer_target = target var layer_target = target
var layer_size = target_size var layer_size = target_size
var owns_target = false var owns_target = false
// Check for resolution override
if (node.resolution) { if (node.resolution) {
owns_target = true owns_target = true
layer_size = {width: node.resolution.w, height: node.resolution.h} layer_size = {w: node.resolution.w, h: node.resolution.h}
layer_target = allocate_target(ctx, layer_size, node.name || renderer_type + '_target') 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) { if (owns_target && node.clear) {
ctx.passes.push({ ctx.passes.push({
type: 'clear', type: 'clear',
@@ -270,7 +243,7 @@ function compile_renderer_layer(node, ctx, target, target_size, parent_target, p
clear: owns_target ? node.clear : null 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) { if (owns_target && parent_target) {
var presentation = node.presentation || 'disabled' var presentation = node.presentation || 'disabled'
var blend = node.blend || 'over' 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} return {output: layer_target}
} }
// Compile an effect // Compile an effect using the effect registry
function compile_effect(effect, ctx, input_target, target_size) { function _compile_effect(effect, ctx, input_target, target_size, node_id) {
var effect_type = effect.type var effect_type = effect.type
var effect_def = effects_mod.get(effect_type)
if (effect_type == 'crt') { if (!effect_def) {
var output = allocate_target(ctx, target_size, 'crt_output') log.console(`compositor: Unknown effect: ${effect_type}`)
ctx.passes.push({ return input_target
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_type == 'bloom') { // Build effect context
return compile_bloom_effect(effect, ctx, input_target, target_size) var effect_ctx = {
} backend: ctx.backend,
target_size: {w: target_size.w, h: target_size.h},
if (effect_type == 'mask') { alloc_target: function(w, h, hint) {
return compile_mask_effect(effect, ctx, input_target, target_size) return ctx.alloc_target(w, h, hint)
} },
get_persistent_target: function(key, w, h) {
if (effect_type == 'blur') { return ctx.get_persistent_target(node_id + '_' + key, w, h)
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
} }
} }
// 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 return false
} }
// ======================================================================== // Check if type is a renderer
// PRESENTATION HELPERS 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) { compositor.calculate_presentation_rect = function(source_size, dest_size, mode) {
var sw = source_size.width var sw = source_size.w || source_size.width
var sh = source_size.height var sh = source_size.h || source_size.height
var dw = dest_size.width var dw = dest_size.w || dest_size.width
var dh = dest_size.height 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 == 'disabled') {
if (mode == 'stretch') return {x: 0, y: 0, width: dw, height: dh} 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 src_aspect = sw / sh
var dst_aspect = dw / dh var dst_aspect = dw / dh
if (mode == 'letterbox') { if (mode == 'letterbox') {
var scale = src_aspect > dst_aspect var scale = src_aspect > dst_aspect ? dw / sw : dh / sh
? dw / sw
: dh / sh
var w = sw * scale var w = sw * scale
var h = sh * scale var h = sh * scale
return {x: (dw - w) / 2, y: (dh - h) / 2, width: w, height: h} return {x: (dw - w) / 2, y: (dh - h) / 2, width: w, height: h}
} }
if (mode == 'overscan') { if (mode == 'overscan') {
var scale = src_aspect > dst_aspect var scale = src_aspect > dst_aspect ? dh / sh : dw / sw
? dh / sh
: dw / sw
var w = sw * scale var w = sw * scale
var h = sh * scale var h = sh * scale
return {x: (dw - w) / 2, y: (dh - h) / 2, width: w, height: h} 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} 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} return {x: 0, y: 0, width: sw, height: sh}
} }
// ========================================================================
// PLAN EXECUTION
// ========================================================================
// Execute a compiled render plan // Execute a compiled render plan
compositor.execute = function(plan, renderers, backend) { compositor.execute = function(plan, renderers, backend) {
var target_cache = {} var target_cache = {}
// Pre-allocate all targets // Pre-allocate targets
for (var key in plan.targets) { for (var key in plan.targets) {
var spec = plan.targets[key] 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 // Resolve target references
@@ -616,18 +429,21 @@ compositor.execute = function(plan, renderers, backend) {
// Execute passes // Execute passes
var commands = [] var commands = []
for (var pass of plan.passes) { for (var i = 0; i < plan.passes.length; i++) {
var pass_commands = execute_pass(pass, renderers, backend, resolve_target) var pass = plan.passes[i]
for (var cmd of pass_commands) { var pass_cmds = _execute_pass(pass, renderers, backend, resolve_target, compositor)
commands.push(cmd) for (var j = 0; j < pass_cmds.length; j++) {
commands.push(pass_cmds[j])
} }
} }
return {commands: commands} 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 commands = []
var source
switch (pass.type) { switch (pass.type) {
case 'clear': case 'clear':
@@ -640,7 +456,7 @@ function execute_pass(pass, renderers, backend, resolve_target) {
var renderer = renderers[pass.renderer] var renderer = renderers[pass.renderer]
if (renderer && renderer.render) { if (renderer && renderer.render) {
var target = resolve_target(pass.target) var target = resolve_target(pass.target)
var render_result = renderer.render({ var result = renderer.render({
root: pass.root, root: pass.root,
camera: pass.camera, camera: pass.camera,
target: target, target: target,
@@ -648,9 +464,9 @@ function execute_pass(pass, renderers, backend, resolve_target) {
blend: pass.blend, blend: pass.blend,
clear: pass.clear clear: pass.clear
}, backend) }, backend)
if (render_result && render_result.commands) { if (result && result.commands) {
for (var cmd of render_result.commands) { for (var i = 0; i < result.commands.length; i++) {
commands.push(cmd) commands.push(result.commands[i])
} }
} }
} }
@@ -659,10 +475,10 @@ function execute_pass(pass, renderers, backend, resolve_target) {
case 'shader_pass': case 'shader_pass':
var input = resolve_target(pass.input) var input = resolve_target(pass.input)
var output = resolve_target(pass.output) var output = resolve_target(pass.output)
var extra_inputs = [] var extra = []
if (pass.extra_inputs) { if (pass.extra_inputs) {
for (var t of pass.extra_inputs) { for (var i = 0; i < pass.extra_inputs.length; i++) {
extra_inputs.push(resolve_target(t)) extra.push(resolve_target(pass.extra_inputs[i]))
} }
} }
commands.push({ commands.push({
@@ -670,15 +486,15 @@ function execute_pass(pass, renderers, backend, resolve_target) {
shader: pass.shader, shader: pass.shader,
input: input, input: input,
output: output, output: output,
extra_inputs: extra_inputs, extra_inputs: extra,
uniforms: pass.uniforms uniforms: pass.uniforms
}) })
break break
case 'composite': case 'composite': {
var source = resolve_target(pass.source) source = resolve_target(pass.source)
var dest = resolve_target(pass.dest) 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) { if (pass.pos) {
rect.x += (pass.pos.x || 0) rect.x += (pass.pos.x || 0)
rect.y += (pass.pos.y || 0) rect.y += (pass.pos.y || 0)
@@ -691,11 +507,12 @@ function execute_pass(pass, renderers, backend, resolve_target) {
dst_rect: rect, dst_rect: rect,
filter: filter filter: filter
}) })
}
break break
case 'blit_to_screen': case 'blit_to_screen': {
var source = resolve_target(pass.source) source = resolve_target(pass.source)
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) { if (pass.pos) {
rect.x += (pass.pos.x || 0) rect.x += (pass.pos.x || 0)
rect.y += (pass.pos.y || 0) rect.y += (pass.pos.y || 0)
@@ -708,9 +525,10 @@ function execute_pass(pass, renderers, backend, resolve_target) {
dst_rect: rect, dst_rect: rect,
filter: filter filter: filter
}) })
}
break break
case 'composite_textures': case 'composite_textures': {
var base = resolve_target(pass.base) var base = resolve_target(pass.base)
var overlay = resolve_target(pass.overlay) var overlay = resolve_target(pass.overlay)
var output = resolve_target(pass.output) var output = resolve_target(pass.output)
@@ -721,9 +539,10 @@ function execute_pass(pass, renderers, backend, resolve_target) {
output: output, output: output,
mode: pass.mode || 'over' mode: pass.mode || 'over'
}) })
}
break break
case 'apply_mask': case 'apply_mask': {
var content = resolve_target(pass.content) var content = resolve_target(pass.content)
var mask = resolve_target(pass.mask) var mask = resolve_target(pass.mask)
var output = resolve_target(pass.output) var output = resolve_target(pass.output)
@@ -735,37 +554,38 @@ function execute_pass(pass, renderers, backend, resolve_target) {
mode: pass.mode, mode: pass.mode,
invert: pass.invert invert: pass.invert
}) })
}
break break
case 'imgui': case 'imgui': {
commands.push({ commands.push({
cmd: 'imgui', cmd: 'imgui',
draw: pass.draw, draw: pass.draw,
rect: pass.rect, rect: pass.rect,
target: resolve_target(pass.target) target: resolve_target(pass.target)
}) })
}
break break
case 'render_mask_source': case 'render_mask_source': {
// This would render the mask source node
// For now, delegate to film2d renderer
var target = resolve_target(pass.target) var target = resolve_target(pass.target)
var renderer = renderers['film2d'] var renderer = renderers['film2d']
if (renderer && renderer.render) { if (renderer && renderer.render) {
var render_result = renderer.render({ var result = renderer.render({
root: pass.source, 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: target,
target_size: pass.target_size, target_size: pass.target_size,
blend: 'replace', blend: 'replace',
clear: {r: 0, g: 0, b: 0, a: 0} clear: {r: 0, g: 0, b: 0, a: 0}
}, backend) }, backend)
if (render_result && render_result.commands) { if (result && result.commands) {
for (var cmd of render_result.commands) { for (var i = 0; i < result.commands.length; i++) {
commands.push(cmd) commands.push(result.commands[i])
} }
} }
} }
}
break break
} }

444
film2d.cm
View File

@@ -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. // Handles scene tree traversal, sorting, batching, and draw command emission.
// This is the "how to draw a plate" module - it knows about sprites, tilemaps, // This is the "how to draw a 2D plate" module.
// text, particles, etc.
// //
// 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. // and it produces draw commands.
// //
// Scene-level effects (bloom, mask, blur on groups) are handled here: // This module does NOT handle effects - that's compositor territory.
// - Effects that can be done inline (stencil mask) are done without extra targets // It only knows about sprites, tilemaps, text, particles, and rects.
// - 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.)
var film2d = {} var film2d = {}
// Renderer capabilities - what this renderer can do inline vs requiring islands // Renderer capabilities
film2d.capabilities = { film2d.capabilities = {
supports_mask_stencil: true, // Hard masks via stencil (no target needed) supports_mask_stencil: true,
supports_mask_alpha: true, // Soft masks (needs target) supports_mask_alpha: true,
supports_bloom: true, // Bloom effect (needs target) supports_bloom: true,
supports_blur: true // Blur effect (needs target) supports_blur: true
} }
// ======================================================================== // Main render function
// 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.)
film2d.render = function(params, backend) { film2d.render = function(params, backend) {
var root = params.root var root = params.root
var camera = params.camera var camera = params.camera
@@ -42,36 +29,31 @@ film2d.render = function(params, backend) {
if (!root) return {commands: []} if (!root) return {commands: []}
// Context for effect processing // Collect all drawables from scene tree
var ctx = { var drawables = _collect_drawables(root, camera, null, null, null, null)
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)
// Sort by layer, then by Y for depth sorting // Sort by layer, then by Y for depth sorting
log.console(drawables.length)
drawables.sort(function(a, b) { 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 return b.world_y - a.world_y
}) })
// Build render commands // Build render commands
var commands = ctx.commands var commands = []
commands.push({cmd: 'begin_render', target: target, clear: clear_color}) commands.push({cmd: 'begin_render', target: target, clear: clear_color})
commands.push({cmd: 'set_camera', camera: camera}) commands.push({cmd: 'set_camera', camera: camera})
// Batch and emit draw commands // Batch and emit draw commands
var batches = batch_drawables(drawables) var batches = _batch_drawables(drawables)
var current_scissor = null var current_scissor = null
for (var batch of batches) { for (var i = 0; i < batches.length; i++) {
// Emit scissor command if changed var batch = batches[i]
if (!rect_equal(current_scissor, batch.scissor)) {
// Emit scissor if changed
if (!_rect_equal(current_scissor, batch.scissor)) {
commands.push({cmd: 'scissor', rect: batch.scissor}) commands.push({cmd: 'scissor', rect: batch.scissor})
current_scissor = batch.scissor current_scissor = batch.scissor
} }
@@ -102,15 +84,6 @@ film2d.render = function(params, backend) {
texture: batch.texture, texture: batch.texture,
material: batch.material 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} return {target: target, commands: commands}
} }
// Process scene tree, handling effects on groups // Collect drawables from scene tree
// Returns drawables for the current level, may emit commands for effect islands function _collect_drawables(node, camera, parent_tint, parent_opacity, parent_scissor, parent_pos) {
function process_scene_tree(node, camera, ctx, parent_tint, parent_opacity, parent_scissor, parent_pos) {
if (!node) return [] 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_tint = parent_tint || [1, 1, 1, 1]
parent_opacity = parent_opacity != null ? parent_opacity : 1 parent_opacity = parent_opacity != null ? parent_opacity : 1
parent_pos = parent_pos || {x: 0, y: 0} 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 = [] var drawables = []
// Compute absolute position // Compute absolute position
parent_pos = parent_pos || {x: 0, y: 0}
var node_pos = node.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_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_y = parent_pos.y + (node_pos.y != null ? node_pos.y : node_pos[1] || 0)
var current_pos = {x: abs_x, y: abs_y} var current_pos = {x: abs_x, y: abs_y}
// Compute inherited tint/opacity // Compute inherited tint/opacity
@@ -404,7 +122,6 @@ function collect_drawables(node, camera, parent_tint, parent_opacity, parent_sci
var current_scissor = parent_scissor var current_scissor = parent_scissor
if (node.scissor) { if (node.scissor) {
if (parent_scissor) { if (parent_scissor) {
// Intersect parent and node scissor
var x1 = Math.max(parent_scissor.x, node.scissor.x) var x1 = Math.max(parent_scissor.x, node.scissor.x)
var y1 = Math.max(parent_scissor.y, node.scissor.y) 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) 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 // Handle different node types
if (node.type == 'sprite' || (node.image && !node.type)) { if (node.type == 'sprite' || (node.image && !node.type)) {
var sprite_drawables = collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_scissor) 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) for (var i = 0; i < sprite_drawables.length; i++) {
drawables.push(sprite_drawables[i])
}
} }
if (node.type == 'text') { if (node.type == 'text') {
@@ -436,7 +155,7 @@ function collect_drawables(node, camera, parent_tint, parent_opacity, parent_sci
outline_color: node.outline_color, outline_color: node.outline_color,
anchor_x: node.anchor_x, anchor_x: node.anchor_x,
anchor_y: node.anchor_y, anchor_y: node.anchor_y,
color: tint_to_color(world_tint, world_opacity), color: _tint_to_color(world_tint, world_opacity),
scissor: current_scissor 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}, pos: {x: abs_x, y: abs_y},
width: node.width || 1, width: node.width || 1,
height: node.height || 1, height: node.height || 1,
color: tint_to_color(world_tint, world_opacity), color: _tint_to_color(world_tint, world_opacity),
scissor: current_scissor scissor: current_scissor
}) })
} }
if (node.type == 'particles' || node.particles) { if (node.type == 'particles' || node.particles) {
var 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 px = p.pos ? p.pos.x : 0
var py = p.pos ? p.pos.y : 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), height: (node.height || 1) * (p.scale || 1),
anchor_x: 0.5, anchor_x: 0.5,
anchor_y: 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, material: node.material,
scissor: current_scissor scissor: current_scissor
}) })
@@ -479,20 +199,19 @@ function collect_drawables(node, camera, parent_tint, parent_opacity, parent_sci
} }
if (node.type == 'tilemap' || node.tiles) { if (node.type == 'tilemap' || node.tiles) {
var tile_drawables = collect_tilemap(node, abs_x, abs_y, world_tint, world_opacity, current_scissor) 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) 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) { if (node.children) {
for (var child of node.children) { for (var i = 0; i < node.children.length; i++) {
var child_drawables var child_drawables = _collect_drawables(node.children[i], camera, world_tint, world_opacity, current_scissor, current_pos)
if (ctx) { for (var j = 0; j < child_drawables.length; j++) {
child_drawables = process_scene_tree(child, camera, ctx, world_tint, world_opacity, current_scissor, current_pos) drawables.push(child_drawables[j])
} else {
child_drawables = collect_drawables(child, camera, world_tint, world_opacity, current_scissor, current_pos, null)
} }
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) // 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 = [] var drawables = []
if (node.slice && node.tile) { 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 h = node.height || 1
var ax = node.anchor_x || 0 var ax = node.anchor_x || 0
var ay = node.anchor_y || 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) { function add_sprite(rect, uv) {
drawables.push({ drawables.push({
type: 'sprite', 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) { function emit_tiled(rect, uv, tile_size) {
var tx = tile_size ? (tile_size.x || tile_size) : rect.width var tx = tile_size ? (tile_size.x || tile_size) : rect.width
var ty = tile_size ? (tile_size.y || tile_size) : rect.height 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 x0 = abs_x - w * ax
var y0 = abs_y - h * ay var y0 = abs_y - h * ay
if (node.slice) { if (node.slice) {
// 9-Slice logic // 9-slice
var s = node.slice var s = node.slice
var L = s.left != null ? s.left : (typeof s == 'number' ? s : 0) var L = s.left != null ? s.left : (typeof s == 'number' ? s : 0)
var R = s.right != null ? s.right : (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 B = s.bottom != null ? s.bottom : (typeof s == 'number' ? s : 0)
var stretch = s.stretch != null ? s.stretch : node.stretch var stretch = s.stretch != null ? s.stretch : node.stretch
var Sx = stretch != null ? (stretch.x || stretch) : w var Sx = stretch != null ? (stretch.x || stretch) : w
var Sy = stretch != null ? (stretch.y || stretch) : h var Sy = stretch != null ? (stretch.y || stretch) : h
// World sizes of borders
var WL = L * Sx var WL = L * Sx
var WR = R * Sx var WR = R * Sx
var HT = T * Sy var HT = T * Sy
var HB = B * Sy var HB = B * Sy
// Middle areas
var WM = w - WL - WR var WM = w - WL - WR
var HM = h - HT - HB var HM = h - HT - HB
// UV mid dimensions
var UM = 1 - L - R var UM = 1 - L - R
var VM = 1 - T - B var VM = 1 - T - B
// Natural tile sizes for middle parts
var TW = stretch != null ? UM * Sx : WM var TW = stretch != null ? UM * Sx : WM
var TH = stretch != null ? VM * Sy : HM 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}) 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}) 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}) 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}) 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}) 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}) 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}) 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}) 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}) 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) { } 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) emit_tiled({x: x0, y: y0, width: w, height: h}, {x:0, y:0, width: 1, height: 1}, node.tile)
} else { } else {
// Normal sprite // Normal sprite
@@ -640,14 +341,14 @@ function collect_sprite(node, abs_x, abs_y, world_tint, world_opacity, current_s
} }
// Collect tilemap drawables // 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 drawables = []
var tiles = node.tiles || [] var tiles = node.tiles || []
var offset_x = node.offset_x || 0 var offset_x = node.offset_x || 0
var offset_y = node.offset_y || 0 var offset_y = node.offset_y || 0
var scale_x = node.scale_x || 1 var scale_x = node.scale_x || 1
var scale_y = node.scale_y || 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++) { for (var x = 0; x < tiles.length; x++) {
if (!tiles[x]) continue if (!tiles[x]) continue
@@ -679,7 +380,7 @@ function collect_tilemap(node, abs_x, abs_y, world_tint, world_opacity, current_
return drawables return drawables
} }
function tint_to_color(tint, opacity) { function _tint_to_color(tint, opacity) {
return { return {
r: tint[0], r: tint[0],
g: tint[1], g: tint[1],
@@ -688,37 +389,36 @@ function tint_to_color(tint, opacity) {
} }
} }
// ======================================================================== // Batch drawables for efficient rendering
// BATCHING function _batch_drawables(drawables) {
// ========================================================================
function batch_drawables(drawables) {
var batches = [] var batches = []
var current_batch = null var current_batch = null
var mat = {blend: 'alpha', sampler: 'nearest'}
for (var drawable of drawables) { array.for(drawables, drawable => {
if (drawable.type == 'sprite') { if (drawable.type == 'sprite') {
var texture = drawable.texture || drawable.image var texture = drawable.texture || drawable.image
var material = drawable.material || {blend: 'alpha', sampler: 'nearest'} var material = drawable.material || mat
var scissor = drawable.scissor var scissor = drawable.scissor
// Start new batch if texture/material/scissor changed // Check if can merge with current batch
if (!current_batch || if (current_batch &&
current_batch.type != 'sprite_batch' || current_batch.type == 'sprite_batch' &&
current_batch.texture != texture || current_batch.texture == texture &&
!rect_equal(current_batch.scissor, scissor) || _rect_equal(current_batch.scissor, scissor) &&
!materials_equal(current_batch.material, material)) { _materials_equal(current_batch.material, material)) {
current_batch.sprites.push(drawable)
} else {
if (current_batch) batches.push(current_batch) if (current_batch) batches.push(current_batch)
current_batch = { current_batch = {
type: 'sprite_batch', type: 'sprite_batch',
texture: texture, texture: texture,
material: material, material: material,
scissor: scissor, scissor: scissor,
sprites: [] sprites: [drawable]
} }
} }
current_batch.sprites.push(drawable)
} else { } else {
// Non-sprite: flush batch, add individually // Non-sprite: flush batch, add individually
if (current_batch) { if (current_batch) {
@@ -727,20 +427,20 @@ function batch_drawables(drawables) {
} }
batches.push({type: drawable.type, drawable: drawable, scissor: drawable.scissor}) batches.push({type: drawable.type, drawable: drawable, scissor: drawable.scissor})
} }
} })
if (current_batch) batches.push(current_batch) if (current_batch) batches.push(current_batch)
return batches return batches
} }
function rect_equal(a, b) { function _rect_equal(a, b) {
if (!a && !b) return true if (!a && !b) return true
if (!a || !b) return false if (!a || !b) return false
return a.x == b.x && a.y == b.y && a.width == b.width && a.height == b.height 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 if (!a || !b) return a == b
return a.blend == b.blend && a.sampler == b.sampler && a.shader == b.shader return a.blend == b.blend && a.sampler == b.sampler && a.shader == b.shader
} }

View File

@@ -1200,6 +1200,91 @@ JSC_CCALL(geometry_weave,
return result; 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[] = { static const JSCFunctionListEntry js_geometry_funcs[] = {
MIST_FUNC_DEF(geometry, rect_intersection, 2), MIST_FUNC_DEF(geometry, rect_intersection, 2),
MIST_FUNC_DEF(geometry, rect_intersects, 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, rect_transform, 2),
MIST_FUNC_DEF(geometry, tilemap_to_data, 1), MIST_FUNC_DEF(geometry, tilemap_to_data, 1),
MIST_FUNC_DEF(geometry, sprites_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(geometry, transform_xy_blob, 2),
MIST_FUNC_DEF(gpu, tile, 4), MIST_FUNC_DEF(gpu, tile, 4),
MIST_FUNC_DEF(gpu, slice9, 4), MIST_FUNC_DEF(gpu, slice9, 4),

View File

@@ -158,7 +158,7 @@ function create_image(path){
def bytes = io.slurp(path); def bytes = io.slurp(path);
var ext = path.split('.').pop() var ext = path.split('.').pop()
let raw = decode_image(bytes, ext); var raw = decode_image(bytes, ext);
/* ── Case A: single surface (from make_texture) ────────────── */ /* ── Case A: single surface (from make_texture) ────────────── */
if(raw && raw.width && raw.pixels && !isa(raw, array)) { if(raw && raw.width && raw.pixels && !isa(raw, array)) {

View File

@@ -9,7 +9,6 @@ var io = use('cellfs')
var geometry = use('geometry') var geometry = use('geometry')
var blob = use('blob') var blob = use('blob')
var imgui = use('imgui') var imgui = use('imgui')
var utf8 = use('utf8')
var json = use('json') var json = use('json')
var os = use('os') var os = use('os')
@@ -223,7 +222,7 @@ function make_shader(sh_file)
{ {
var file = `shaders/${shader_type}/${sh_file}.${shader_type}` var file = `shaders/${shader_type}/${sh_file}.${shader_type}`
if (shader_cache[file]) return shader_cache[file] 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 = { var shader = {
code: io.slurp(file), code: io.slurp(file),

View File

@@ -647,6 +647,7 @@ function _create_gpu_texture(w, h, pixels) {
function _load_image_file(path) { function _load_image_file(path) {
var bytes = io.slurp(path) var bytes = io.slurp(path)
var decoded
if (!bytes) return null if (!bytes) return null
var ext = path.split('.').pop().toLowerCase() var ext = path.split('.').pop().toLowerCase()
@@ -663,14 +664,14 @@ function _load_image_file(path) {
surface = qoi.decode(bytes) surface = qoi.decode(bytes)
break break
case 'gif': case 'gif':
var decoded = gif.decode(bytes) decoded = gif.decode(bytes)
if (decoded && decoded.frames && decoded.frames.length > 0) { if (decoded && decoded.frames && decoded.frames.length > 0) {
surface = decoded.frames[0] surface = decoded.frames[0]
} }
break break
case 'ase': case 'ase':
case 'aseprite': case 'aseprite':
var decoded = aseprite.decode(bytes) decoded = aseprite.decode(bytes)
if (decoded && decoded.frames && decoded.frames.length > 0) { if (decoded && decoded.frames && decoded.frames.length > 0) {
surface = decoded.frames[0] surface = decoded.frames[0]
} }
@@ -786,19 +787,21 @@ function _build_sprite_vertices(sprites, camera) {
var vertices_per_sprite = 4 var vertices_per_sprite = 4
var indices_per_sprite = 6 var indices_per_sprite = 6
var vertex_data = new blob_mod(sprites.length * vertices_per_sprite * floats_per_vertex * 4) var vertex_data = new blob_mod(sprites.length * vertices_per_sprite * floats_per_vertex * 32)
var index_data = new blob_mod(sprites.length * indices_per_sprite * 2) var index_data = geometry.make_quad_indices(sprites.length)
var vertex_count = 0 var vertex_count = 0
var white = {r: 1, g: 1, b: 1, a: 1}
for (var s of sprites) { array.for(sprites, s => {
var px = s.pos.x != null ? s.pos.x : (s.pos[0] || 0) var px = s.pos.x
var py = s.pos.y != null ? s.pos.y : (s.pos[1] || 0) var py = s.pos.y
var w = s.width || 1 var w = s.width || 1
var h = s.height || 1 var h = s.height || 1
var ax = s.anchor_x || 0 var ax = s.anchor_x || 0
var ay = s.anchor_y || 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 // Apply anchor
var x = px - w * ax 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 v0 = s.uv_rect ? s.uv_rect.y : 0
var u1 = s.uv_rect ? (s.uv_rect.x + s.uv_rect.width) : 1 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 var v1 = s.uv_rect ? (s.uv_rect.y + s.uv_rect.height) : 1
// Quad vertices (bottom-left, bottom-right, top-right, top-left) // call to implement
// v0: bottom-left var verts = geometry.sprite_vertices({x,y,w,h,u0,v0,u1,v1,c})
vertex_data.wf(x) vertex_data.write_blob(verts)
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
}
return { return {
vertices: stone(vertex_data), vertices: stone(vertex_data),
@@ -1042,6 +998,7 @@ function _execute_commands(commands, window_size) {
var current_target = null var current_target = null
var current_camera = null var current_camera = null
var pending_draws = [] var pending_draws = []
var target
// Cache swapchain texture for the duration of this command buffer // Cache swapchain texture for the duration of this command buffer
var _swapchain_tex = null var _swapchain_tex = null
@@ -1067,7 +1024,7 @@ function _execute_commands(commands, window_size) {
} }
// Start new pass // Start new pass
var target = cmd.target target = cmd.target
var clear = cmd.clear var clear = cmd.clear
if (target == 'screen') { if (target == 'screen') {
@@ -1209,7 +1166,7 @@ function _execute_commands(commands, window_size) {
imgui_mod.prepare(cmd_buffer) imgui_mod.prepare(cmd_buffer)
// Restart pass to the same target for rendering // Restart pass to the same target for rendering
var target = cmd.target target = cmd.target
var swap_tex = null var swap_tex = null
if (target == 'screen') { if (target == 'screen') {
swap_tex = get_swapchain_tex() swap_tex = get_swapchain_tex()

101
sprite.cm
View File

@@ -1,6 +1,10 @@
// sprite // sprite.cm - Sprite node factory
//
// Returns a function that creates sprite instances via meme()
var sprite = { var sprite = {
pos: {x:0, y:0}, type: 'sprite',
pos: null,
layer: 0, layer: 0,
image: null, image: null,
width: 1, width: 1,
@@ -9,10 +13,95 @@ var sprite = {
anchor_y: 0, anchor_y: 0,
scale_x: 1, scale_x: 1,
scale_y: 1, scale_y: 1,
color: {r:1, g:1, b:1, a:1}, color: null,
animation: null, // 'walk', 'attack', etc uv_rect: null,
slice: null,
tile: null,
material: null,
animation: null,
frame: 0, 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 // 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
}