390 lines
10 KiB
Plaintext
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
|