596 lines
16 KiB
Plaintext
596 lines
16 KiB
Plaintext
// compositor.cm - Unified Compositor (Rewritten)
|
|
//
|
|
// The compositor is the SINGLE OWNER of all effects.
|
|
// It takes scene descriptions and produces abstract render plans.
|
|
//
|
|
// Architecture:
|
|
// Scene Tree (Retained) -> Compositor (Effect orchestration) -> Render Plan -> Backend
|
|
//
|
|
// The compositor does NOT know about sprites/tilemaps/text - that's renderer territory.
|
|
// It only knows about plates, targets, viewports, effects, and post-processing.
|
|
|
|
var effects_mod = use('effects')
|
|
|
|
var compositor = {}
|
|
|
|
// Presentation modes
|
|
compositor.PRESENTATION = {
|
|
DISABLED: 'disabled',
|
|
STRETCH: 'stretch',
|
|
LETTERBOX: 'letterbox',
|
|
OVERSCAN: 'overscan',
|
|
INTEGER_SCALE: 'integer_scale'
|
|
}
|
|
|
|
// Blend modes
|
|
compositor.BLEND = {
|
|
REPLACE: 'replace',
|
|
OVER: 'over',
|
|
ADD: 'add'
|
|
}
|
|
|
|
// Compile a composition tree into a render plan
|
|
compositor.compile = function(comp, renderers, backend) {
|
|
var ctx = {
|
|
renderers: renderers,
|
|
backend: backend,
|
|
passes: [],
|
|
targets: {},
|
|
persistent_targets: {},
|
|
target_counter: 0,
|
|
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}
|
|
}
|
|
}
|
|
|
|
// Get screen size from backend
|
|
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.height) ctx.screen_size.height = 720
|
|
|
|
// Normalize to w/h
|
|
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 {
|
|
passes: ctx.passes,
|
|
targets: ctx.targets,
|
|
persistent_targets: ctx.persistent_targets,
|
|
screen_size: ctx.screen_size
|
|
}
|
|
}
|
|
|
|
// Compile a single node
|
|
function _compile_node(node, ctx, parent_target, parent_size) {
|
|
if (!node) return {output: null}
|
|
|
|
var node_type = node.type
|
|
var owns_target = false
|
|
var target = parent_target
|
|
var target_size = {w: parent_size.w || parent_size.width, h: parent_size.h || parent_size.height}
|
|
|
|
// Determine target ownership
|
|
if (node.target == 'screen') {
|
|
owns_target = true
|
|
target = 'screen'
|
|
target_size = {w: ctx.screen_size.w, h: ctx.screen_size.h}
|
|
} else if (node.resolution) {
|
|
owns_target = true
|
|
target_size = {w: node.resolution.w || node.resolution.width, h: node.resolution.h || node.resolution.height}
|
|
target = ctx.alloc_target(target_size.w, target_size.h, node.name || 'res_target')
|
|
} else if (node.effects && _effects_require_target(node.effects, ctx)) {
|
|
owns_target = true
|
|
target = ctx.alloc_target(target_size.w, target_size.h, node.name || 'effect_target')
|
|
}
|
|
|
|
ctx.target_size = target_size
|
|
|
|
// Handle clear
|
|
if (owns_target && node.clear) {
|
|
ctx.passes.push({
|
|
type: 'clear',
|
|
target: target,
|
|
color: node.clear
|
|
})
|
|
}
|
|
|
|
// Process by type
|
|
if (node_type == 'composition') {
|
|
return _compile_composition(node, ctx, target, target_size)
|
|
} else if (node_type == 'group') {
|
|
return _compile_group(node, ctx, target, target_size, parent_target, parent_size)
|
|
} else if (_is_renderer_type(node_type)) {
|
|
return _compile_renderer(node, ctx, target, target_size, parent_target, parent_size)
|
|
} else if (node_type == 'imgui') {
|
|
ctx.passes.push({
|
|
type: 'imgui',
|
|
target: target,
|
|
target_size: target_size,
|
|
draw: node.draw,
|
|
rect: node.rect
|
|
})
|
|
return {output: target}
|
|
}
|
|
|
|
return {output: target}
|
|
}
|
|
|
|
// Compile composition root
|
|
function _compile_composition(node, ctx, target, target_size) {
|
|
var layers = node.layers || []
|
|
|
|
for (var i = 0; i < layers.length; i++) {
|
|
var layer = layers[i]
|
|
if (!layer.blend) {
|
|
layer.blend = (i == 0) ? 'replace' : 'over'
|
|
}
|
|
_compile_node(layer, ctx, target, target_size)
|
|
}
|
|
|
|
return {output: target}
|
|
}
|
|
|
|
// Compile group with effects
|
|
function _compile_group(node, ctx, target, target_size, parent_target, parent_size) {
|
|
var layers = node.layers || []
|
|
var original_target = target
|
|
|
|
// Process child layers
|
|
for (var i = 0; i < layers.length; i++) {
|
|
var layer = layers[i]
|
|
if (!layer.blend) {
|
|
layer.blend = (i == 0) ? 'replace' : 'over'
|
|
}
|
|
_compile_node(layer, ctx, target, target_size)
|
|
}
|
|
|
|
// Apply effects - this is where the compositor owns all effect logic
|
|
if (node.effects && node.effects.length > 0) {
|
|
for (var j = 0; j < node.effects.length; j++) {
|
|
var effect = node.effects[j]
|
|
target = _compile_effect(effect, ctx, target, target_size, node.name || ('group_' + ctx.target_counter))
|
|
}
|
|
}
|
|
|
|
// Composite back to parent if needed
|
|
var needs_composite = (original_target != parent_target || target != original_target) && parent_target
|
|
if (needs_composite) {
|
|
var presentation = node.presentation || 'disabled'
|
|
var blend = node.blend || 'over'
|
|
|
|
if (parent_target == 'screen') {
|
|
ctx.passes.push({
|
|
type: 'blit_to_screen',
|
|
source: target,
|
|
source_size: target_size,
|
|
dest_size: parent_size,
|
|
presentation: presentation,
|
|
pos: node.pos
|
|
})
|
|
} else {
|
|
ctx.passes.push({
|
|
type: 'composite',
|
|
source: target,
|
|
dest: parent_target,
|
|
source_size: target_size,
|
|
dest_size: parent_size,
|
|
presentation: presentation,
|
|
blend: blend,
|
|
pos: node.pos
|
|
})
|
|
}
|
|
}
|
|
|
|
return {output: target}
|
|
}
|
|
|
|
// Compile renderer layer (film2d, forward3d, etc.)
|
|
function _compile_renderer(node, ctx, target, target_size, parent_target, parent_size) {
|
|
var renderer_type = node.type
|
|
var renderer = ctx.renderers[renderer_type]
|
|
|
|
if (!renderer) {
|
|
log.console(`compositor: Unknown renderer: ${renderer_type}`)
|
|
return {output: target}
|
|
}
|
|
|
|
var layer_target = target
|
|
var layer_size = target_size
|
|
var owns_target = false
|
|
|
|
// Check for resolution override
|
|
if (node.resolution) {
|
|
owns_target = true
|
|
layer_size = {w: node.resolution.w, h: node.resolution.h}
|
|
layer_target = ctx.alloc_target(layer_size.w, layer_size.h, node.name || renderer_type + '_target')
|
|
}
|
|
|
|
// Clear if we own target
|
|
if (owns_target && node.clear) {
|
|
ctx.passes.push({
|
|
type: 'clear',
|
|
target: layer_target,
|
|
color: node.clear
|
|
})
|
|
}
|
|
|
|
// Emit render pass
|
|
ctx.passes.push({
|
|
type: 'render',
|
|
renderer: renderer_type,
|
|
root: node.root,
|
|
camera: node.camera,
|
|
target: layer_target,
|
|
target_size: layer_size,
|
|
blend: node.blend || 'over',
|
|
clear: owns_target ? node.clear : null
|
|
})
|
|
|
|
// Composite back to parent
|
|
if (owns_target && parent_target) {
|
|
var presentation = node.presentation || 'disabled'
|
|
var blend = node.blend || 'over'
|
|
|
|
ctx.passes.push({
|
|
type: 'composite',
|
|
source: layer_target,
|
|
dest: parent_target,
|
|
source_size: layer_size,
|
|
dest_size: parent_size,
|
|
presentation: presentation,
|
|
blend: blend,
|
|
pos: node.pos
|
|
})
|
|
}
|
|
|
|
return {output: layer_target}
|
|
}
|
|
|
|
// Compile an effect using the effect registry
|
|
function _compile_effect(effect, ctx, input_target, target_size, node_id) {
|
|
var effect_type = effect.type
|
|
var effect_def = effects_mod.get(effect_type)
|
|
|
|
if (!effect_def) {
|
|
log.console(`compositor: Unknown effect: ${effect_type}`)
|
|
return input_target
|
|
}
|
|
|
|
// Build effect context
|
|
var effect_ctx = {
|
|
backend: ctx.backend,
|
|
target_size: {w: target_size.w, h: target_size.h},
|
|
alloc_target: function(w, h, hint) {
|
|
return ctx.alloc_target(w, h, hint)
|
|
},
|
|
get_persistent_target: function(key, w, h) {
|
|
return ctx.get_persistent_target(node_id + '_' + key, w, h)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Check if type is a renderer
|
|
function _is_renderer_type(type) {
|
|
return type == 'film2d' || type == 'forward3d' || type == 'imgui' || type == 'retro3d' || type == 'simple2d'
|
|
}
|
|
|
|
// Calculate presentation rect
|
|
compositor.calculate_presentation_rect = function(source_size, dest_size, mode) {
|
|
var sw = source_size.w || source_size.width
|
|
var sh = source_size.h || source_size.height
|
|
var dw = dest_size.w || dest_size.width
|
|
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 == 'stretch') {
|
|
return {x: 0, y: 0, width: dw, height: dh}
|
|
}
|
|
|
|
var src_aspect = sw / sh
|
|
var dst_aspect = dw / dh
|
|
|
|
if (mode == 'letterbox') {
|
|
var scale = src_aspect > dst_aspect ? dw / sw : dh / sh
|
|
var w = sw * scale
|
|
var h = sh * scale
|
|
return {x: (dw - w) / 2, y: (dh - h) / 2, width: w, height: h}
|
|
}
|
|
|
|
if (mode == 'overscan') {
|
|
var scale = src_aspect > dst_aspect ? dh / sh : dw / sw
|
|
var w = sw * scale
|
|
var h = sh * scale
|
|
return {x: (dw - w) / 2, y: (dh - h) / 2, width: w, height: h}
|
|
}
|
|
|
|
if (mode == 'integer_scale') {
|
|
var scale_x = number.floor(dw / sw)
|
|
var scale_y = number.floor(dh / sh)
|
|
var scale = number.max(1, number.min(scale_x, scale_y))
|
|
var w = sw * scale
|
|
var h = sh * scale
|
|
return {x: (dw - w) / 2, y: (dh - h) / 2, width: w, height: h}
|
|
}
|
|
|
|
return {x: 0, y: 0, width: sw, height: sh}
|
|
}
|
|
|
|
// Execute a compiled render plan
|
|
compositor.execute = function(plan, renderers, backend) {
|
|
var target_cache = {}
|
|
|
|
// Pre-allocate targets
|
|
for (var key in plan.targets) {
|
|
var spec = plan.targets[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
|
|
function resolve_target(t) {
|
|
if (t == 'screen') return 'screen'
|
|
if (t && t.type == 'target') return target_cache[t.key]
|
|
return t
|
|
}
|
|
|
|
// Execute passes
|
|
var commands = []
|
|
|
|
for (var i = 0; i < plan.passes.length; i++) {
|
|
var pass = plan.passes[i]
|
|
var pass_cmds = _execute_pass(pass, renderers, backend, resolve_target, compositor)
|
|
for (var j = 0; j < pass_cmds.length; j++) {
|
|
commands.push(pass_cmds[j])
|
|
}
|
|
}
|
|
|
|
return {commands: commands}
|
|
}
|
|
|
|
// Execute a single pass
|
|
function _execute_pass(pass, renderers, backend, resolve_target, comp) {
|
|
var commands = []
|
|
var source
|
|
|
|
switch (pass.type) {
|
|
case 'clear':
|
|
var target = resolve_target(pass.target)
|
|
commands.push({cmd: 'begin_render', target: target, clear: pass.color})
|
|
commands.push({cmd: 'end_render'})
|
|
break
|
|
|
|
case 'render':
|
|
var renderer = renderers[pass.renderer]
|
|
if (renderer && renderer.render) {
|
|
var target = resolve_target(pass.target)
|
|
var result = renderer.render({
|
|
root: pass.root,
|
|
camera: pass.camera,
|
|
target: target,
|
|
target_size: pass.target_size,
|
|
blend: pass.blend,
|
|
clear: pass.clear
|
|
}, backend)
|
|
if (result && result.commands) {
|
|
for (var i = 0; i < result.commands.length; i++) {
|
|
commands.push(result.commands[i])
|
|
}
|
|
}
|
|
}
|
|
break
|
|
|
|
case 'shader_pass':
|
|
var input = resolve_target(pass.input)
|
|
var output = resolve_target(pass.output)
|
|
var extra = []
|
|
if (pass.extra_inputs) {
|
|
for (var i = 0; i < pass.extra_inputs.length; i++) {
|
|
extra.push(resolve_target(pass.extra_inputs[i]))
|
|
}
|
|
}
|
|
commands.push({
|
|
cmd: 'shader_pass',
|
|
shader: pass.shader,
|
|
input: input,
|
|
output: output,
|
|
extra_inputs: extra,
|
|
uniforms: pass.uniforms
|
|
})
|
|
break
|
|
|
|
case 'composite': {
|
|
source = resolve_target(pass.source)
|
|
var dest = resolve_target(pass.dest)
|
|
var rect = comp.calculate_presentation_rect(pass.source_size, pass.dest_size, pass.presentation)
|
|
if (pass.pos) {
|
|
rect.x += (pass.pos.x || 0)
|
|
rect.y += (pass.pos.y || 0)
|
|
}
|
|
var filter = pass.presentation == 'integer_scale' ? 'nearest' : 'linear'
|
|
commands.push({
|
|
cmd: 'blit',
|
|
texture: source,
|
|
target: dest,
|
|
dst_rect: rect,
|
|
filter: filter
|
|
})
|
|
}
|
|
break
|
|
|
|
case 'blit_to_screen': {
|
|
source = resolve_target(pass.source)
|
|
var rect = comp.calculate_presentation_rect(pass.source_size, pass.dest_size, pass.presentation)
|
|
if (pass.pos) {
|
|
rect.x += (pass.pos.x || 0)
|
|
rect.y += (pass.pos.y || 0)
|
|
}
|
|
var filter = pass.presentation == 'integer_scale' ? 'nearest' : 'linear'
|
|
commands.push({
|
|
cmd: 'blit',
|
|
texture: source,
|
|
target: 'screen',
|
|
dst_rect: rect,
|
|
filter: filter
|
|
})
|
|
}
|
|
break
|
|
|
|
case 'composite_textures': {
|
|
var base = resolve_target(pass.base)
|
|
var overlay = resolve_target(pass.overlay)
|
|
var output = resolve_target(pass.output)
|
|
commands.push({
|
|
cmd: 'composite_textures',
|
|
base: base,
|
|
overlay: overlay,
|
|
output: output,
|
|
mode: pass.mode || 'over'
|
|
})
|
|
}
|
|
break
|
|
|
|
case 'apply_mask': {
|
|
var content = resolve_target(pass.content)
|
|
var mask = resolve_target(pass.mask)
|
|
var output = resolve_target(pass.output)
|
|
commands.push({
|
|
cmd: 'apply_mask',
|
|
content_texture: content,
|
|
mask_texture: mask,
|
|
output: output,
|
|
mode: pass.mode,
|
|
invert: pass.invert
|
|
})
|
|
}
|
|
break
|
|
|
|
case 'imgui': {
|
|
commands.push({
|
|
cmd: 'imgui',
|
|
draw: pass.draw,
|
|
rect: pass.rect,
|
|
target: resolve_target(pass.target)
|
|
})
|
|
}
|
|
break
|
|
|
|
case 'render_mask_source': {
|
|
var target = resolve_target(pass.target)
|
|
var renderer = renderers['film2d']
|
|
if (renderer && renderer.render) {
|
|
var result = renderer.render({
|
|
root: pass.source,
|
|
camera: {pos: [0, 0], width: pass.target_size.w, height: pass.target_size.h, anchor: [0, 0], ortho: true},
|
|
target: target,
|
|
target_size: pass.target_size,
|
|
blend: 'replace',
|
|
clear: {r: 0, g: 0, b: 0, a: 0}
|
|
}, backend)
|
|
if (result && result.commands) {
|
|
for (var i = 0; i < result.commands.length; i++) {
|
|
commands.push(result.commands[i])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break
|
|
}
|
|
|
|
return commands
|
|
}
|
|
|
|
return compositor
|