Files
prosperon/compositor.cm
2026-01-16 20:56:16 -06:00

371 lines
11 KiB
Plaintext

var effects_mod = use('effects')
var backend = use('sdl_gpu')
var film2d = use('film2d')
var compositor = {}
// Compile compositor config into render plan
compositor.compile = function(config) {
var ctx = {
passes: [],
targets: {},
counter: 0,
screen_size: backend.get_window_size ? backend.get_window_size() : {width: 1280, height: 720},
alloc: function(w, h, hint) {
var key = (hint || 't') + '_' + text(this.counter++)
this.targets[key] = {width: w, height: h, key: key}
return {type: 'target', key: key, width: w, height: h}
}
}
var group_effects = config.group_effects || {}
// Clear screen
if (config.clear)
ctx.passes.push({type: 'clear', target: 'screen', color: config.clear})
// 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_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'
// 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: []}
var base_sprites = []
for (var i = 0; i < all_sprites.length; i++) {
var s = all_sprites[i]
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]) {
if (!effect_groups[gname])
effect_groups[gname] = {sprites: [], effects: group_effects[gname].effects}
effect_groups[gname].sprites.push(s)
assigned = true
break // Only assign to first matching effect group
}
}
// Add to base sprites if not assigned to effect group and not mask-only
if (!assigned && !is_mask_only) base_sprites.push(s)
}
// Allocate plane target
var plane_target = ctx.alloc(res.width, res.height, plane_config.name)
// 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) {
var eg = effect_groups[gname]
if (eg.sprites.length == 0) continue
var group_target = ctx.alloc(res.width, res.height, gname + '_content')
// Render group content
ctx.passes.push({
type: 'render',
renderer: 'film2d',
drawables: eg.sprites,
camera: camera,
target: group_target,
target_size: res,
layer_sort: layer_sort,
clear: {r: 0, g: 0, b: 0, a: 0}
})
// Apply effects
var current = group_target
for (var e = 0; e < eg.effects.length; e++) {
var effect = eg.effects[e]
current = apply_effect(ctx, effect, current, res, camera, gname, plane_name, group_effects)
}
// Composite result to plane
ctx.passes.push({
type: 'composite',
source: current,
dest: plane_target,
source_size: res,
dest_size: res,
blend: 'over'
})
}
// Render base sprites (no effects)
if (base_sprites.length > 0) {
ctx.passes.push({
type: 'render',
renderer: 'film2d',
drawables: base_sprites,
camera: camera,
target: plane_target,
target_size: res,
layer_sort: layer_sort,
clear: null // Don't clear, blend on top
})
}
// Composite plane to screen
ctx.passes.push({
type: 'blit_to_screen',
source: plane_target,
source_size: res,
dest_size: ctx.screen_size,
presentation: plane_config.presentation || 'stretch'
})
}
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 = 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
ctx.passes.push({
type: 'shader_pass',
shader: 'threshold',
input: input,
output: bright,
uniforms: {threshold: effect.threshold || 0.8, intensity: effect.intensity || 1}
})
// Blur passes
var blur_passes = effect.blur_passes || 2
var blur_in = bright
for (var p = 0; p < blur_passes; p++) {
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
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
// 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 = ctx.alloc(size.width, size.height, hint + '_mask')
// Render mask
ctx.passes.push({
type: 'render',
renderer: 'film2d',
drawables: mask_sprites,
camera: camera,
target: mask_target,
target_size: size,
clear: {r: 0, g: 0, b: 0, a: 0}
})
// Apply mask
ctx.passes.push({
type: 'apply_mask',
content: input,
mask: mask_target,
output: output,
mode: effect.channel || 'alpha',
invert: effect.invert || false
})
} else {
// No mask sprites, pass through
ctx.passes.push({type: 'blit', source: input, dest: output})
}
} else {
// Unknown effect, pass through
ctx.passes.push({type: 'blit', source: input, dest: output})
}
return output
}
// Execute compiled plan
compositor.execute = function(plan) {
var cache = {}
for (var key in plan.targets) {
var spec = plan.targets[key]
cache[key] = backend.get_or_create_target(spec.width, spec.height, key)
}
function resolve(t) {
if (t == 'screen') return 'screen'
if (t && t.type == 'target') return cache[t.key]
return t
}
var commands = []
for (var i = 0; i < plan.passes.length; i++) {
var pass = plan.passes[i]
if (pass.type == 'clear') {
var target = resolve(pass.target)
commands.push({cmd: 'begin_render', target: target, clear: pass.color})
commands.push({cmd: 'end_render'})
} else if (pass.type == 'render') {
var result = film2d.render({
drawables: pass.drawables,
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++)
commands.push(result.commands[c])
} else if (pass.type == 'shader_pass') {
commands.push({
cmd: 'shader_pass',
shader: pass.shader,
input: resolve(pass.input),
output: resolve(pass.output),
uniforms: pass.uniforms
})
} else if (pass.type == 'composite_textures') {
commands.push({
cmd: 'composite_textures',
base: resolve(pass.base),
overlay: resolve(pass.overlay),
output: resolve(pass.output),
mode: pass.mode
})
} else if (pass.type == 'apply_mask') {
commands.push({
cmd: 'apply_mask',
content_texture: resolve(pass.content),
mask_texture: resolve(pass.mask),
output: resolve(pass.output),
mode: pass.mode,
invert: pass.invert
})
} else if (pass.type == 'composite') {
commands.push({
cmd: 'blit',
texture: resolve(pass.source),
target: resolve(pass.dest),
dst_rect: {x: 0, y: 0, width: pass.dest_size.width, height: pass.dest_size.height}
})
} else if (pass.type == 'blit_to_screen') {
var rect = _calc_presentation(pass.source_size, pass.dest_size, pass.presentation)
commands.push({
cmd: 'blit',
texture: resolve(pass.source),
target: 'screen',
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, plan: plan}
}
function _calc_presentation(src, dst, mode) {
if (mode == 'stretch')
return {x: 0, y: 0, width: dst.width, height: dst.height}
if (mode == 'integer_scale') {
var sx = floor(dst.width / src.width)
var sy = floor(dst.height / src.height)
var s = max(1, min(sx, sy))
var w = src.width * s
var h = src.height * s
return {x: (dst.width - w) / 2, y: (dst.height - h) / 2, width: w, height: h}
}
// letterbox
var scale = min(dst.width / src.width, dst.height / src.height)
var w = src.width * scale
var h = src.height * scale
return {x: (dst.width - w) / 2, y: (dst.height - h) / 2, width: w, height: h}
}
return compositor