This commit is contained in:
2026-01-07 14:25:11 -06:00
parent 0522b967ca
commit 4ea9d43a94
9 changed files with 302 additions and 69 deletions

View File

@@ -25,21 +25,53 @@ compositor.compile = function(config) {
if (config.clear)
ctx.passes.push({type: 'clear', target: 'screen', color: config.clear})
// Process each layer
var layers = config.layers || []
for (var i = 0; i < layers.length; i++)
compile_layer(layers[i], ctx, group_effects)
// Process each plane (supports both 'planes' and legacy 'layers' key)
var planes = config.planes || config.layers || []
for (var i = 0; i < planes.length; i++) {
var plane = planes[i]
var type = plane.type || 'film2d'
if (type == 'imgui') {
compile_imgui_layer(plane, ctx)
} else {
compile_plane(plane, ctx, group_effects)
}
}
return {passes: ctx.passes, targets: ctx.targets, screen_size: ctx.screen_size}
}
function compile_layer(layer, ctx, group_effects) {
var group = layer.group
var res = layer.resolution || ctx.screen_size
var camera = layer.camera
function compile_imgui_layer(layer, ctx) {
ctx.passes.push({
type: 'imgui',
target: 'screen',
draw: layer.draw
})
}
function compile_plane(plane_config, ctx, group_effects) {
var plane_name = plane_config.plane || plane_config.name
var res = plane_config.resolution || ctx.screen_size
var camera = plane_config.camera
var layer_sort = plane_config.layer_sort || {} // layer -> 'y' or 'explicit'
// Get all sprites in this group
var all_sprites = film2d.query({group: group})
// Build set of groups used as masks (these should not be drawn directly)
var mask_groups = {}
for (var gname in group_effects) {
var effects = group_effects[gname].effects || []
for (var e = 0; e < effects.length; e++) {
if (effects[e].type == 'mask' && effects[e].mask_group)
mask_groups[effects[e].mask_group] = true
}
}
// Get all sprites in this plane
var all_sprites = film2d.query({plane: plane_name})
// Add manual drawables
if (plane_config.drawables) {
for (var i = 0; i < plane_config.drawables.length; i++)
all_sprites.push(plane_config.drawables[i])
}
// Find which sprites belong to groups with effects
var effect_groups = {} // group_name -> {sprites: [], effects: []}
@@ -47,10 +79,20 @@ function compile_layer(layer, ctx, group_effects) {
for (var i = 0; i < all_sprites.length; i++) {
var s = all_sprites[i]
var assigned = false
// Check if sprite belongs to any effect group
var sprite_groups = s.groups || []
var assigned = false
var is_mask_only = sprite_groups.length > 0
// First pass: check if sprite has any non-mask group
for (var g = 0; g < sprite_groups.length; g++) {
var gname = sprite_groups[g]
if (!mask_groups[gname]) {
is_mask_only = false
break
}
}
// Second pass: assign to effect groups
for (var g = 0; g < sprite_groups.length; g++) {
var gname = sprite_groups[g]
if (group_effects[gname]) {
@@ -62,15 +104,16 @@ function compile_layer(layer, ctx, group_effects) {
}
}
if (!assigned) base_sprites.push(s)
// Add to base sprites if not assigned to effect group and not mask-only
if (!assigned && !is_mask_only) base_sprites.push(s)
}
// Allocate layer target
var layer_target = ctx.alloc(res.width, res.height, layer.name)
// Allocate plane target
var plane_target = ctx.alloc(res.width, res.height, plane_config.name)
// Clear layer
if (layer.clear)
ctx.passes.push({type: 'clear', target: layer_target, color: layer.clear})
// Clear plane
if (plane_config.clear)
ctx.passes.push({type: 'clear', target: plane_target, color: plane_config.clear})
// Render each effect group to temp target, apply effects, composite back
for (var gname in effect_groups) {
@@ -87,6 +130,7 @@ function compile_layer(layer, ctx, group_effects) {
camera: camera,
target: group_target,
target_size: res,
layer_sort: layer_sort,
clear: {r: 0, g: 0, b: 0, a: 0}
})
@@ -94,14 +138,14 @@ function compile_layer(layer, ctx, group_effects) {
var current = group_target
for (var e = 0; e < eg.effects.length; e++) {
var effect = eg.effects[e]
current = apply_effect(effect, current, res, gname, group_effects)
current = apply_effect(ctx, effect, current, res, camera, gname, plane_name, group_effects)
}
// Composite result to layer
// Composite result to plane
ctx.passes.push({
type: 'composite',
source: current,
dest: layer_target,
dest: plane_target,
source_size: res,
dest_size: res,
blend: 'over'
@@ -115,32 +159,33 @@ function compile_layer(layer, ctx, group_effects) {
renderer: 'film2d',
drawables: base_sprites,
camera: camera,
target: layer_target,
target: plane_target,
target_size: res,
layer_sort: layer_sort,
clear: null // Don't clear, blend on top
})
}
// Composite layer to screen
// Composite plane to screen
ctx.passes.push({
type: 'blit_to_screen',
source: layer_target,
source: plane_target,
source_size: res,
dest_size: ctx.screen_size,
presentation: layer.presentation || 'stretch'
presentation: plane_config.presentation || 'stretch'
})
}
function apply_effect(effect, input, size, hint, group_effects) {
var output = alloc(size.width, size.height, hint + '_' + effect.type)
function apply_effect(ctx, effect, input, size, camera, hint, current_plane, group_effects) {
var output = ctx.alloc(size.width, size.height, hint + '_' + effect.type)
if (effect.type == 'bloom') {
var bright = alloc(size.width, size.height, hint + '_bright')
var blur1 = alloc(size.width, size.height, hint + '_blur1')
var blur2 = alloc(size.width, size.height, hint + '_blur2')
var bright = ctx.alloc(size.width, size.height, hint + '_bright')
var blur1 = ctx.alloc(size.width, size.height, hint + '_blur1')
var blur2 = ctx.alloc(size.width, size.height, hint + '_blur2')
// Threshold
passes.push({
ctx.passes.push({
type: 'shader_pass',
shader: 'threshold',
input: input,
@@ -152,34 +197,35 @@ function apply_effect(effect, input, size, hint, group_effects) {
var blur_passes = effect.blur_passes || 2
var blur_in = bright
for (var p = 0; p < blur_passes; p++) {
passes.push({type: 'shader_pass', shader: 'blur', input: blur_in, output: blur1, uniforms: {direction: {x: 1, y: 0}, texel_size: {x: 1/size.width, y: 1/size.height}}})
passes.push({type: 'shader_pass', shader: 'blur', input: blur1, output: blur2, uniforms: {direction: {x: 0, y: 1}, texel_size: {x: 1/size.width, y: 1/size.height}}})
ctx.passes.push({type: 'shader_pass', shader: 'blur', input: blur_in, output: blur1, uniforms: {direction: {x: 1, y: 0}, texel_size: {x: 1/size.width, y: 1/size.height}}})
ctx.passes.push({type: 'shader_pass', shader: 'blur', input: blur1, output: blur2, uniforms: {direction: {x: 0, y: 1}, texel_size: {x: 1/size.width, y: 1/size.height}}})
blur_in = blur2
}
// Composite bloom
passes.push({type: 'composite_textures', base: input, overlay: blur2, output: output, mode: 'add'})
ctx.passes.push({type: 'composite_textures', base: input, overlay: blur2, output: output, mode: 'add'})
} else if (effect.type == 'mask') {
var mask_group = effect.mask_group
var mask_sprites = film2d.query({group: mask_group})
// Query masks within the same plane to avoid cross-plane mask issues
var mask_sprites = film2d.query({group: mask_group, plane: current_plane})
if (mask_sprites.length > 0) {
var mask_target = alloc(size.width, size.height, hint + '_mask')
var mask_target = ctx.alloc(size.width, size.height, hint + '_mask')
// Render mask
passes.push({
ctx.passes.push({
type: 'render',
renderer: 'film2d',
drawables: mask_sprites,
camera: null, // Same camera as parent? Need to pass this
camera: camera,
target: mask_target,
target_size: size,
clear: {r: 0, g: 0, b: 0, a: 0}
})
// Apply mask
passes.push({
ctx.passes.push({
type: 'apply_mask',
content: input,
mask: mask_target,
@@ -189,11 +235,11 @@ function apply_effect(effect, input, size, hint, group_effects) {
})
} else {
// No mask sprites, pass through
passes.push({type: 'blit', source: input, dest: output})
ctx.passes.push({type: 'blit', source: input, dest: output})
}
} else {
// Unknown effect, pass through
passes.push({type: 'blit', source: input, dest: output})
ctx.passes.push({type: 'blit', source: input, dest: output})
}
return output
@@ -230,6 +276,7 @@ compositor.execute = function(plan) {
camera: pass.camera,
target: resolve(pass.target),
target_size: pass.target_size,
layer_sort: pass.layer_sort || {},
clear: pass.clear
}, backend)
for (var c = 0; c < result.commands.length; c++)
@@ -280,10 +327,25 @@ compositor.execute = function(plan) {
dst_rect: rect,
filter: pass.presentation == 'integer_scale' ? 'nearest' : 'linear'
})
} else if (pass.type == 'blit') {
var src = resolve(pass.source)
var dst = resolve(pass.dest)
commands.push({
cmd: 'blit',
texture: src,
target: dst,
dst_rect: {x: 0, y: 0, width: dst.width, height: dst.height}
})
} else if (pass.type == 'imgui') {
commands.push({
cmd: 'imgui',
target: resolve(pass.target),
draw: pass.draw
})
}
}
return {commands: commands}
return {commands: commands, plan: plan}
}
function _calc_presentation(src, dst, mode) {

56
core.cm
View File

@@ -13,6 +13,7 @@
var video = use('sdl3/video')
var events = use('sdl3/input')
var time_mod = use('time')
var debug_imgui = use('debug_imgui')
var core = {}
@@ -48,7 +49,7 @@ core.start = function(config) {
_window = _backend.get_window()
if (config.imgui && imgui.init) {
if ((config.imgui || config.editor) && imgui.init) {
imgui.init(_window, _backend.get_device())
}
@@ -75,12 +76,19 @@ core.window_size = function() {
core.backend = function() {
return _backend
}
// FPS tracking
var _fps_samples = []
var _fps_sample_count = 60
var _current_fps = 0
var _frame_time_ms = 0
// Main loop
function _main_loop() {
var frame_start = time_mod.number()
if (!_running) return
var now = time_mod.number()
var now = frame_start
var dt = now - _last_time
_last_time = now
@@ -90,7 +98,7 @@ function _main_loop() {
var win_size = _backend.get_window_size()
for (var ev of evts) {
if (_config.imgui) {
if (_config.imgui || _config.editor) {
imgui.process_event(ev)
}
@@ -117,9 +125,12 @@ function _main_loop() {
_config.update(dt)
}
var imgui_mod = use('imgui')
var debug_imgui = use('debug_imgui')
// ImGui Frame
if (_config.imgui) {
imgui.newframe()
if (_config.imgui || _config.editor) {
imgui_mod.newframe()
}
// Render
@@ -133,12 +144,24 @@ function _main_loop() {
}
var dbg = _config.debug == 'cmd'
// Build stats for debug_imgui
var stats = {
fps: _current_fps,
frame_time_ms: _frame_time_ms
}
// Handle both compositor result ({commands: [...]}) and fx_graph (graph object)
if (render_result.commands) {
if (_config.imgui) {
if (_config.imgui || _config.editor) {
render_result.commands.push({
cmd: 'imgui',
draw: _config.imgui,
draw: function(ui) {
if (_config.imgui) _config.imgui(ui)
if (_config.editor) {
debug_imgui.render(ui, null, render_result.plan, stats)
_config.editor(ui)
}
},
target: 'screen'
})
}
@@ -156,9 +179,26 @@ function _main_loop() {
}
}
// Measure actual frame work time (excluding delay)
var frame_end = time_mod.number()
var actual_frame_time = frame_end - frame_start
// Track FPS based on actual work time
_frame_time_ms = actual_frame_time * 1000
_fps_samples.push(actual_frame_time)
if (_fps_samples.length > _fps_sample_count) {
_fps_samples.shift()
}
var avg_frame_time = 0
for (var i = 0; i < _fps_samples.length; i++) {
avg_frame_time += _fps_samples[i]
}
avg_frame_time = avg_frame_time / _fps_samples.length
_current_fps = avg_frame_time > 0 ? 1 / avg_frame_time : 0
// Schedule next frame
var frame_time = 1 / _framerate
var elapsed = time_mod.number() - now
var elapsed = frame_end - frame_start
var delay = frame_time - elapsed
if (delay < 0) delay = 0

View File

@@ -8,11 +8,13 @@
var debug_imgui = {}
var json = use('json')
// State
var _show_scene_tree = false
var _show_render_graph = false
var _show_effects = false
var _show_stats = false
var _show_stats = true
var _show_targets = false
var _selected_node = null

138
film2d.cm
View File

@@ -3,13 +3,20 @@ var film2d = {}
var next_id = 1
var registry = {} // id -> drawable
var group_index = {} // group_name -> [id, id, ...]
var plane_index = {} // plane_name -> [id, id, ...]
film2d.register = function(drawable) {
var id = text(next_id++)
drawable._id = id
registry[id] = drawable
var groups = drawable.groups || ['default']
// Index by plane
var plane = drawable.plane || 'default'
if (!plane_index[plane]) plane_index[plane] = []
plane_index[plane].push(id)
// Index by groups (effect routing only)
var groups = drawable.groups || []
for (var i = 0; i < groups.length; i++) {
var g = groups[i]
if (!group_index[g]) group_index[g] = []
@@ -24,6 +31,14 @@ film2d.unregister = function(id) {
var drawable = registry[id_str]
if (!drawable) return
// Remove from plane index
var plane = drawable.plane || 'default'
if (plane_index[plane]) {
var idx = plane_index[plane].indexOf(id_str)
if (idx >= 0) plane_index[plane].splice(idx, 1)
}
// Remove from group indices
var groups = drawable.groups || []
for (var i = 0; i < groups.length; i++) {
var g = groups[i]
@@ -59,10 +74,29 @@ film2d.get = function(id) {
return registry[text(id)]
}
// Query by group - returns array of drawables
// Query by plane and/or group - returns array of drawables
film2d.query = function(selector) {
var result = []
// Query by plane (primary selection)
if (selector.plane) {
var ids = plane_index[selector.plane] || []
for (var i = 0; i < ids.length; i++) {
var d = registry[ids[i]]
if (d && d.visible != false) {
// If also filtering by group, check membership
if (selector.group) {
var groups = d.groups || []
if (groups.indexOf(selector.group) >= 0) result.push(d)
} else {
result.push(d)
}
}
}
return result
}
// Query by group only (for effect routing)
if (selector.group) {
var ids = group_index[selector.group] || []
for (var i = 0; i < ids.length; i++) {
@@ -116,14 +150,43 @@ film2d.render = function(params, backend) {
var target = params.target
var target_size = params.target_size
var clear_color = params.clear
var layer_sort = params.layer_sort || {} // layer -> 'y' or 'explicit'
if (drawables.length == 0) return {commands: []}
function _y_sort_key(d) {
if (!d || !d.pos) return 0
var y = d.pos.y || 0
var h = d.height || 0
var ay = d.anchor_y
if (ay == null) ay = 0.5
// Convert "pos.y at anchor" -> "feet y"
return y + h * (1 - ay)
}
// Sort by layer, then Y
// Sort by layer, then optionally by Y based on layer_sort policy
drawables.sort(function(a, b) {
var dl = (a.layer || 0) - (b.layer || 0)
var al = a.layer || 0
var bl = b.layer || 0
var dl = al - bl
if (dl != 0) return dl
return (b.pos.y || 0) - (a.pos.y || 0)
var sort_mode = layer_sort[text(al)] || 'explicit'
if (sort_mode == 'y') {
var ay = _y_sort_key(a)
var by = _y_sort_key(b)
// Make this explicit instead of guessing
var y_down = camera && camera.y_down == true
// If y_down: bigger y is lower on screen => should draw later (on top)
// If y_up: smaller y is lower on screen => should draw later (on top)
if (ay != by) return y_down ? (ay - by) : (by - ay)
}
var aid = a._id || 0
var bid = b._id || 0
return aid < bid ? -1 : 1
})
var commands = []
@@ -164,6 +227,71 @@ function _batch_drawables(drawables) {
if (current) batches.push(current)
current = {type: 'sprite_batch', texture: tex, material: mat, sprites: [d]}
}
} else if (d.type == 'particles') {
// Convert particles to sprites
var tex = d.texture || d.image
var mat = d.material || default_mat
var particles = d.particles || []
for (var p = 0; p < particles.length; p++) {
var part = particles[p]
var sprite = {
type: 'sprite',
pos: part.pos,
width: (d.width || 16) * (part.scale || 1),
height: (d.height || 16) * (part.scale || 1),
anchor_x: 0.5,
anchor_y: 0.5,
color: part.color || {r: 1, g: 1, b: 1, a: 1}
}
if (current && current.type == 'sprite_batch' && current.texture == tex && _mat_eq(current.material, mat)) {
current.sprites.push(sprite)
} else {
if (current) batches.push(current)
current = {type: 'sprite_batch', texture: tex, material: mat, sprites: [sprite]}
}
}
} else if (d.type == 'tilemap') {
// Expand tilemap to sprites
var tiles = d.tiles || []
var tile_w = d.tile_width || 1
var tile_h = d.tile_height || 1
var off_x = d.offset_x || 0
var off_y = d.offset_y || 0
for (var x = 0; x < tiles.length; x++) {
if (!tiles[x]) continue
for (var y = 0; y < tiles[x].length; y++) {
var img = tiles[x][y]
if (!img) continue
var wx = (x + off_x) * tile_w
var wy = (y + off_y) * tile_h
// Center anchor for sprite
var sprite = {
type: 'sprite',
image: img,
pos: {x: wx + tile_w/2, y: wy + tile_h/2},
width: tile_w,
height: tile_h,
anchor_x: 0.5,
anchor_y: 0.5,
color: {r: 1, g: 1, b: 1, a: 1}
}
// Batching
var tex = img
var mat = default_mat
if (current && current.type == 'sprite_batch' && current.texture == tex && _mat_eq(current.material, mat)) {
current.sprites.push(sprite)
} else {
if (current) batches.push(current)
current = {type: 'sprite_batch', texture: tex, material: mat, sprites: [sprite]}
}
}
}
} else {
if (current) {
batches.push(current)

View File

@@ -116,12 +116,13 @@ function lerp(a, b, t) { return a + (b - a) * t }
var factory = function(props) {
var defaults = {
type: 'particles',
pos: {x: 0, y: 0},
image: null,
width: 16,
height: 16,
plane: 'default',
layer: 0,
tags: [],
groups: [],
particles: []
}

View File

@@ -1627,9 +1627,8 @@ function _do_blit(cmd_buffer, cmd, current_target, get_swapchain_tex) {
var pass = cmd_buffer.render_pass({
color_targets: [{
texture: swap_tex,
load: "clear",
store: "store",
clear_color: {r: 0, g: 0, b: 0, a: 1}
load: "load", // Load existing content to blend layers properly
store: "store"
}]
})

View File

@@ -52,8 +52,9 @@ return function(props) {
rotation: 0,
color: {r: 1, g: 1, b: 1, a: 1},
opacity: 1,
plane: 'default',
layer: 0,
groups: ['default'],
groups: [],
visible: true
}
@@ -62,7 +63,7 @@ return function(props) {
for (var k in props) data[k] = props[k]
// Ensure groups is array
if (!data.groups) data.groups = ['default']
if (!data.groups) data.groups = []
if (is_text(data.groups)) data.groups = [data.groups]
var s = meme(sprite_proto, data)

View File

@@ -31,14 +31,14 @@ return function(props) {
pos: {x: 0, y: 0},
plane: 'default',
layer: 0,
groups: [],
font: "fonts/dos",
size: 16,
color: {r: 1, g: 1, b: 1, a: 1},
mode: null,
sdf: null,
outline_width: null,
outline_color: null,
tags: []
outline_color: null
}
var data = {}

View File

@@ -32,7 +32,7 @@ var tilemap = {
}
}
stone(tilemap)
//stone(tilemap)
// Factory function - auto-registers with film2d
return function(props) {
@@ -43,9 +43,9 @@ return function(props) {
offset_y: 0,
plane: 'default',
layer: 0,
groups: [],
tile_width: 1,
tile_height: 1,
tags: []
tile_height: 1
}
var data = {}
@@ -53,7 +53,7 @@ return function(props) {
for(var k in props) data[k] = props[k]
var newtilemap = meme(tilemap, data)
newtilemap.tiles = [] // Initialize tiles
// newtilemap.tiles = [] // Initialize tiles
film2d.register(newtilemap)
return newtilemap
}