309 lines
9.0 KiB
Plaintext
309 lines
9.0 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 layer
|
|
var layers = config.layers || []
|
|
for (var i = 0; i < layers.length; i++)
|
|
compile_layer(layers[i], 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
|
|
|
|
// Get all sprites in this group
|
|
var all_sprites = film2d.query({group: group})
|
|
|
|
// 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 assigned = false
|
|
|
|
// Check if sprite belongs to any effect group
|
|
var sprite_groups = s.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
|
|
}
|
|
}
|
|
|
|
if (!assigned) base_sprites.push(s)
|
|
}
|
|
|
|
// Allocate layer target
|
|
var layer_target = ctx.alloc(res.width, res.height, layer.name)
|
|
|
|
// Clear layer
|
|
if (layer.clear)
|
|
ctx.passes.push({type: 'clear', target: layer_target, color: layer.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,
|
|
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(effect, current, res, gname, group_effects)
|
|
}
|
|
|
|
// Composite result to layer
|
|
ctx.passes.push({
|
|
type: 'composite',
|
|
source: current,
|
|
dest: layer_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: layer_target,
|
|
target_size: res,
|
|
clear: null // Don't clear, blend on top
|
|
})
|
|
}
|
|
|
|
// Composite layer to screen
|
|
ctx.passes.push({
|
|
type: 'blit_to_screen',
|
|
source: layer_target,
|
|
source_size: res,
|
|
dest_size: ctx.screen_size,
|
|
presentation: layer.presentation || 'stretch'
|
|
})
|
|
}
|
|
|
|
function apply_effect(effect, input, size, hint, group_effects) {
|
|
var output = 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')
|
|
|
|
// Threshold
|
|
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++) {
|
|
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}}})
|
|
blur_in = blur2
|
|
}
|
|
|
|
// Composite bloom
|
|
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})
|
|
|
|
if (mask_sprites.length > 0) {
|
|
var mask_target = alloc(size.width, size.height, hint + '_mask')
|
|
|
|
// Render mask
|
|
passes.push({
|
|
type: 'render',
|
|
renderer: 'film2d',
|
|
drawables: mask_sprites,
|
|
camera: null, // Same camera as parent? Need to pass this
|
|
target: mask_target,
|
|
target_size: size,
|
|
clear: {r: 0, g: 0, b: 0, a: 0}
|
|
})
|
|
|
|
// Apply mask
|
|
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
|
|
passes.push({type: 'blit', source: input, dest: output})
|
|
}
|
|
} else {
|
|
// Unknown effect, pass through
|
|
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,
|
|
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'
|
|
})
|
|
}
|
|
}
|
|
|
|
return {commands: commands}
|
|
}
|
|
|
|
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 = number.floor(dst.width / src.width)
|
|
var sy = number.floor(dst.height / src.height)
|
|
var s = number.max(1, number.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 = number.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 |