Files
prosperon/compositor.cm
2025-12-22 16:21:21 -06:00

189 lines
5.7 KiB
Plaintext

// compositor.cm - High-level compositing API built on fx_graph
//
// Provides a simple interface for common rendering patterns:
// - World view with camera
// - UI overlay
// - Masking
// - Post-effects
//
// Usage:
// var comp = compositor()
// comp.set_world(scene_root, world_camera)
// comp.set_ui(ui_root, ui_camera)
// comp.set_mask(mask_root) // optional
// var cmds = comp.render(backend, window_size)
var fx_graph = use('fx_graph')
var compositor = {}
// Set the world view (main game scene)
compositor.set_world = function(root_or_fn, camera, target_spec) {
this.world_view = {
root: typeof root_or_fn == 'function' ? root_or_fn : function() { return root_or_fn },
camera: camera,
target: target_spec || this.world_target_spec
}
}
// Set the UI overlay
compositor.set_ui = function(root_or_fn, camera, target_spec) {
this.ui_view = {
root: typeof root_or_fn == 'function' ? root_or_fn : function() { return root_or_fn },
camera: camera,
target: target_spec || this.ui_target_spec
}
}
// Set a mask for the world view
compositor.set_mask = function(content_root_or_fn, mask_root_or_fn, opts) {
opts = opts || {}
if (!content_root_or_fn || !mask_root_or_fn) {
this.mask_config = null
return
}
this.mask_config = {
content: typeof content_root_or_fn == 'function' ? content_root_or_fn : function() { return content_root_or_fn },
mask: typeof mask_root_or_fn == 'function' ? mask_root_or_fn : function() { return mask_root_or_fn },
invert: opts.invert || false,
mode: opts.mode || 'alpha'
}
}
// Add a named layer for more complex compositing
compositor.add_layer = function(name, opts) {
this.layers[name] = {
root: typeof opts.root == 'function' ? opts.root : function() { return opts.root },
camera: opts.camera,
target: opts.target || this.world_target_spec,
clear_color: opts.clear_color,
blend: opts.blend || 'over',
opacity: opts.opacity != null ? opts.opacity : 1
}
}
compositor.remove_layer = function(name) {
delete this.layers[name]
}
// Build and execute the render graph
compositor.render = function(backend, window_size) {
var graph = this.build_graph(window_size)
return backend.render(graph, window_size)
}
// Build the fx_graph from current configuration
compositor.build_graph = function(window_size) {
var graph = fx_graph()
var fit = fx_graph.fit_to_screen
// Get scene trees
var world_root = this.world_view ? this.world_view.root() : null
var ui_root = this.ui_view ? this.ui_view.root() : null
var outputs = []
// Render world view
if (world_root && this.world_view) {
var world_node = graph.add_node('render_view', {
root: world_root,
camera: this.world_view.camera,
target: this.world_view.target,
clear_color: this.world_view.camera.background || {r: 0, g: 0, b: 0, a: 1}
})
outputs.push({output: world_node.output, target_spec: this.world_view.target, name: 'world'})
}
// Render additional layers
for (var name in this.layers) {
var layer = this.layers[name]
var layer_root = layer.root()
if (!layer_root) continue
var layer_node = graph.add_node('render_view', {
root: layer_root,
camera: layer.camera,
target: layer.target,
clear_color: layer.clear_color || {r: 0, g: 0, b: 0, a: 0}
})
outputs.push({output: layer_node.output, target_spec: layer.target, blend: layer.blend, opacity: layer.opacity, name: name})
}
// Handle masking if configured
if (this.mask_config) {
var content_root = this.mask_config.content()
var mask_root = this.mask_config.mask()
if (content_root && mask_root) {
var content_node = graph.add_node('render_view', {
root: content_root,
camera: this.world_view ? this.world_view.camera : this.ui_view.camera,
target: this.world_target_spec,
clear_color: {r: 0, g: 0, b: 0, a: 0}
})
var mask_node = graph.add_node('render_view', {
root: mask_root,
camera: this.world_view ? this.world_view.camera : this.ui_view.camera,
target: this.world_target_spec,
clear_color: {r: 0, g: 0, b: 0, a: 0}
})
var masked_node = graph.add_node('mask', {
content: content_node.output,
mask: mask_node.output,
mode: this.mask_config.mode,
invert: this.mask_config.invert
})
outputs.push({output: masked_node.output, target_spec: this.world_target_spec, name: 'masked'})
}
}
// Render UI
if (ui_root && this.ui_view) {
var ui_node = graph.add_node('render_view', {
root: ui_root,
camera: this.ui_view.camera,
target: this.ui_view.target,
clear_color: {r: 0, g: 0, b: 0, a: 0}
})
outputs.push({output: ui_node.output, target_spec: this.ui_view.target, name: 'ui'})
}
// Blit all outputs to screen
var screen_clear = graph.add_node('render_view', {
root: {type: 'group', children: []},
camera: {pos: [0, 0], width: window_size.width, height: window_size.height, anchor: [0, 0], ortho: true},
target: 'screen',
clear_color: {r: 0, g: 0, b: 0, a: 1}
})
for (var out of outputs) {
var dst_rect = fit(out.target_spec, window_size)
graph.add_node('blit', {
input: out.output,
target: 'screen',
dst_rect: dst_rect,
filter: out.target_spec.filter || 'nearest'
})
}
// Present
var present_node = graph.add_node('present', {})
graph.set_output(present_node.output)
return graph
}
return function() {
return meme(compositor, {
world_view: null,
ui_view: null,
mask_config: null,
layers: {},
world_target_spec: {width: 1280, height: 720, filter: 'nearest'},
ui_target_spec: {width: 640, height: 360, filter: 'nearest'}
})
}