// 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') push(passes, { 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++) { push(passes, { type: 'shader', shader: 'blur', input: blur_src, output: blur_a, uniforms: {direction: {x: 2, y: 0}, texel_size: texel} }) push(passes, { 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 push(passes, { 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') push(passes, { 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 push(passes, { 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++) { push(passes, { type: 'shader', shader: 'blur', input: src, output: blur_a, uniforms: {direction: {x: 2, y: 0}, texel_size: texel} }) push(passes, { 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 push(passes, {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