189 lines
5.7 KiB
Plaintext
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'}
|
|
})
|
|
}
|