Files
prosperon/effects.cm
2026-01-19 18:57:25 -06:00

390 lines
10 KiB
Plaintext

// effects.cm - Effect Registry with Built-in Effect Recipes
//
// Effects are defined as recipes that produce abstract render passes.
// The compositor uses these recipes to build render plans.
// Backends implement the actual shader logic.
var effects = {}
// Effect registry
var _effects = {}
effects.register = function(name, deff) {
_effects[name] = deff
}
effects.get = function(name) {
return _effects[name]
}
effects.list = function() {
return array(_effects)
}
// Built-in effect: Bloom
effects.register('bloom', {
type: 'multi_pass',
requires_target: true,
params: {
threshold: {default: 0.8, type: 'float'},
intensity: {default: 1.0, type: 'float'},
blur_passes: {default: 3, type: 'int'}
},
build_passes: function(input, output, params, ctx) {
var passes = []
var size = ctx.target_size
// Threshold extraction
var thresh_target = ctx.alloc_target(size.width, size.height, 'bloom_thresh')
passes.push({
type: 'shader',
shader: 'threshold',
input: input,
output: thresh_target,
uniforms: {
threshold: params.threshold != null ? params.threshold : 0.8,
intensity: params.intensity != null ? params.intensity : 1.0
}
})
// Blur ping-pong
var blur_a = ctx.alloc_target(size.width, size.height, 'bloom_blur_a')
var blur_b = ctx.alloc_target(size.width, size.height, 'bloom_blur_b')
var blur_src = thresh_target
var texel = {x: 1 / size.width, y: 1 / size.height}
var blur_count = params.blur_passes != null ? params.blur_passes : 3
for (var i = 0; i < blur_count; i++) {
passes.push({
type: 'shader',
shader: 'blur',
input: blur_src,
output: blur_a,
uniforms: {direction: {x: 2, y: 0}, texel_size: texel}
})
passes.push({
type: 'shader',
shader: 'blur',
input: blur_a,
output: blur_b,
uniforms: {direction: {x: 0, y: 2}, texel_size: texel}
})
blur_src = blur_b
}
// Additive composite
passes.push({
type: 'composite',
base: input,
overlay: blur_src,
output: output,
blend: 'add'
})
return passes
}
})
// Built-in effect: Mask
effects.register('mask', {
type: 'conditional',
requires_target: true,
params: {
source: {required: false}, // Legacy: direct handle reference
source_id: {required: false}, // New: ID string for film2d.get()
channel: {default: 'alpha'},
invert: {default: false},
soft: {default: false},
space: {default: 'local'}
},
build_passes: function(input, output, params, ctx) {
var passes = []
var size = ctx.target_size
// Check backend capabilities for stencil optimization
if (!params.soft && ctx.backend && ctx.backend.caps && ctx.backend.caps.has_stencil) {
// Could use stencil - but for now use texture approach
}
if (ctx.backend && ctx.backend.caps && !ctx.backend.caps.has_render_targets) {
// Can't do masks on this backend - just pass through
return [{type: 'blit', source: input, dest: output}]
}
// Resolve mask source
var mask_source = params.source
if (params.source_id && ctx.film2d) {
mask_source = ctx.film2d.get(params.source_id)
}
if (!mask_source) {
// No mask source - pass through
return [{type: 'blit', source: input, dest: output}]
}
// Render mask source to target
var mask_target = ctx.alloc_target(size.width, size.height, 'mask_src')
passes.push({
type: 'render_subtree',
root: mask_source,
output: mask_target,
clear: {r: 0, g: 0, b: 0, a: 0},
space: params.space || 'local'
})
// Apply mask shader
passes.push({
type: 'shader',
shader: 'mask',
inputs: [input, mask_target],
output: output,
uniforms: {
channel: params.channel == 'alpha' ? 0 : 1,
invert: params.invert ? 1 : 0
}
})
return passes
}
})
// Built-in effect: CRT
effects.register('crt', {
type: 'single_pass',
requires_target: true,
params: {
curvature: {default: 0.1},
scanline_intensity: {default: 0.3},
vignette: {default: 0.2}
},
build_passes: function(input, output, params, ctx) {
return [{
type: 'shader',
shader: 'crt',
input: input,
output: output,
uniforms: {
curvature: params.curvature != null ? params.curvature : 0.1,
scanline_intensity: params.scanline_intensity != null ? params.scanline_intensity : 0.3,
vignette: params.vignette != null ? params.vignette : 0.2,
resolution: {width: ctx.target_size.width, height: ctx.target_size.height}
}
}]
}
})
// Built-in effect: Blur
effects.register('blur', {
type: 'multi_pass',
requires_target: true,
params: {
passes: {default: 2}
},
build_passes: function(input, output, params, ctx) {
var passes = []
var size = ctx.target_size
var texel = {x: 1 / size.width, y: 1 / size.height}
var blur_a = ctx.alloc_target(size.width, size.height, 'blur_a')
var blur_b = ctx.alloc_target(size.width, size.height, 'blur_b')
var src = input
var blur_count = params.passes != null ? params.passes : 2
for (var i = 0; i < blur_count; i++) {
passes.push({
type: 'shader',
shader: 'blur',
input: src,
output: blur_a,
uniforms: {direction: {x: 2, y: 0}, texel_size: texel}
})
passes.push({
type: 'shader',
shader: 'blur',
input: blur_a,
output: blur_b,
uniforms: {direction: {x: 0, y: 2}, texel_size: texel}
})
src = blur_b
}
// Final blit to output
passes.push({type: 'blit', source: src, dest: output})
return passes
}
})
// Built-in effect: Accumulator (motion blur / trails)
effects.register('accumulator', {
type: 'stateful',
requires_target: true,
params: {
decay: {default: 0.9}
},
build_passes: function(input, output, params, ctx) {
var size = ctx.target_size
var prev = ctx.get_persistent_target('accum_prev', size.width, size.height)
var curr = ctx.get_persistent_target('accum_curr', size.width, size.height)
return [
{
type: 'shader',
shader: 'accumulator',
inputs: [input, prev],
output: curr,
uniforms: {decay: params.decay != null ? params.decay : 0.9}
},
{type: 'blit', source: curr, dest: prev},
{type: 'blit', source: curr, dest: output}
]
}
})
// Built-in effect: Pixelate
effects.register('pixelate', {
type: 'single_pass',
requires_target: true,
params: {
pixel_size: {default: 4}
},
build_passes: function(input, output, params, ctx) {
return [{
type: 'shader',
shader: 'pixelate',
input: input,
output: output,
uniforms: {
pixel_size: params.pixel_size != null ? params.pixel_size : 4,
resolution: {width: ctx.target_size.width, height: ctx.target_size.height}
}
}]
}
})
// Built-in effect: Color grading
effects.register('color_grade', {
type: 'single_pass',
requires_target: true,
params: {
brightness: {default: 0},
contrast: {default: 1},
saturation: {default: 1},
gamma: {default: 1}
},
build_passes: function(input, output, params, ctx) {
return [{
type: 'shader',
shader: 'color_grade',
input: input,
output: output,
uniforms: {
brightness: params.brightness != null ? params.brightness : 0,
contrast: params.contrast != null ? params.contrast : 1,
saturation: params.saturation != null ? params.saturation : 1,
gamma: params.gamma != null ? params.gamma : 1
}
}]
}
})
// Built-in effect: Vignette
effects.register('vignette', {
type: 'single_pass',
requires_target: true,
params: {
intensity: {default: 0.3},
softness: {default: 0.5}
},
build_passes: function(input, output, params, ctx) {
return [{
type: 'shader',
shader: 'vignette',
input: input,
output: output,
uniforms: {
intensity: params.intensity != null ? params.intensity : 0.3,
softness: params.softness != null ? params.softness : 0.5,
resolution: {width: ctx.target_size.width, height: ctx.target_size.height}
}
}]
}
})
// Built-in effect: Chromatic aberration
effects.register('chromatic', {
type: 'single_pass',
requires_target: true,
params: {
offset: {default: 0.005}
},
build_passes: function(input, output, params, ctx) {
return [{
type: 'shader',
shader: 'chromatic',
input: input,
output: output,
uniforms: {
offset: params.offset != null ? params.offset : 0.005
}
}]
}
})
// Built-in effect: Outline
effects.register('outline', {
type: 'single_pass',
requires_target: true,
params: {
color: {default: {r: 0, g: 0, b: 0, a: 1}},
width: {default: 1}
},
build_passes: function(input, output, params, ctx) {
var c = params.color || {r: 0, g: 0, b: 0, a: 1}
return [{
type: 'shader',
shader: 'outline',
input: input,
output: output,
uniforms: {
outline_color: c,
outline_width: params.width != null ? params.width : 1,
texel_size: {x: 1 / ctx.target_size.width, y: 1 / ctx.target_size.height}
}
}]
}
})
// Helper: Check if an effect requires a render target
effects.requires_target = function(effect_type) {
var deff = _effects[effect_type]
return deff ? (deff.requires_target || false) : false
}
// Helper: Get default params for an effect
effects.default_params = function(effect_type) {
var deff = _effects[effect_type]
if (!deff || !deff.params) return {}
var defaults = {}
arrfor(array(deff.params), k => {
if (deff.params[k].default != null) {
defaults[k] = deff.params[k].default
}
})
return defaults
}
// Helper: Validate effect params
effects.validate_params = function(effect_type, params) {
var deff = _effects[effect_type]
if (!deff || !deff.params) return true
arrfor(array(deff.params), k => {
if (deff.params[k].required && params[k] == null) {
return false
}
})
return true
}
return effects